mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-11-08 23:25:51 +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"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-object-rest-spread"
|
||||
"transform-object-rest-spread",
|
||||
"babel-plugin-dynamic-import-node"
|
||||
]
|
||||
},
|
||||
"testOldEnv": {
|
||||
|
@ -40,7 +40,7 @@ module.exports = {
|
||||
'<rootDir>/test',
|
||||
],
|
||||
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',
|
||||
'\\.(png)$': '<rootDir>/node_modules/identity-obj-proxy',
|
||||
'\\.(svg)$': '<rootDir>/test/unit/empty.js'
|
||||
|
@ -59,6 +59,7 @@
|
||||
"babel-eslint": "8.2.6",
|
||||
"babel-jest": "23.4.0",
|
||||
"babel-loader": "7.1.5",
|
||||
"babel-plugin-dynamic-import-node": "2.0.0",
|
||||
"babel-plugin-flow-runtime": "0.17.0",
|
||||
"babel-plugin-syntax-dynamic-import": "6.18.0",
|
||||
"babel-plugin-transform-async-to-generator": "6.24.1",
|
||||
@ -77,8 +78,8 @@
|
||||
"babel-register": "6.26.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"bundlesize": "0.17.0",
|
||||
"cross-env": "5.1.4",
|
||||
"codecov": "3.0.4",
|
||||
"cross-env": "5.1.4",
|
||||
"css-loader": "0.28.10",
|
||||
"element-react": "1.4.8",
|
||||
"element-theme-default": "1.4.13",
|
||||
@ -114,8 +115,8 @@
|
||||
"ora": "1.4.0",
|
||||
"prop-types": "15.6.1",
|
||||
"puppeteer": "1.1.1",
|
||||
"react": "16.2.0",
|
||||
"react-dom": "16.2.0",
|
||||
"react": "16.4.2",
|
||||
"react-dom": "16.4.2",
|
||||
"react-hot-loader": "4.2.0",
|
||||
"react-router-dom": "4.2.2",
|
||||
"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) {
|
||||
return jwt.sign(payload, secret, {
|
||||
notBefore: '1000', // Make sure the time will not rollback :)
|
||||
notBefore: '1', // Make sure the time will not rollback :)
|
||||
...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 {i18n} from 'element-react';
|
||||
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);
|
||||
|
||||
import Route from './router';
|
||||
@ -10,10 +19,147 @@ import Route from './router';
|
||||
import './styles/main.scss';
|
||||
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() {
|
||||
const {isUserLoggedIn} = this.state;
|
||||
return (
|
||||
<Route />
|
||||
<div className="page-full-height">
|
||||
{this.renderHeader()}
|
||||
{this.renderLoginModal()}
|
||||
<Route isUserLoggedIn={isUserLoggedIn} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -41,5 +41,9 @@ .header {
|
||||
background-color: $saltpan;
|
||||
}
|
||||
}
|
||||
|
||||
.usernameField {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,245 +1,68 @@
|
||||
import React from 'react';
|
||||
import {Form, Button, Dialog, Input, Alert} from 'element-react';
|
||||
import isString from 'lodash/isString';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'element-react';
|
||||
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 {HEADERS} from '../../../lib/constants';
|
||||
|
||||
import classes from './header.scss';
|
||||
import './logo.png';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
state = {
|
||||
showLogin: false,
|
||||
username: '',
|
||||
password: '',
|
||||
logo: '',
|
||||
loginError: null,
|
||||
scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : ''
|
||||
};
|
||||
const Header = ({
|
||||
logo = '',
|
||||
scope = '',
|
||||
username = '',
|
||||
handleLogout = () => {},
|
||||
toggleLoginModal = () => {}
|
||||
}) => {
|
||||
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) {
|
||||
super(props);
|
||||
this.toggleLoginModal = this.toggleLoginModal.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
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"
|
||||
<div className={classes.headerRight}>
|
||||
{username ? (
|
||||
<div className="user-logged">
|
||||
<span
|
||||
className={`user-logged-greetings ${classes.usernameField}`}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
Hi, {capitalize(username)}
|
||||
</span>
|
||||
<Button
|
||||
nativeType="submit"
|
||||
className="login-button"
|
||||
onClick={this.handleSubmit}
|
||||
className={`${classes.headerButton} header-button-logout`}
|
||||
type="danger"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Login
|
||||
Logout
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Form>
|
||||
</Dialog>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
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 {Loading} from 'element-react';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -12,9 +12,10 @@ import PackageSidebar from '../../components/PackageSidebar/index';
|
||||
|
||||
const loadingMessage = 'Loading...';
|
||||
|
||||
export default class Detail extends React.Component {
|
||||
export default class Detail extends Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object
|
||||
match: PropTypes.object,
|
||||
isUserLoggedIn: PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -23,7 +24,7 @@ export default class Detail extends React.Component {
|
||||
};
|
||||
|
||||
getPackageName(props = this.props) {
|
||||
let params = props.match.params;
|
||||
const params = props.match.params;
|
||||
return `${(params.scope && '@' + params.scope + '/') || ''}${params.package}`;
|
||||
}
|
||||
get packageName() {
|
||||
@ -34,11 +35,11 @@ export default class Detail extends React.Component {
|
||||
await this.loadPackageInfo(this.packageName);
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(newProps) {
|
||||
let packageName = this.getPackageName(newProps);
|
||||
if (packageName === this.packageName) return;
|
||||
|
||||
await this.loadPackageInfo(packageName);
|
||||
componentDidUpdate(newProps) {
|
||||
if (newProps.isUserLoggedIn !== this.props.isUserLoggedIn) {
|
||||
const packageName = this.getPackageName(newProps);
|
||||
this.loadPackageInfo(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPackageInfo(packageName) {
|
||||
@ -49,7 +50,8 @@ export default class Detail extends React.Component {
|
||||
try {
|
||||
const resp = await API.request(`package/readme/${packageName}`, 'GET');
|
||||
this.setState({
|
||||
readMe: resp
|
||||
readMe: resp,
|
||||
notFound: false
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
@ -59,7 +61,7 @@ export default class Detail extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let {notFound, readMe} = this.state;
|
||||
const {notFound, readMe} = this.state;
|
||||
|
||||
if (notFound) {
|
||||
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 {Loading, MessageBox} from 'element-react';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -9,17 +9,17 @@ import API from '../../utils/api';
|
||||
import PackageList from '../../components/PackageList';
|
||||
import Search from '../../components/Search';
|
||||
|
||||
|
||||
export default class Home extends React.Component {
|
||||
export default class Home extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element
|
||||
}
|
||||
children: PropTypes.element,
|
||||
isUserLoggedIn: PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
fistTime: true,
|
||||
query: ''
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -31,8 +31,7 @@ export default class Home extends React.Component {
|
||||
this.loadPackages();
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps, prevState) { // eslint-disable-line no-unused-vars
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.query !== this.state.query) {
|
||||
if (this.req && this.req.abort) this.req.abort();
|
||||
this.setState({
|
||||
@ -45,6 +44,10 @@ export default class Home extends React.Component {
|
||||
this.searchPackage(this.state.query);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.isUserLoggedIn !== this.props.isUserLoggedIn) {
|
||||
this.loadPackages();
|
||||
}
|
||||
}
|
||||
|
||||
async loadPackages() {
|
||||
@ -57,11 +60,11 @@ export default class Home extends React.Component {
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
MessageBox.msgbox({
|
||||
type: 'error',
|
||||
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() {
|
||||
const {packages, loading} = this.state;
|
||||
return (
|
||||
<div>
|
||||
<Fragment>
|
||||
{this.renderSearchBar()}
|
||||
{this.state.loading ? this.renderLoading() : this.renderPackageList()}
|
||||
</div>
|
||||
{loading ? (
|
||||
<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} />;
|
||||
}
|
||||
|
||||
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 {asyncComponent} from './utils/asyncComponent';
|
||||
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
import {asyncComponent} from './utils/asyncComponent';
|
||||
|
||||
const DetailPackage = asyncComponent(() => import('./modules/detail'));
|
||||
const HomePage = asyncComponent(() => import('./modules/home'));
|
||||
|
||||
const RouterApp = () => {
|
||||
return (
|
||||
<Router>
|
||||
<div className="page-full-height">
|
||||
<Header/>
|
||||
class RouterApp extends Component {
|
||||
static propTypes = {
|
||||
isUserLoggedIn: PropTypes.bool
|
||||
};
|
||||
render() {
|
||||
const {isUserLoggedIn} = this.props;
|
||||
return (
|
||||
<Router>
|
||||
<div className="container">
|
||||
<Switch>
|
||||
<Route exact path="/(search/:keyword)?" component={ HomePage } />
|
||||
<Route exact path="/detail/@:scope/:package" component={DetailPackage} />
|
||||
<Route exact path="/detail/:package" component={DetailPackage} />
|
||||
<Route
|
||||
exact
|
||||
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>
|
||||
</div>
|
||||
<Footer/>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RouterApp;
|
||||
|
@ -5,7 +5,7 @@ export function asyncComponent(getComponent) {
|
||||
static Component = null;
|
||||
state = {Component: AsyncComponent.Component};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (!this.state.Component) {
|
||||
getComponent().then(({default: 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 () => {
|
||||
const signInButton = await page.$('header button');
|
||||
await signInButton.click();
|
||||
const signInDialog = await page.$('header .el-dialog__wrapper');
|
||||
const signInDialog = await page.$('.login-dialog .el-dialog__wrapper');
|
||||
|
||||
expect(signInDialog).not.toBeNull();
|
||||
})
|
||||
|
@ -8,28 +8,4 @@ import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
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();
|
||||
global.__APP_VERSION__ = '1.0.0';
|
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 login from '../store/login';
|
||||
import { packageMeta } from '../store/packageMeta';
|
||||
import { packageInformation } from '../store/package';
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
exports[`<Header /> component shallow renderUserActionButton - should show login button 1`] = `
|
||||
<Button
|
||||
className="headerButton header-button-login"
|
||||
disabled={false}
|
||||
loading={false}
|
||||
nativeType="button"
|
||||
onClick={[Function]}
|
||||
plain={false}
|
||||
type="default"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
`;
|
||||
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>"`;
|
||||
|
||||
exports[`<Header /> component shallow renderUserActionButton - should show users as loggedin 1`] = `
|
||||
<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>"`;
|
||||
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>"`;
|
||||
|
@ -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
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { Base64 } from 'js-base64';
|
||||
import addHours from 'date-fns/add_hours'
|
||||
import { shallow } from 'enzyme';
|
||||
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();
|
||||
|
||||
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', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
</BrowserRouter>
|
||||
);
|
||||
it('should give error for required props', () => {
|
||||
shallow(<Header />);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should check the initial state', () => {
|
||||
const state = {
|
||||
showLogin: false,
|
||||
username: '',
|
||||
password: '',
|
||||
logo: '',
|
||||
scope: '',
|
||||
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;
|
||||
}
|
||||
it('should load header component in login state', () => {
|
||||
const props = {
|
||||
username: 'verdaccio',
|
||||
logo: 'logo.png',
|
||||
scope: 'scope:',
|
||||
handleLogout: jest.fn(),
|
||||
toggleLoginModal: () => {}
|
||||
}
|
||||
wrapper = shallow(
|
||||
<BrowserRouter>
|
||||
<MockedHeader />
|
||||
</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>
|
||||
);
|
||||
const wrapper = shallow(<Header {...props} />);
|
||||
wrapper.find('.header-button-logout').simulate('click');
|
||||
expect(props.handleLogout).toHaveBeenCalled();
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<Header /> snapshot for loggedin user', () => {
|
||||
beforeAll(() => {
|
||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||
storage.setItem('username', 'verdaccio');
|
||||
})
|
||||
afterAll(() => {
|
||||
storage.removeItem('token');
|
||||
})
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = mount(
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
</BrowserRouter>);
|
||||
it('should load header component in logout state', () => {
|
||||
const props = {
|
||||
username: undefined,
|
||||
logo: 'logo.png',
|
||||
scope: 'scope:',
|
||||
handleLogout: () => {},
|
||||
toggleLoginModal: jest.fn()
|
||||
}
|
||||
const wrapper = shallow(<Header {...props} />);
|
||||
wrapper.find('.header-button-login').simulate('click');
|
||||
expect(props.toggleLoginModal).toHaveBeenCalled();
|
||||
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
|
||||
* @param {object} config configuration of api call
|
||||
@ -13,7 +14,7 @@ export default function(config) {
|
||||
});
|
||||
} else {
|
||||
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