fix: login without reload (#678) (#679) (#914)

* 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:
Ayush Sharma 2018-08-20 16:29:47 +02:00 committed by Juan Picado @jotadeveloper
parent 1fc1a33c4c
commit 9cd3ccb408
28 changed files with 1029 additions and 659 deletions

View File

@ -34,7 +34,8 @@
}], "flow", "react"],
"plugins": [
"transform-class-properties",
"transform-object-rest-spread"
"transform-object-rest-spread",
"babel-plugin-dynamic-import-node"
]
},
"testOldEnv": {

View File

@ -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'

View File

@ -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",

View File

@ -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,
});
}

View File

@ -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>
);
}
}

View File

@ -41,5 +41,9 @@
background-color: $saltpan;
}
}
.usernameField {
margin-right: 10px;
}
}

View File

@ -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;

View 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>
);
}
}

View File

@ -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}/>;

View File

@ -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} />;
}
}

View File

@ -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;

View File

@ -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
View 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
View 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);
}
}

View File

@ -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();
})

View File

@ -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
View 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);
});
});

View File

@ -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}`);
};
/**

View 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`;
}

View File

@ -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>"`;

View File

@ -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>"`;

View File

@ -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();
});
})

View 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();
});
});

View File

@ -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
});
}
});

View 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']
}
}
];

View 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);
});
});

View 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');
});
});

View File

@ -1015,6 +1015,13 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-dynamic-import-node@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.0.0.tgz#d6fc3f6c5e3bdc34e49c15faca7ce069755c0a57"
dependencies:
babel-plugin-syntax-dynamic-import "^6.18.0"
object.assign "^4.1.0"
babel-plugin-flow-runtime@0.17.0:
version "0.17.0"
resolved "https://registry.npmjs.org/babel-plugin-flow-runtime/-/babel-plugin-flow-runtime-0.17.0.tgz#51d7b2e6a526edc48fad8d8eed45bf7cc32ffc08"
@ -8296,9 +8303,9 @@ react-click-outside@^3.0.0:
dependencies:
hoist-non-react-statics "^2.1.1"
react-dom@16.2.0:
version "16.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
react-dom@16.4.2:
version "16.4.2"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.4.2.tgz#4afed569689f2c561d2b8da0b819669c38a0bda4"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
@ -8373,9 +8380,9 @@ react-test-renderer@^16.0.0-0:
prop-types "^15.6.0"
react-is "^16.4.1"
react@16.2.0:
version "16.2.0"
resolved "https://registry.npmjs.org/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
react@16.4.2:
version "16.4.2"
resolved "https://registry.npmjs.org/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"