mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-02-21 07:29:37 +01:00
* fix: login without reload (#678) (#679) * fix: implements code review suggestions (#914) * refactor: adds scope to the app * refactor: handles null value from localstorage for username * refactor: removes text type from <Input /> * refactor: replaces isNull with isNil * refactor: improves makeLogin method * refactor: adds error from api constant * fix: updates error using API_ERROR constant in tests * refactor: updates regex for moduleMapper in jest config
This commit is contained in:
parent
1fc1a33c4c
commit
9cd3ccb408
3
.babelrc
3
.babelrc
@ -34,7 +34,8 @@
|
|||||||
}], "flow", "react"],
|
}], "flow", "react"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"transform-class-properties",
|
"transform-class-properties",
|
||||||
"transform-object-rest-spread"
|
"transform-object-rest-spread",
|
||||||
|
"babel-plugin-dynamic-import-node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"testOldEnv": {
|
"testOldEnv": {
|
||||||
|
@ -40,7 +40,7 @@ module.exports = {
|
|||||||
'<rootDir>/test',
|
'<rootDir>/test',
|
||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(scss)$': '<rootDir>/node_modules/identity-obj-proxy',
|
'\\.(s?css)$': '<rootDir>/node_modules/identity-obj-proxy',
|
||||||
'github-markdown-css': '<rootDir>/node_modules/identity-obj-proxy',
|
'github-markdown-css': '<rootDir>/node_modules/identity-obj-proxy',
|
||||||
'\\.(png)$': '<rootDir>/node_modules/identity-obj-proxy',
|
'\\.(png)$': '<rootDir>/node_modules/identity-obj-proxy',
|
||||||
'\\.(svg)$': '<rootDir>/test/unit/empty.js'
|
'\\.(svg)$': '<rootDir>/test/unit/empty.js'
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"babel-eslint": "8.2.6",
|
"babel-eslint": "8.2.6",
|
||||||
"babel-jest": "23.4.0",
|
"babel-jest": "23.4.0",
|
||||||
"babel-loader": "7.1.5",
|
"babel-loader": "7.1.5",
|
||||||
|
"babel-plugin-dynamic-import-node": "2.0.0",
|
||||||
"babel-plugin-flow-runtime": "0.17.0",
|
"babel-plugin-flow-runtime": "0.17.0",
|
||||||
"babel-plugin-syntax-dynamic-import": "6.18.0",
|
"babel-plugin-syntax-dynamic-import": "6.18.0",
|
||||||
"babel-plugin-transform-async-to-generator": "6.24.1",
|
"babel-plugin-transform-async-to-generator": "6.24.1",
|
||||||
@ -77,8 +78,8 @@
|
|||||||
"babel-register": "6.26.0",
|
"babel-register": "6.26.0",
|
||||||
"babel-runtime": "6.26.0",
|
"babel-runtime": "6.26.0",
|
||||||
"bundlesize": "0.17.0",
|
"bundlesize": "0.17.0",
|
||||||
"cross-env": "5.1.4",
|
|
||||||
"codecov": "3.0.4",
|
"codecov": "3.0.4",
|
||||||
|
"cross-env": "5.1.4",
|
||||||
"css-loader": "0.28.10",
|
"css-loader": "0.28.10",
|
||||||
"element-react": "1.4.8",
|
"element-react": "1.4.8",
|
||||||
"element-theme-default": "1.4.13",
|
"element-theme-default": "1.4.13",
|
||||||
@ -114,8 +115,8 @@
|
|||||||
"ora": "1.4.0",
|
"ora": "1.4.0",
|
||||||
"prop-types": "15.6.1",
|
"prop-types": "15.6.1",
|
||||||
"puppeteer": "1.1.1",
|
"puppeteer": "1.1.1",
|
||||||
"react": "16.2.0",
|
"react": "16.4.2",
|
||||||
"react-dom": "16.2.0",
|
"react-dom": "16.4.2",
|
||||||
"react-hot-loader": "4.2.0",
|
"react-hot-loader": "4.2.0",
|
||||||
"react-router-dom": "4.2.2",
|
"react-router-dom": "4.2.2",
|
||||||
"react-syntax-highlighter": "5.8.0",
|
"react-syntax-highlighter": "5.8.0",
|
||||||
|
@ -46,7 +46,7 @@ export function generateRandomHexString(length: number = 8) {
|
|||||||
|
|
||||||
export function signPayload(payload: JWTPayload, secret: string, options: JWTSignOptions) {
|
export function signPayload(payload: JWTPayload, secret: string, options: JWTSignOptions) {
|
||||||
return jwt.sign(payload, secret, {
|
return jwt.sign(payload, secret, {
|
||||||
notBefore: '1000', // Make sure the time will not rollback :)
|
notBefore: '1', // Make sure the time will not rollback :)
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
152
src/webui/app.js
152
src/webui/app.js
@ -1,8 +1,17 @@
|
|||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
|
import isNil from 'lodash/isNil';
|
||||||
import 'element-theme-default';
|
import 'element-theme-default';
|
||||||
import {i18n} from 'element-react';
|
import {i18n} from 'element-react';
|
||||||
import locale from 'element-react/src/locale/lang/en';
|
import locale from 'element-react/src/locale/lang/en';
|
||||||
|
|
||||||
|
import storage from './utils/storage';
|
||||||
|
import logo from './utils/logo';
|
||||||
|
import {makeLogin, isTokenExpire} from './utils/login';
|
||||||
|
|
||||||
|
import Header from './components/Header';
|
||||||
|
import Footer from './components/Footer';
|
||||||
|
import LoginModal from './components/Login';
|
||||||
|
|
||||||
i18n.use(locale);
|
i18n.use(locale);
|
||||||
|
|
||||||
import Route from './router';
|
import Route from './router';
|
||||||
@ -10,10 +19,147 @@ import Route from './router';
|
|||||||
import './styles/main.scss';
|
import './styles/main.scss';
|
||||||
import 'normalize.css';
|
import 'normalize.css';
|
||||||
|
|
||||||
export default class App extends React.Component {
|
export default class App extends Component {
|
||||||
|
state = {
|
||||||
|
error: {},
|
||||||
|
logoUrl: '',
|
||||||
|
user: {},
|
||||||
|
scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : '',
|
||||||
|
showLoginModal: false,
|
||||||
|
isUserLoggedIn: false
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
|
this.toggleLoginModal = this.toggleLoginModal.bind(this);
|
||||||
|
this.doLogin = this.doLogin.bind(this);
|
||||||
|
this.loadLogo = this.loadLogo.bind(this);
|
||||||
|
this.isUserAlreadyLoggedIn = this.isUserAlreadyLoggedIn.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadLogo();
|
||||||
|
this.isUserAlreadyLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserAlreadyLoggedIn() {
|
||||||
|
// checks for token validity
|
||||||
|
const token = storage.getItem('token');
|
||||||
|
const username = storage.getItem('username');
|
||||||
|
|
||||||
|
if (isTokenExpire(token) || isNil(username)) {
|
||||||
|
this.handleLogout();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
user: {username, token},
|
||||||
|
isUserLoggedIn: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadLogo() {
|
||||||
|
const logoUrl = await logo();
|
||||||
|
this.setState({logoUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toogles the login modal
|
||||||
|
* Required by: <LoginModal /> <Header />
|
||||||
|
*/
|
||||||
|
toggleLoginModal() {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
showLoginModal: !prevState.showLoginModal,
|
||||||
|
error: {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles login
|
||||||
|
* Required by: <Header />
|
||||||
|
*/
|
||||||
|
async doLogin(usernameValue, passwordValue) {
|
||||||
|
const {username, token, error} = await makeLogin(
|
||||||
|
usernameValue,
|
||||||
|
passwordValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (username && token) {
|
||||||
|
this.setState({
|
||||||
|
user: {
|
||||||
|
username,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
storage.setItem('username', username);
|
||||||
|
storage.setItem('token', token);
|
||||||
|
// close login modal after successful login
|
||||||
|
// set userLoggined to true
|
||||||
|
this.setState({
|
||||||
|
isUserLoggedIn: true,
|
||||||
|
showLoginModal: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.setState({
|
||||||
|
user: {},
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logouts user
|
||||||
|
* Required by: <Header />
|
||||||
|
*/
|
||||||
|
handleLogout() {
|
||||||
|
storage.removeItem('username');
|
||||||
|
storage.removeItem('token');
|
||||||
|
this.setState({
|
||||||
|
user: {},
|
||||||
|
isUserLoggedIn: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader() {
|
||||||
|
const {
|
||||||
|
logoUrl,
|
||||||
|
user,
|
||||||
|
scope,
|
||||||
|
} = this.state;
|
||||||
|
return <Header
|
||||||
|
logo={logoUrl}
|
||||||
|
username={user.username}
|
||||||
|
scope={scope}
|
||||||
|
toggleLoginModal={this.toggleLoginModal}
|
||||||
|
handleLogout={this.handleLogout}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoginModal() {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
showLoginModal
|
||||||
|
} = this.state;
|
||||||
|
return <LoginModal
|
||||||
|
visibility={showLoginModal}
|
||||||
|
error={error}
|
||||||
|
onChange={this.setUsernameAndPassword}
|
||||||
|
onCancel={this.toggleLoginModal}
|
||||||
|
onSubmit={this.doLogin}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {isUserLoggedIn} = this.state;
|
||||||
return (
|
return (
|
||||||
<Route />
|
<div className="page-full-height">
|
||||||
|
{this.renderHeader()}
|
||||||
|
{this.renderLoginModal()}
|
||||||
|
<Route isUserLoggedIn={isUserLoggedIn} />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,5 +41,9 @@ .header {
|
|||||||
background-color: $saltpan;
|
background-color: $saltpan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usernameField {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,245 +1,68 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Form, Button, Dialog, Input, Alert} from 'element-react';
|
import PropTypes from 'prop-types';
|
||||||
import isString from 'lodash/isString';
|
import {Button} from 'element-react';
|
||||||
import isNumber from 'lodash/isNumber';
|
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import {Link} from 'react-router-dom';
|
|
||||||
import {Base64} from 'js-base64';
|
|
||||||
|
|
||||||
import API from '../../utils/api';
|
|
||||||
import storage from '../../utils/storage';
|
|
||||||
import {getRegistryURL} from '../../utils/url';
|
import {getRegistryURL} from '../../utils/url';
|
||||||
import {HEADERS} from '../../../lib/constants';
|
|
||||||
|
|
||||||
import classes from './header.scss';
|
import classes from './header.scss';
|
||||||
import './logo.png';
|
import './logo.png';
|
||||||
|
|
||||||
export default class Header extends React.Component {
|
const Header = ({
|
||||||
state = {
|
logo = '',
|
||||||
showLogin: false,
|
scope = '',
|
||||||
username: '',
|
username = '',
|
||||||
password: '',
|
handleLogout = () => {},
|
||||||
logo: '',
|
toggleLoginModal = () => {}
|
||||||
loginError: null,
|
}) => {
|
||||||
scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : ''
|
const registryUrl = getRegistryURL();
|
||||||
};
|
return (
|
||||||
|
<header className={classes.header}>
|
||||||
|
<div className={classes.headerWrap}>
|
||||||
|
<a href="/#/">
|
||||||
|
<img src={logo} className={classes.logo} />
|
||||||
|
</a>
|
||||||
|
<figure>
|
||||||
|
npm set {scope}
|
||||||
|
registry {registryUrl}
|
||||||
|
<br />
|
||||||
|
npm adduser --registry {registryUrl}
|
||||||
|
</figure>
|
||||||
|
|
||||||
constructor(props) {
|
<div className={classes.headerRight}>
|
||||||
super(props);
|
{username ? (
|
||||||
this.toggleLoginModal = this.toggleLoginModal.bind(this);
|
<div className="user-logged">
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
<span
|
||||||
this.handleInput = this.handleInput.bind(this);
|
className={`user-logged-greetings ${classes.usernameField}`}
|
||||||
this.loadLogo = this.loadLogo.bind(this);
|
|
||||||
this.renderUserActionButton = this.renderUserActionButton.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLoginModal() {
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
showLogin: !prevState.showLogin
|
|
||||||
}));
|
|
||||||
this.setState({loginError: null});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput(name, e) {
|
|
||||||
this.setState({
|
|
||||||
[name]: e
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.loadLogo();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadLogo() {
|
|
||||||
try {
|
|
||||||
const logo = await API.request('logo');
|
|
||||||
this.setState({logo});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (this.state.username === '' || this.state.password === '') {
|
|
||||||
return this.setState({
|
|
||||||
loginError: {
|
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
|
||||||
description: 'Username or password can\'t be empty!'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const credentials = {
|
|
||||||
username: this.state.username,
|
|
||||||
password: this.state.password
|
|
||||||
};
|
|
||||||
const resp = await API.request(`login`, 'POST', {
|
|
||||||
body: JSON.stringify(credentials),
|
|
||||||
headers: {
|
|
||||||
Accept: HEADERS.JSON,
|
|
||||||
'Content-Type': HEADERS.JSON
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
storage.setItem('token', resp.token);
|
|
||||||
storage.setItem('username', resp.username);
|
|
||||||
location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
const errorObj = {
|
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
|
||||||
description: e.error
|
|
||||||
};
|
|
||||||
this.setState({loginError: errorObj});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isTokenExpire() {
|
|
||||||
const token = storage.getItem('token');
|
|
||||||
|
|
||||||
if (!isString(token)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = token.split('.')[1];
|
|
||||||
|
|
||||||
if (!payload) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(Base64.decode(payload));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Invalid token:', err, token); // eslint-disable-line
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.exp || !isNumber(payload.exp)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Report as expire before (real expire time - 30s)
|
|
||||||
const jsTimestamp = payload.exp * 1000 - 30000;
|
|
||||||
const expired = Date.now() >= jsTimestamp;
|
|
||||||
|
|
||||||
if (expired) {
|
|
||||||
storage.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLogout() {
|
|
||||||
storage.clear();
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUserActionButton() {
|
|
||||||
if (!this.isTokenExpire) {
|
|
||||||
// TODO: Check jwt token expire
|
|
||||||
const username = capitalize(storage.getItem('username'));
|
|
||||||
return (
|
|
||||||
<div className="user-logged">
|
|
||||||
<span
|
|
||||||
className="user-logged-greetings"
|
|
||||||
style={{marginRight: '10px'}}
|
|
||||||
>
|
|
||||||
Hi, {username}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
className={`${classes.headerButton} header-button-logout`}
|
|
||||||
type="danger"
|
|
||||||
onClick={this.handleLogout}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={`${classes.headerButton} header-button-login`}
|
|
||||||
onClick={this.toggleLoginModal}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const registryURL = getRegistryURL();
|
|
||||||
const {logo, scope, loginError, showLogin} = this.state;
|
|
||||||
return (
|
|
||||||
<header className={classes.header}>
|
|
||||||
<div className={classes.headerWrap}>
|
|
||||||
<Link to="/">
|
|
||||||
<img src={logo} className={classes.logo} />
|
|
||||||
</Link>
|
|
||||||
<figure>
|
|
||||||
npm set { scope }registry { registryURL }
|
|
||||||
<br/>
|
|
||||||
npm adduser --registry { registryURL }
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<div className={classes.headerRight}>
|
|
||||||
{this.renderUserActionButton()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
title="Login"
|
|
||||||
size="tiny"
|
|
||||||
visible={showLogin}
|
|
||||||
onCancel={this.toggleLoginModal}
|
|
||||||
>
|
|
||||||
<Form className="login-form">
|
|
||||||
<Dialog.Body>
|
|
||||||
{loginError && (
|
|
||||||
<Alert
|
|
||||||
title={loginError.title}
|
|
||||||
type={loginError.type}
|
|
||||||
description={loginError.description}
|
|
||||||
showIcon={true}
|
|
||||||
closable={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<br />
|
|
||||||
<Input
|
|
||||||
name="username"
|
|
||||||
placeholder="Username"
|
|
||||||
onChange={this.handleInput.bind(this, 'username')}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Type your password"
|
|
||||||
onChange={this.handleInput.bind(this, 'password')}
|
|
||||||
/>
|
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer className="dialog-footer">
|
|
||||||
<Button
|
|
||||||
onClick={this.toggleLoginModal}
|
|
||||||
className="cancel-login-button"
|
|
||||||
>
|
>
|
||||||
Cancel
|
Hi, {capitalize(username)}
|
||||||
</Button>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
nativeType="submit"
|
className={`${classes.headerButton} header-button-logout`}
|
||||||
className="login-button"
|
type="danger"
|
||||||
onClick={this.handleSubmit}
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
Login
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</div>
|
||||||
</Form>
|
) : (
|
||||||
</Dialog>
|
<Button
|
||||||
</header>
|
className={`${classes.headerButton} header-button-login`}
|
||||||
);
|
onClick={toggleLoginModal}
|
||||||
}
|
>
|
||||||
}
|
Login
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
logo: PropTypes.string,
|
||||||
|
scope: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
handleLogout: PropTypes.func.isRequired,
|
||||||
|
toggleLoginModal: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
111
src/webui/components/Login/index.js
Normal file
111
src/webui/components/Login/index.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {Form, Button, Dialog, Input, Alert} from 'element-react';
|
||||||
|
|
||||||
|
export default class LoginModal extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
visibility: PropTypes.bool,
|
||||||
|
error: PropTypes.object,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onSubmit: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
visibility: true,
|
||||||
|
error: {},
|
||||||
|
onCancel: () => {},
|
||||||
|
onSubmit: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.submitCredentials = this.submitCredentials.bind(this);
|
||||||
|
this.setCredentials = this.setCredentials.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set login modal's username and password to current state
|
||||||
|
* Required by: <LoginModal />
|
||||||
|
*/
|
||||||
|
setCredentials(name, e) {
|
||||||
|
this.setState({
|
||||||
|
[name]: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitCredentials(event) {
|
||||||
|
// prevents default submit behaviour
|
||||||
|
event.preventDefault();
|
||||||
|
const {username, password} = this.state;
|
||||||
|
await this.props.onSubmit(username, password);
|
||||||
|
// let's wait for API response and then set
|
||||||
|
// username and password filed to empty state
|
||||||
|
this.setState({username: '', password: ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoginError({type, title, description} = {}) {
|
||||||
|
return type ? (
|
||||||
|
<Alert
|
||||||
|
title={title}
|
||||||
|
type={type}
|
||||||
|
description={description}
|
||||||
|
showIcon={true}
|
||||||
|
closable={false}
|
||||||
|
/>
|
||||||
|
) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {visibility, onCancel, error} = this.props;
|
||||||
|
const {username, password} = this.state;
|
||||||
|
return (
|
||||||
|
<div className="login-dialog">
|
||||||
|
<Dialog
|
||||||
|
title="Login"
|
||||||
|
size="tiny"
|
||||||
|
visible={visibility}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<Form className="login-form">
|
||||||
|
<Dialog.Body>
|
||||||
|
{this.renderLoginError(error)}
|
||||||
|
<br />
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={this.setCredentials.bind(this, 'username')}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Type your password"
|
||||||
|
value={password}
|
||||||
|
onChange={this.setCredentials.bind(this, 'password')}
|
||||||
|
/>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer className="dialog-footer">
|
||||||
|
<Button onClick={onCancel} className="cancel-login-button">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
nativeType="submit"
|
||||||
|
className="login-button"
|
||||||
|
onClick={this.submitCredentials}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Form>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {Loading} from 'element-react';
|
import {Loading} from 'element-react';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
@ -12,9 +12,10 @@ import PackageSidebar from '../../components/PackageSidebar/index';
|
|||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
const loadingMessage = 'Loading...';
|
||||||
|
|
||||||
export default class Detail extends React.Component {
|
export default class Detail extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
match: PropTypes.object
|
match: PropTypes.object,
|
||||||
|
isUserLoggedIn: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -23,7 +24,7 @@ export default class Detail extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getPackageName(props = this.props) {
|
getPackageName(props = this.props) {
|
||||||
let params = props.match.params;
|
const params = props.match.params;
|
||||||
return `${(params.scope && '@' + params.scope + '/') || ''}${params.package}`;
|
return `${(params.scope && '@' + params.scope + '/') || ''}${params.package}`;
|
||||||
}
|
}
|
||||||
get packageName() {
|
get packageName() {
|
||||||
@ -34,11 +35,11 @@ export default class Detail extends React.Component {
|
|||||||
await this.loadPackageInfo(this.packageName);
|
await this.loadPackageInfo(this.packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillReceiveProps(newProps) {
|
componentDidUpdate(newProps) {
|
||||||
let packageName = this.getPackageName(newProps);
|
if (newProps.isUserLoggedIn !== this.props.isUserLoggedIn) {
|
||||||
if (packageName === this.packageName) return;
|
const packageName = this.getPackageName(newProps);
|
||||||
|
this.loadPackageInfo(packageName);
|
||||||
await this.loadPackageInfo(packageName);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPackageInfo(packageName) {
|
async loadPackageInfo(packageName) {
|
||||||
@ -49,7 +50,8 @@ export default class Detail extends React.Component {
|
|||||||
try {
|
try {
|
||||||
const resp = await API.request(`package/readme/${packageName}`, 'GET');
|
const resp = await API.request(`package/readme/${packageName}`, 'GET');
|
||||||
this.setState({
|
this.setState({
|
||||||
readMe: resp
|
readMe: resp,
|
||||||
|
notFound: false
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -59,7 +61,7 @@ export default class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let {notFound, readMe} = this.state;
|
const {notFound, readMe} = this.state;
|
||||||
|
|
||||||
if (notFound) {
|
if (notFound) {
|
||||||
return <NotFound pkg={this.packageName}/>;
|
return <NotFound pkg={this.packageName}/>;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, {Component, Fragment} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {Loading, MessageBox} from 'element-react';
|
import {Loading, MessageBox} from 'element-react';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
@ -9,17 +9,17 @@ import API from '../../utils/api';
|
|||||||
import PackageList from '../../components/PackageList';
|
import PackageList from '../../components/PackageList';
|
||||||
import Search from '../../components/Search';
|
import Search from '../../components/Search';
|
||||||
|
|
||||||
|
export default class Home extends Component {
|
||||||
export default class Home extends React.Component {
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.element
|
children: PropTypes.element,
|
||||||
}
|
isUserLoggedIn: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
fistTime: true,
|
fistTime: true,
|
||||||
query: ''
|
query: ''
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -31,8 +31,7 @@ export default class Home extends React.Component {
|
|||||||
this.loadPackages();
|
this.loadPackages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
componentDidUpdate(prevProps, prevState) { // eslint-disable-line no-unused-vars
|
|
||||||
if (prevState.query !== this.state.query) {
|
if (prevState.query !== this.state.query) {
|
||||||
if (this.req && this.req.abort) this.req.abort();
|
if (this.req && this.req.abort) this.req.abort();
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -45,6 +44,10 @@ export default class Home extends React.Component {
|
|||||||
this.searchPackage(this.state.query);
|
this.searchPackage(this.state.query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.isUserLoggedIn !== this.props.isUserLoggedIn) {
|
||||||
|
this.loadPackages();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPackages() {
|
async loadPackages() {
|
||||||
@ -57,11 +60,11 @@ export default class Home extends React.Component {
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
MessageBox.msgbox({
|
MessageBox.msgbox({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
message: 'Unable to load package list: ' + err.message
|
message: `Unable to load package list: ${error.message}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,11 +101,16 @@ export default class Home extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {packages, loading} = this.state;
|
||||||
return (
|
return (
|
||||||
<div>
|
<Fragment>
|
||||||
{this.renderSearchBar()}
|
{this.renderSearchBar()}
|
||||||
{this.state.loading ? this.renderLoading() : this.renderPackageList()}
|
{loading ? (
|
||||||
</div>
|
<Loading text="Loading..." />
|
||||||
|
) : (
|
||||||
|
<PackageList help={isEmpty(packages) === true} packages={packages} />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +120,4 @@ export default class Home extends React.Component {
|
|||||||
}
|
}
|
||||||
return <Search handleSearchInput={this.handleSearchInput} />;
|
return <Search handleSearchInput={this.handleSearchInput} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoading() {
|
|
||||||
return <Loading text="Loading..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPackageList() {
|
|
||||||
return <PackageList help={this.state.fistTime} packages={this.state.packages} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,46 @@
|
|||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {HashRouter as Router, Route, Switch} from 'react-router-dom';
|
import {HashRouter as Router, Route, Switch} from 'react-router-dom';
|
||||||
import {asyncComponent} from './utils/asyncComponent';
|
|
||||||
|
|
||||||
import Header from './components/Header';
|
import {asyncComponent} from './utils/asyncComponent';
|
||||||
import Footer from './components/Footer';
|
|
||||||
|
|
||||||
const DetailPackage = asyncComponent(() => import('./modules/detail'));
|
const DetailPackage = asyncComponent(() => import('./modules/detail'));
|
||||||
const HomePage = asyncComponent(() => import('./modules/home'));
|
const HomePage = asyncComponent(() => import('./modules/home'));
|
||||||
|
|
||||||
const RouterApp = () => {
|
class RouterApp extends Component {
|
||||||
return (
|
static propTypes = {
|
||||||
<Router>
|
isUserLoggedIn: PropTypes.bool
|
||||||
<div className="page-full-height">
|
};
|
||||||
<Header/>
|
render() {
|
||||||
|
const {isUserLoggedIn} = this.props;
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/(search/:keyword)?" component={ HomePage } />
|
<Route
|
||||||
<Route exact path="/detail/@:scope/:package" component={DetailPackage} />
|
exact
|
||||||
<Route exact path="/detail/:package" component={DetailPackage} />
|
path="/(search/:keyword)?"
|
||||||
|
render={() => <HomePage isUserLoggedIn={isUserLoggedIn} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/detail/@:scope/:package"
|
||||||
|
render={(props) => (
|
||||||
|
<DetailPackage {...props} isUserLoggedIn={isUserLoggedIn} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/detail/:package"
|
||||||
|
render={(props) => (
|
||||||
|
<DetailPackage {...props} isUserLoggedIn={isUserLoggedIn} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Footer/>
|
</Router>
|
||||||
</div>
|
);
|
||||||
</Router>
|
}
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default RouterApp;
|
export default RouterApp;
|
||||||
|
@ -5,7 +5,7 @@ export function asyncComponent(getComponent) {
|
|||||||
static Component = null;
|
static Component = null;
|
||||||
state = {Component: AsyncComponent.Component};
|
state = {Component: AsyncComponent.Component};
|
||||||
|
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
if (!this.state.Component) {
|
if (!this.state.Component) {
|
||||||
getComponent().then(({default: Component}) => {
|
getComponent().then(({default: Component}) => {
|
||||||
AsyncComponent.Component = Component;
|
AsyncComponent.Component = Component;
|
||||||
|
73
src/webui/utils/login.js
Normal file
73
src/webui/utils/login.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import isString from 'lodash/isString';
|
||||||
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import {Base64} from 'js-base64';
|
||||||
|
import API from './api';
|
||||||
|
import {HEADERS} from '../../lib/constants';
|
||||||
|
|
||||||
|
export function isTokenExpire(token) {
|
||||||
|
if (!isString(token)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [
|
||||||
|
,
|
||||||
|
payload
|
||||||
|
] = token.split('.');
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(Base64.decode(payload));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error('Invalid token:', error, token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.exp || !isNumber(payload.exp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Report as expire before (real expire time - 30s)
|
||||||
|
const jsTimestamp = (payload.exp * 1000) - 30000;
|
||||||
|
const expired = Date.now() >= jsTimestamp;
|
||||||
|
|
||||||
|
return expired;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function makeLogin(username, password) {
|
||||||
|
// checks isEmpty
|
||||||
|
if (isEmpty(username) || isEmpty(password)) {
|
||||||
|
const error = {
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error',
|
||||||
|
description: 'Username or password can\'t be empty!'
|
||||||
|
};
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await API.request('login', 'POST', {
|
||||||
|
body: JSON.stringify({username, password}),
|
||||||
|
headers: {
|
||||||
|
Accept: HEADERS.JSON,
|
||||||
|
'Content-Type': HEADERS.JSON
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = {
|
||||||
|
username: response.username,
|
||||||
|
token: response.token
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
const error = {
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error',
|
||||||
|
description: e.error
|
||||||
|
};
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
}
|
10
src/webui/utils/logo.js
Normal file
10
src/webui/utils/logo.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import API from './api';
|
||||||
|
|
||||||
|
export default async function logo() {
|
||||||
|
try {
|
||||||
|
const logo = await API.request('logo');
|
||||||
|
return logo;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
@ -86,7 +86,7 @@ describe('/ (Verdaccio Page)', () => {
|
|||||||
it('should click on sign in button', async () => {
|
it('should click on sign in button', async () => {
|
||||||
const signInButton = await page.$('header button');
|
const signInButton = await page.$('header button');
|
||||||
await signInButton.click();
|
await signInButton.click();
|
||||||
const signInDialog = await page.$('header .el-dialog__wrapper');
|
const signInDialog = await page.$('.login-dialog .el-dialog__wrapper');
|
||||||
|
|
||||||
expect(signInDialog).not.toBeNull();
|
expect(signInDialog).not.toBeNull();
|
||||||
})
|
})
|
||||||
|
@ -8,28 +8,4 @@ import Adapter from 'enzyme-adapter-react-16';
|
|||||||
|
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
global.__APP_VERSION__ = '1.0.0';
|
global.__APP_VERSION__ = '1.0.0';
|
||||||
|
|
||||||
class LocalStorageMock {
|
|
||||||
constructor() {
|
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getItem(key) {
|
|
||||||
return this.store[key] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setItem(key, value) {
|
|
||||||
this.store[key] = value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeItem(key) {
|
|
||||||
delete this.store[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
global.localStorage = new LocalStorageMock();
|
|
108
test/unit/webui/app.spec.js
Normal file
108
test/unit/webui/app.spec.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import storage from '../../../src/webui/utils/storage';
|
||||||
|
import App from '../../../src/webui/app';
|
||||||
|
import { API_ERROR } from '../../../src/lib/constants';
|
||||||
|
|
||||||
|
import {generateTokenWithTimeRange} from './components/__mocks__/token';
|
||||||
|
|
||||||
|
jest.mock('../../../src/webui/utils/storage', () => {
|
||||||
|
class LocalStorageMock {
|
||||||
|
constructor() {
|
||||||
|
this.store = {};
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.store = {};
|
||||||
|
}
|
||||||
|
getItem(key) {
|
||||||
|
return this.store[key] || null;
|
||||||
|
}
|
||||||
|
setItem(key, value) {
|
||||||
|
this.store[key] = value.toString();
|
||||||
|
}
|
||||||
|
removeItem(key) {
|
||||||
|
delete this.store[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new LocalStorageMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('element-theme-default', () => ({}));
|
||||||
|
|
||||||
|
jest.mock('element-react/src/locale/lang/en', () => ({}));
|
||||||
|
|
||||||
|
jest.mock('../../../src/webui/utils/api', () => ({
|
||||||
|
request: require('./components/__mocks__/api').default.request
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mount(<App />);
|
||||||
|
});
|
||||||
|
it('loadLogo: set logo url in state', async () => {
|
||||||
|
const { loadLogo } = wrapper.instance();
|
||||||
|
await loadLogo();
|
||||||
|
expect(wrapper.state().logoUrl).toEqual(
|
||||||
|
'http://localhost/-/static/logo.png'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggleLoginModal: should toggle the value in state', () => {
|
||||||
|
const { toggleLoginModal } = wrapper.instance();
|
||||||
|
expect(wrapper.state().showLoginModal).toBeFalsy();
|
||||||
|
toggleLoginModal();
|
||||||
|
expect(wrapper.state('showLoginModal')).toBeTruthy();
|
||||||
|
expect(wrapper.state('error')).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isUserAlreadyLoggedIn: token already available in storage', async () => {
|
||||||
|
|
||||||
|
storage.setItem('username', 'verdaccio');
|
||||||
|
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||||
|
const { isUserAlreadyLoggedIn } = wrapper.instance();
|
||||||
|
|
||||||
|
isUserAlreadyLoggedIn();
|
||||||
|
|
||||||
|
expect(wrapper.state('user').username).toEqual('verdaccio');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleLogout - logouts the user and clear localstorage', () => {
|
||||||
|
const { handleLogout } = wrapper.instance();
|
||||||
|
storage.setItem('username', 'verdaccio');
|
||||||
|
storage.setItem('token', 'xxxx.TOKEN.xxxx');
|
||||||
|
|
||||||
|
handleLogout();
|
||||||
|
expect(handleLogout()).toBeUndefined();
|
||||||
|
expect(wrapper.state('user')).toEqual({});
|
||||||
|
expect(wrapper.state('isLoggedIn')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doLogin - login the user successfully', async () => {
|
||||||
|
const { doLogin } = wrapper.instance();
|
||||||
|
await doLogin('sam', '1234');
|
||||||
|
const result = {
|
||||||
|
username: 'sam',
|
||||||
|
token: 'TEST_TOKEN'
|
||||||
|
};
|
||||||
|
expect(wrapper.state('user')).toEqual(result);
|
||||||
|
expect(wrapper.state('isUserLoggedIn')).toBeTruthy();
|
||||||
|
expect(wrapper.state('showLoginModal')).toBeFalsy();
|
||||||
|
expect(storage.getItem('username')).toEqual('sam');
|
||||||
|
expect(storage.getItem('token')).toEqual('TEST_TOKEN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doLogin - authentication failure', async () => {
|
||||||
|
const { doLogin } = wrapper.instance();
|
||||||
|
await doLogin('sam', '12345');
|
||||||
|
console.log(API_ERROR.BAD_USERNAME_PASSWORD);
|
||||||
|
const result = {
|
||||||
|
description: 'bad username/password, access denied',
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error'
|
||||||
|
};
|
||||||
|
expect(wrapper.state('user')).toEqual({});
|
||||||
|
expect(wrapper.state('error')).toEqual(result);
|
||||||
|
});
|
||||||
|
});
|
@ -4,6 +4,7 @@
|
|||||||
import logo from '../store/logo';
|
import logo from '../store/logo';
|
||||||
import login from '../store/login';
|
import login from '../store/login';
|
||||||
import { packageMeta } from '../store/packageMeta';
|
import { packageMeta } from '../store/packageMeta';
|
||||||
|
import { packageInformation } from '../store/package';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register mock api endpoints
|
* Register mock api endpoints
|
||||||
@ -26,7 +27,13 @@ const register = (url, method = 'get', options = {}) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error('Not found');
|
if (url === 'packages' && method.toLocaleLowerCase() === 'get') {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
resolve(packageInformation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error(`URL not found: ${url}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
26
test/unit/webui/components/__mocks__/token.js
Normal file
26
test/unit/webui/components/__mocks__/token.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Token Utility
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import addHours from 'date-fns/add_hours';
|
||||||
|
|
||||||
|
export function generateTokenWithTimeRange (limit = 0) {
|
||||||
|
const payload = {
|
||||||
|
username: 'verdaccio',
|
||||||
|
exp: Number.parseInt(addHours(new Date(), limit).getTime() / 1000, 10)
|
||||||
|
};
|
||||||
|
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateTokenWithExpirationAsString () {
|
||||||
|
const payload = { username: 'verdaccio', exp: 'I am not a number' };
|
||||||
|
return `xxxxxx.${Base64.encode(payload)}.xxxxxx`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateTokenWithOutExpiration (){
|
||||||
|
const payload = {
|
||||||
|
username: 'verdaccio'
|
||||||
|
};
|
||||||
|
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
||||||
|
}
|
@ -1,48 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<Header /> component shallow renderUserActionButton - should show login button 1`] = `
|
exports[`<Header /> component shallow should load header component in login state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope:registry http://localhost<br/>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><div class=\\"user-logged\\"><span class=\\"user-logged-greetings usernameField\\">Hi, Verdaccio</span><button class=\\"el-button el-button--danger headerButton header-button-logout\\" type=\\"button\\"><span>Logout</span></button></div></div></div></header>"`;
|
||||||
<Button
|
|
||||||
className="headerButton header-button-login"
|
|
||||||
disabled={false}
|
|
||||||
loading={false}
|
|
||||||
nativeType="button"
|
|
||||||
onClick={[Function]}
|
|
||||||
plain={false}
|
|
||||||
type="default"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Header /> component shallow renderUserActionButton - should show users as loggedin 1`] = `
|
exports[`<Header /> component shallow should load header component in logout state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope:registry http://localhost<br/>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><button class=\\"el-button el-button--default headerButton header-button-login\\" type=\\"button\\"><span>Login</span></button></div></div></header>"`;
|
||||||
<div
|
|
||||||
className="user-logged"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="user-logged-greetings"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "10px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Hi,
|
|
||||||
Sam
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
className="headerButton header-button-logout"
|
|
||||||
disabled={false}
|
|
||||||
loading={false}
|
|
||||||
nativeType="button"
|
|
||||||
onClick={[Function]}
|
|
||||||
plain={false}
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<Header /> snapshot for loggedin user should match snapshot 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/\\"><img src=\\"\\" class=\\"logo\\"></a><figure>npm set registry http://localhost<br>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><div class=\\"user-logged\\"><span class=\\"user-logged-greetings\\" style=\\"margin-right: 10px;\\">Hi, Verdaccio</span><button class=\\"el-button el-button--danger headerButton header-button-logout\\" type=\\"button\\"><span>Logout</span></button></div></div></div><div><div style=\\"z-index: 1013; display: none;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><br><div class=\\"el-input\\"><input type=\\"text\\" name=\\"username\\" placeholder=\\"Username\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div><br><br><div class=\\"el-input\\"><input type=\\"password\\" name=\\"password\\" placeholder=\\"Type your password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012; display: none;\\"></div></div></header>"`;
|
|
||||||
|
|
||||||
exports[`<Header /> snapshot test shoud match snapshot 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/\\"><img src=\\"\\" class=\\"logo\\"></a><figure>npm set registry http://localhost<br>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><button class=\\"el-button el-button--default headerButton header-button-login\\" type=\\"button\\"><span>Login</span></button></div></div><div><div style=\\"z-index: 1013; display: none;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><br><div class=\\"el-input\\"><input type=\\"text\\" name=\\"username\\" placeholder=\\"Username\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div><br><br><div class=\\"el-input\\"><input type=\\"password\\" name=\\"password\\" placeholder=\\"Type your password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012; display: none;\\"></div></div></header>"`;
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<LoginModal /> should load the component in default state 1`] = `"<div class=\\"login-dialog\\"><div><div style=\\"z-index: 1013;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><br><div class=\\"el-input\\"><input name=\\"username\\" placeholder=\\"Username\\" type=\\"text\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div><br><br><div class=\\"el-input\\"><input name=\\"password\\" placeholder=\\"Type your password\\" type=\\"password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012;\\"></div></div></div>"`;
|
||||||
|
|
||||||
|
exports[`<LoginModal /> should load the component with props 1`] = `"<div class=\\"login-dialog\\"><div><div style=\\"z-index: 1013;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><div class=\\"el-alert el-alert--error\\"><i class=\\"el-alert__icon el-icon-circle-cross is-big\\"></i><div class=\\"el-alert__content\\"><span class=\\"el-alert__title is-bold\\">Error Title</span><p class=\\"el-alert__description\\">Error Description</p><i class=\\"el-alert__closebtn el-icon-close\\" style=\\"display: none;\\"></i></div></div><br><div class=\\"el-input\\"><input name=\\"username\\" placeholder=\\"Username\\" type=\\"text\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div><br><br><div class=\\"el-input\\"><input name=\\"password\\" placeholder=\\"Type your password\\" type=\\"password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012;\\"></div></div></div>"`;
|
@ -2,305 +2,43 @@
|
|||||||
* Header component
|
* Header component
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, mount } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Base64 } from 'js-base64';
|
|
||||||
import addHours from 'date-fns/add_hours'
|
|
||||||
import Header from '../../../../src/webui/components/Header';
|
import Header from '../../../../src/webui/components/Header';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import storage from '../../../../src/webui/utils/storage';
|
|
||||||
|
|
||||||
jest.mock('../../../../src/webui/utils/storage', () => {
|
|
||||||
class LocalStorageMock {
|
|
||||||
constructor() {
|
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getItem(key) {
|
|
||||||
return this.store[key] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setItem(key, value) {
|
|
||||||
this.store[key] = value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeItem(key) {
|
|
||||||
delete this.store[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new LocalStorageMock();
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('../../../../src/webui/utils/api', () => ({
|
|
||||||
request: require('./__mocks__/api').default.request,
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.error = jest.fn();
|
console.error = jest.fn();
|
||||||
|
|
||||||
const generateTokenWithTimeRange = (limit = 0) => {
|
|
||||||
const payload = {
|
|
||||||
username: 'verdaccio',
|
|
||||||
exp: Number.parseInt((addHours(new Date(), limit).getTime() / 1000), 10)
|
|
||||||
}
|
|
||||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('<Header /> component shallow', () => {
|
describe('<Header /> component shallow', () => {
|
||||||
let wrapper;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
it('should give error for required props', () => {
|
||||||
wrapper = shallow(
|
shallow(<Header />);
|
||||||
<BrowserRouter>
|
expect(console.error).toHaveBeenCalled();
|
||||||
<Header />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check the initial state', () => {
|
it('should load header component in login state', () => {
|
||||||
const state = {
|
const props = {
|
||||||
showLogin: false,
|
username: 'verdaccio',
|
||||||
username: '',
|
logo: 'logo.png',
|
||||||
password: '',
|
scope: 'scope:',
|
||||||
logo: '',
|
handleLogout: jest.fn(),
|
||||||
scope: '',
|
toggleLoginModal: () => {}
|
||||||
loginError: null
|
|
||||||
};
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
expect(HeaderWrapper.state()).toEqual(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loadLogo - should load verdaccio logo', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { loadLogo } = HeaderWrapper.instance();
|
|
||||||
|
|
||||||
loadLogo().then(() => {
|
|
||||||
expect(HeaderWrapper.state('logo'))
|
|
||||||
.toEqual('http://localhost/-/static/logo.png');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggleLoginModal - should toggle login modal', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { toggleLoginModal } = HeaderWrapper.instance();
|
|
||||||
|
|
||||||
expect(toggleLoginModal()).toBeUndefined();
|
|
||||||
expect(HeaderWrapper.state('showLogin')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('toggleLoginModal - click on login button and cancel button in login dialog', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const spy = jest.spyOn(HeaderWrapper.instance(), 'toggleLoginModal');
|
|
||||||
HeaderWrapper.find('.header-button-login').simulate('click');
|
|
||||||
HeaderWrapper.find('.cancel-login-button').simulate('click');
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
spy.mockRestore();
|
|
||||||
})
|
|
||||||
|
|
||||||
it('handleInput - should set username and password in state', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const handleInput = HeaderWrapper.instance().handleInput;
|
|
||||||
|
|
||||||
expect(handleInput('username', 'xyz')).toBeUndefined();
|
|
||||||
expect(HeaderWrapper.state('username')).toEqual('xyz');
|
|
||||||
|
|
||||||
expect(handleInput('password', '1234')).toBeUndefined();
|
|
||||||
expect(HeaderWrapper.state('password')).toEqual('1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handleSubmit - should give error for blank username and password', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const {handleSubmit} = HeaderWrapper.instance();
|
|
||||||
const error = {
|
|
||||||
description: "Username or password can't be empty!",
|
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error'
|
|
||||||
};
|
|
||||||
expect(handleSubmit({ preventDefault: () => {} })).toBeDefined();
|
|
||||||
expect(HeaderWrapper.state('loginError')).toEqual(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handleSubmit - should login successfully', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const {handleSubmit} = HeaderWrapper.instance();
|
|
||||||
const event = {preventDefault: () => {}}
|
|
||||||
const spy = jest.spyOn(event, 'preventDefault');
|
|
||||||
|
|
||||||
HeaderWrapper.setState({ username: 'sam', password: '1234' });
|
|
||||||
|
|
||||||
handleSubmit(event).then(() => {
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
expect(storage.getItem('token')).toEqual('TEST_TOKEN');
|
|
||||||
expect(storage.getItem('username')).toEqual('sam');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handleSubmit - login should failed with 401', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const {handleSubmit} = HeaderWrapper.instance();
|
|
||||||
const errorObject = {
|
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
|
||||||
description: 'bad username/password, access denied'
|
|
||||||
};
|
|
||||||
const event = { preventDefault: () => { } }
|
|
||||||
const spy = jest.spyOn(event, 'preventDefault');
|
|
||||||
HeaderWrapper.setState({ username: 'sam', password: '12345' });
|
|
||||||
|
|
||||||
handleSubmit(event).then(() => {
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
expect(HeaderWrapper.state('loginError')).toEqual(errorObject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handleSubmit - login should failed with when no data is sent', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const {handleSubmit} = HeaderWrapper.instance();
|
|
||||||
const error = {
|
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
|
||||||
description: "Username or password can't be empty!"
|
|
||||||
};
|
|
||||||
const event = { preventDefault: () => { } }
|
|
||||||
const spy = jest.spyOn(event, 'preventDefault');
|
|
||||||
|
|
||||||
HeaderWrapper.setState({});
|
|
||||||
|
|
||||||
handleSubmit(event).then(() => {
|
|
||||||
expect(spy).toHaveBeenCalled();
|
|
||||||
expect(HeaderWrapper.state('loginError')).toEqual(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renderUserActionButton - should show login button', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { renderUserActionButton } = HeaderWrapper.instance();
|
|
||||||
expect(renderUserActionButton()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renderUserActionButton - should show users as loggedin', () => {
|
|
||||||
class MockedHeader extends Header {
|
|
||||||
get isTokenExpire() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
wrapper = shallow(
|
const wrapper = shallow(<Header {...props} />);
|
||||||
<BrowserRouter>
|
wrapper.find('.header-button-logout').simulate('click');
|
||||||
<MockedHeader />
|
expect(props.handleLogout).toHaveBeenCalled();
|
||||||
</BrowserRouter>);
|
|
||||||
const HeaderWrapper = wrapper.find(MockedHeader).dive();
|
|
||||||
const { renderUserActionButton } = HeaderWrapper.instance();
|
|
||||||
expect(renderUserActionButton()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token is not availabe in storage', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { isTokenExpire } = HeaderWrapper.instance();
|
|
||||||
expect(isTokenExpire).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token is not a valid payload', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { isTokenExpire } = HeaderWrapper.instance();
|
|
||||||
storage.setItem('token', 'not_a_valid_token');
|
|
||||||
expect(isTokenExpire).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token should not expire in 24 hrs range', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
|
||||||
expect(HeaderWrapper.instance().isTokenExpire).toBeFalsy();
|
|
||||||
storage.removeItem('token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token should expire for present', () => {
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
storage.setItem('token', generateTokenWithTimeRange());
|
|
||||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
|
||||||
storage.removeItem('token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token expiration is not available', () => {
|
|
||||||
const generateTokenWithOutExpiration = () => {
|
|
||||||
const payload = {
|
|
||||||
username: 'verdaccio'
|
|
||||||
}
|
|
||||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
|
||||||
}
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
storage.setItem('token', generateTokenWithOutExpiration());
|
|
||||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
|
||||||
storage.removeItem('token');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('isTokenExpire - token expiration is not a number', () => {
|
|
||||||
const generateTokenWithExpirationAsString = () => {
|
|
||||||
const payload = {
|
|
||||||
username: 'verdaccio',
|
|
||||||
exp: 'I am not a number'
|
|
||||||
};
|
|
||||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
|
||||||
};
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
storage.setItem('token', generateTokenWithExpirationAsString());
|
|
||||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
|
||||||
storage.removeItem('token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTokenExpire - token is not a valid json token', () => {
|
|
||||||
const generateTokenWithExpirationAsString = () => {
|
|
||||||
const payload = { username: 'verdaccio', exp: 'I am not a number' };
|
|
||||||
return `xxxxxx.${Base64.encode(payload)}.xxxxxx`;
|
|
||||||
};
|
|
||||||
const result = [
|
|
||||||
'Invalid token:',
|
|
||||||
SyntaxError('Unexpected token o in JSON at position 1'),
|
|
||||||
'xxxxxx.W29iamVjdCBPYmplY3Rd.xxxxxx'
|
|
||||||
]
|
|
||||||
storage.setItem('token', generateTokenWithExpirationAsString());
|
|
||||||
wrapper.find(Header).dive().isTokenExpire;
|
|
||||||
expect(console.error).toBeCalledWith(...result);
|
|
||||||
storage.removeItem('token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handleLogout - should clear the local stoage', () => {
|
|
||||||
const storageSpy = jest.spyOn(storage, 'clear');
|
|
||||||
const locationSpy = jest.spyOn(window.location, 'reload');
|
|
||||||
const HeaderWrapper = wrapper.find(Header).dive();
|
|
||||||
const { handleLogout } = HeaderWrapper.instance();
|
|
||||||
handleLogout();
|
|
||||||
expect(storageSpy).toHaveBeenCalled();
|
|
||||||
expect(locationSpy).toHaveBeenCalled()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('<Header /> snapshot test', () => {
|
|
||||||
it('shoud match snapshot', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Header />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('<Header /> snapshot for loggedin user', () => {
|
it('should load header component in logout state', () => {
|
||||||
beforeAll(() => {
|
const props = {
|
||||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
username: undefined,
|
||||||
storage.setItem('username', 'verdaccio');
|
logo: 'logo.png',
|
||||||
})
|
scope: 'scope:',
|
||||||
afterAll(() => {
|
handleLogout: () => {},
|
||||||
storage.removeItem('token');
|
toggleLoginModal: jest.fn()
|
||||||
})
|
}
|
||||||
it('should match snapshot', () => {
|
const wrapper = shallow(<Header {...props} />);
|
||||||
const wrapper = mount(
|
wrapper.find('.header-button-login').simulate('click');
|
||||||
<BrowserRouter>
|
expect(props.toggleLoginModal).toHaveBeenCalled();
|
||||||
<Header />
|
|
||||||
</BrowserRouter>);
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
79
test/unit/webui/components/login.spec.js
Normal file
79
test/unit/webui/components/login.spec.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import LoginModal from '../../../../src/webui/components/Login';
|
||||||
|
|
||||||
|
describe('<LoginModal />', () => {
|
||||||
|
it('should load the component in default state', () => {
|
||||||
|
const wrapper = mount(<LoginModal />);
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load the component with props', () => {
|
||||||
|
const props = {
|
||||||
|
visibility: true,
|
||||||
|
error: {
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Title',
|
||||||
|
description: 'Error Description'
|
||||||
|
},
|
||||||
|
onCancel: () => {},
|
||||||
|
onSubmit: () => {}
|
||||||
|
};
|
||||||
|
const wrapper = mount(<LoginModal {...props} />);
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onCancel: should close the login modal', () => {
|
||||||
|
const props = {
|
||||||
|
visibility: true,
|
||||||
|
error: {
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Title',
|
||||||
|
description: 'Error Description'
|
||||||
|
},
|
||||||
|
onCancel: jest.fn(),
|
||||||
|
onSubmit: () => {}
|
||||||
|
};
|
||||||
|
const wrapper = mount(<LoginModal {...props} />);
|
||||||
|
wrapper.find('.el-dialog__footer > .cancel-login-button').simulate('click');
|
||||||
|
expect(props.onCancel).toHaveBeenCalled();
|
||||||
|
wrapper.find('.el-dialog__headerbtn > .el-dialog__close').simulate('click');
|
||||||
|
expect(props.onCancel).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setCredentials - should set username and password in state', () => {
|
||||||
|
const props = {
|
||||||
|
visibility: true,
|
||||||
|
error: {},
|
||||||
|
onCancel: () => { },
|
||||||
|
onSubmit: () => { }
|
||||||
|
};
|
||||||
|
const wrapper = mount(<LoginModal {...props} />);
|
||||||
|
const { setCredentials } = wrapper.instance();
|
||||||
|
|
||||||
|
expect(setCredentials('username', 'xyz')).toBeUndefined();
|
||||||
|
expect(wrapper.state('username')).toEqual('xyz');
|
||||||
|
|
||||||
|
expect(setCredentials('password', '1234')).toBeUndefined();
|
||||||
|
expect(wrapper.state('password')).toEqual('1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submitCredential: should call the onSubmit', async () => {
|
||||||
|
const props = {
|
||||||
|
visibility: true,
|
||||||
|
error: {},
|
||||||
|
onCancel: () => { },
|
||||||
|
onSubmit: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
preventDefault: jest.fn()
|
||||||
|
}
|
||||||
|
const wrapper = mount(<LoginModal {...props} />);
|
||||||
|
const { submitCredentials } = wrapper.instance();
|
||||||
|
wrapper.setState({username: 'sam', password: 1234})
|
||||||
|
await submitCredentials(event);
|
||||||
|
expect(props.onSubmit).toHaveBeenCalledWith('sam', 1234);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
import {API_ERROR} from '../../../../../src/lib/constants';
|
||||||
/**
|
/**
|
||||||
* API mock for login endpoint
|
* API mock for login endpoint
|
||||||
* @param {object} config configuration of api call
|
* @param {object} config configuration of api call
|
||||||
@ -13,7 +14,7 @@ export default function(config) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject({
|
reject({
|
||||||
error: 'bad username/password, access denied'
|
error: API_ERROR.BAD_USERNAME_PASSWORD
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
168
test/unit/webui/components/store/package.js
Normal file
168
test/unit/webui/components/store/package.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
export const packageInformation = [
|
||||||
|
{
|
||||||
|
name: 'jquery',
|
||||||
|
title: 'jQuery',
|
||||||
|
description: 'JavaScript library for DOM operations',
|
||||||
|
version: '3.3.2-pre',
|
||||||
|
main: 'dist/jquery.js',
|
||||||
|
homepage: 'https://jquery.com',
|
||||||
|
author: {
|
||||||
|
name: 'JS Foundation and other contributors',
|
||||||
|
url: 'https://github.com/jquery/jquery/blob/master/AUTHORS.txt'
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
type: 'git',
|
||||||
|
url: 'https://github.com/jquery/jquery.git'
|
||||||
|
},
|
||||||
|
keywords: ['jquery', 'javascript', 'browser', 'library'],
|
||||||
|
bugs: {
|
||||||
|
url: 'https://github.com/jquery/jquery/issues'
|
||||||
|
},
|
||||||
|
license: 'MIT',
|
||||||
|
dependencies: {},
|
||||||
|
devDependencies: {
|
||||||
|
'babel-core': '7.0.0-beta.0',
|
||||||
|
'babel-plugin-transform-es2015-for-of': '7.0.0-beta.0',
|
||||||
|
commitplease: '3.2.0',
|
||||||
|
'core-js': '2.5.7',
|
||||||
|
'eslint-config-jquery': '1.0.1',
|
||||||
|
grunt: '1.0.3',
|
||||||
|
'grunt-babel': '7.0.0',
|
||||||
|
'grunt-cli': '1.2.0',
|
||||||
|
'grunt-compare-size': '0.4.2',
|
||||||
|
'grunt-contrib-uglify': '3.3.0',
|
||||||
|
'grunt-contrib-watch': '1.1.0',
|
||||||
|
'grunt-eslint': '20.2.0',
|
||||||
|
'grunt-git-authors': '3.2.0',
|
||||||
|
'grunt-jsonlint': '1.1.0',
|
||||||
|
'grunt-karma': '2.0.0',
|
||||||
|
'grunt-newer': '1.3.0',
|
||||||
|
'grunt-npmcopy': '0.1.0',
|
||||||
|
'gzip-js': '0.3.2',
|
||||||
|
husky: '0.14.3',
|
||||||
|
insight: '0.10.1',
|
||||||
|
jsdom: '5.6.1',
|
||||||
|
karma: '2.0.3',
|
||||||
|
'karma-browserstack-launcher': '1.3.0',
|
||||||
|
'karma-chrome-launcher': '2.2.0',
|
||||||
|
'karma-firefox-launcher': '1.1.0',
|
||||||
|
'karma-qunit': '1.2.1',
|
||||||
|
'load-grunt-tasks': '4.0.0',
|
||||||
|
'native-promise-only': '0.8.1',
|
||||||
|
'promises-aplus-tests': '2.1.2',
|
||||||
|
q: '1.5.1',
|
||||||
|
'qunit-assert-step': '1.1.1',
|
||||||
|
qunitjs: '1.23.1',
|
||||||
|
'raw-body': '2.3.3',
|
||||||
|
requirejs: '2.3.5',
|
||||||
|
sinon: '2.3.7',
|
||||||
|
sizzle: '2.3.3',
|
||||||
|
'strip-json-comments': '2.0.1',
|
||||||
|
testswarm: '1.1.0',
|
||||||
|
'uglify-js': '3.4.0'
|
||||||
|
},
|
||||||
|
scripts: {
|
||||||
|
build: 'npm install && grunt',
|
||||||
|
start: 'grunt watch',
|
||||||
|
'test:browserless': 'grunt && grunt test:slow',
|
||||||
|
'test:browser': 'grunt && grunt karma:main',
|
||||||
|
test: 'grunt && grunt test:slow && grunt karma:main',
|
||||||
|
jenkins: 'npm run test:browserless',
|
||||||
|
precommit: 'grunt lint:newer qunit_fixture',
|
||||||
|
commitmsg: 'node node_modules/commitplease'
|
||||||
|
},
|
||||||
|
commitplease: {
|
||||||
|
nohook: true,
|
||||||
|
components: [
|
||||||
|
'Docs',
|
||||||
|
'Tests',
|
||||||
|
'Build',
|
||||||
|
'Support',
|
||||||
|
'Release',
|
||||||
|
'Core',
|
||||||
|
'Ajax',
|
||||||
|
'Attributes',
|
||||||
|
'Callbacks',
|
||||||
|
'CSS',
|
||||||
|
'Data',
|
||||||
|
'Deferred',
|
||||||
|
'Deprecated',
|
||||||
|
'Dimensions',
|
||||||
|
'Effects',
|
||||||
|
'Event',
|
||||||
|
'Manipulation',
|
||||||
|
'Offset',
|
||||||
|
'Queue',
|
||||||
|
'Selector',
|
||||||
|
'Serialize',
|
||||||
|
'Traversing',
|
||||||
|
'Wrap'
|
||||||
|
],
|
||||||
|
markerPattern: '^((clos|fix|resolv)(e[sd]|ing))|^(refs?)',
|
||||||
|
ticketPattern: '^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lodash',
|
||||||
|
version: '4.17.4',
|
||||||
|
license: 'MIT',
|
||||||
|
private: true,
|
||||||
|
main: 'lodash.js',
|
||||||
|
engines: {
|
||||||
|
node: '>=4.0.0'
|
||||||
|
},
|
||||||
|
sideEffects: false,
|
||||||
|
scripts: {
|
||||||
|
build: 'npm run build:main && npm run build:fp',
|
||||||
|
'build:fp': 'node lib/fp/build-dist.js',
|
||||||
|
'build:fp-modules': 'node lib/fp/build-modules.js',
|
||||||
|
'build:main': 'node lib/main/build-dist.js',
|
||||||
|
'build:main-modules': 'node lib/main/build-modules.js',
|
||||||
|
doc: 'node lib/main/build-doc github && npm run test:doc',
|
||||||
|
'doc:fp': 'node lib/fp/build-doc',
|
||||||
|
'doc:site': 'node lib/main/build-doc site',
|
||||||
|
'doc:sitehtml':
|
||||||
|
'optional-dev-dependency marky-markdown@^9.0.1 && npm run doc:site && node lib/main/build-site',
|
||||||
|
pretest: 'npm run build',
|
||||||
|
style: 'eslint *.js .internal/**/*.js',
|
||||||
|
test: 'npm run test:main && npm run test:fp',
|
||||||
|
'test:doc': 'markdown-doctest doc/*.md',
|
||||||
|
'test:fp': 'node test/test-fp',
|
||||||
|
'test:main': 'node test/test',
|
||||||
|
validate: 'npm run style && npm run test'
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
async: '^2.1.4',
|
||||||
|
benchmark: '^2.1.3',
|
||||||
|
chalk: '^1.1.3',
|
||||||
|
cheerio: '^0.22.0',
|
||||||
|
'codecov.io': '~0.1.6',
|
||||||
|
coveralls: '^2.11.15',
|
||||||
|
'curl-amd': '~0.8.12',
|
||||||
|
docdown: '~0.7.2',
|
||||||
|
dojo: '^1.12.1',
|
||||||
|
ecstatic: '^2.1.0',
|
||||||
|
eslint: '^3.15.0',
|
||||||
|
'eslint-plugin-import': '^2.2.0',
|
||||||
|
'fs-extra': '~1.0.0',
|
||||||
|
glob: '^7.1.1',
|
||||||
|
istanbul: '0.4.5',
|
||||||
|
jquery: '^3.1.1',
|
||||||
|
lodash: '4.17.3',
|
||||||
|
'lodash-doc-globals': '^0.1.1',
|
||||||
|
'markdown-doctest': '^0.9.1',
|
||||||
|
'optional-dev-dependency': '^2.0.0',
|
||||||
|
platform: '^1.3.3',
|
||||||
|
'qunit-extras': '^3.0.0',
|
||||||
|
qunitjs: '^2.1.0',
|
||||||
|
request: '^2.79.0',
|
||||||
|
requirejs: '^2.3.2',
|
||||||
|
'sauce-tunnel': '^2.5.0',
|
||||||
|
'uglify-js': '2.7.5',
|
||||||
|
webpack: '^1.14.0'
|
||||||
|
},
|
||||||
|
greenkeeper: {
|
||||||
|
ignore: ['lodash']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
98
test/unit/webui/utils/login.spec.js
Normal file
98
test/unit/webui/utils/login.spec.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { isTokenExpire, makeLogin } from '../../../../src/webui/utils/login';
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateTokenWithTimeRange,
|
||||||
|
generateTokenWithExpirationAsString,
|
||||||
|
generateTokenWithOutExpiration
|
||||||
|
} from '../components/__mocks__/token';
|
||||||
|
|
||||||
|
console.error = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('.../../../../src/webui/utils/api', () => ({
|
||||||
|
request: require('../components/__mocks__/api').default.request
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('isTokenExpire', () => {
|
||||||
|
it('isTokenExpire - token is not present', () => {
|
||||||
|
expect(isTokenExpire()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isTokenExpire - token is not a valid payload', () => {
|
||||||
|
expect(isTokenExpire('not_a_valid_token')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isTokenExpire - token should not expire in 24 hrs range', () => {
|
||||||
|
const token = generateTokenWithTimeRange(24);
|
||||||
|
expect(isTokenExpire(token)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isTokenExpire - token should expire for current time', () => {
|
||||||
|
const token = generateTokenWithTimeRange();
|
||||||
|
expect(isTokenExpire(token)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isTokenExpire - token expiration is not available', () => {
|
||||||
|
const token = generateTokenWithOutExpiration();
|
||||||
|
expect(isTokenExpire(token)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isTokenExpire - token is not a valid json token', () => {
|
||||||
|
const token = generateTokenWithExpirationAsString();
|
||||||
|
const result = [
|
||||||
|
'Invalid token:',
|
||||||
|
SyntaxError('Unexpected token o in JSON at position 1'),
|
||||||
|
'xxxxxx.W29iamVjdCBPYmplY3Rd.xxxxxx'
|
||||||
|
];
|
||||||
|
expect(isTokenExpire(token)).toBeTruthy();
|
||||||
|
expect(console.error).toBeCalledWith(...result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeLogin', () => {
|
||||||
|
it('makeLogin - should give error for blank username and password', async () => {
|
||||||
|
const result = {
|
||||||
|
error: {
|
||||||
|
description: "Username or password can't be empty!",
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const login = await makeLogin();
|
||||||
|
expect(login).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makeLogin - should login successfully', async () => {
|
||||||
|
const { username, password } = { username: 'sam', password: '1234' };
|
||||||
|
const result = { token: 'TEST_TOKEN', username: 'sam' };
|
||||||
|
const login = await makeLogin(username, password);
|
||||||
|
expect(login).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makeLogin - login should failed with 401', async () => {
|
||||||
|
const result = {
|
||||||
|
error: {
|
||||||
|
description: 'bad username/password, access denied',
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { username, password } = { username: 'sam', password: '123456' };
|
||||||
|
const login = await makeLogin(username, password);
|
||||||
|
expect(login).toEqual(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makeLogin - login should failed with when no data is sent', async () => {
|
||||||
|
const result = {
|
||||||
|
error: {
|
||||||
|
title: 'Unable to login',
|
||||||
|
type: 'error',
|
||||||
|
description: "Username or password can't be empty!"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { username, password } = { username: '', password: '' };
|
||||||
|
const login = await makeLogin(username, password);
|
||||||
|
expect(login).toEqual(result);
|
||||||
|
});
|
||||||
|
});
|
12
test/unit/webui/utils/logo.spec.js
Normal file
12
test/unit/webui/utils/logo.spec.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import logo from '../../../../src/webui/utils/logo';
|
||||||
|
|
||||||
|
jest.mock('../../../../src/webui/utils/api', () => ({
|
||||||
|
request: require('../components/__mocks__/api').default.request
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('logo', () => {
|
||||||
|
it('loadLogo - should load verdaccio logo', async () => {
|
||||||
|
const url = await logo();
|
||||||
|
expect(url).toEqual('http://localhost/-/static/logo.png');
|
||||||
|
});
|
||||||
|
});
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in New Issue
Block a user