From 9cd3ccb4086e09187ca750de95767c8f61848795 Mon Sep 17 00:00:00 2001 From: Ayush Sharma Date: Mon, 20 Aug 2018 16:29:47 +0200 Subject: [PATCH] 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 * 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 --- .babelrc | 3 +- jest.config.js | 2 +- package.json | 7 +- src/lib/crypto-utils.js | 2 +- src/webui/app.js | 152 ++++++++- src/webui/components/Header/header.scss | 4 + src/webui/components/Header/index.js | 291 ++++------------ src/webui/components/Login/index.js | 111 +++++++ src/webui/modules/detail/index.jsx | 24 +- src/webui/modules/home/index.js | 42 +-- src/webui/router.js | 51 ++- src/webui/utils/asyncComponent.js | 2 +- src/webui/utils/login.js | 73 ++++ src/webui/utils/logo.js | 10 + test/e2e/e2e.spec.js | 2 +- test/unit/setup.js | 26 +- test/unit/webui/app.spec.js | 108 ++++++ test/unit/webui/components/__mocks__/api.js | 9 +- test/unit/webui/components/__mocks__/token.js | 26 ++ .../__snapshots__/header.spec.js.snap | 47 +-- .../__snapshots__/login.spec.js.snap | 5 + test/unit/webui/components/header.spec.js | 312 ++---------------- test/unit/webui/components/login.spec.js | 79 +++++ test/unit/webui/components/store/login.js | 3 +- test/unit/webui/components/store/package.js | 168 ++++++++++ test/unit/webui/utils/login.spec.js | 98 ++++++ test/unit/webui/utils/logo.spec.js | 12 + yarn.lock | Bin 377212 -> 377522 bytes 28 files changed, 1016 insertions(+), 653 deletions(-) create mode 100644 src/webui/components/Login/index.js create mode 100644 src/webui/utils/login.js create mode 100644 src/webui/utils/logo.js create mode 100644 test/unit/webui/app.spec.js create mode 100644 test/unit/webui/components/__mocks__/token.js create mode 100644 test/unit/webui/components/__snapshots__/login.spec.js.snap create mode 100644 test/unit/webui/components/login.spec.js create mode 100644 test/unit/webui/components/store/package.js create mode 100644 test/unit/webui/utils/login.spec.js create mode 100644 test/unit/webui/utils/logo.spec.js diff --git a/.babelrc b/.babelrc index 9a7ab1b1f..6d6216c25 100644 --- a/.babelrc +++ b/.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": { diff --git a/jest.config.js b/jest.config.js index fc113d40a..38d2305ae 100644 --- a/jest.config.js +++ b/jest.config.js @@ -40,7 +40,7 @@ module.exports = { '/test', ], moduleNameMapper: { - '\\.(scss)$': '/node_modules/identity-obj-proxy', + '\\.(s?css)$': '/node_modules/identity-obj-proxy', 'github-markdown-css': '/node_modules/identity-obj-proxy', '\\.(png)$': '/node_modules/identity-obj-proxy', '\\.(svg)$': '/test/unit/empty.js' diff --git a/package.json b/package.json index 0d08fabcf..ab68bb6ac 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/crypto-utils.js b/src/lib/crypto-utils.js index 8c4c376ad..8bf2f535d 100644 --- a/src/lib/crypto-utils.js +++ b/src/lib/crypto-utils.js @@ -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, }); } diff --git a/src/webui/app.js b/src/webui/app.js index 0272e070a..025b28279 100644 --- a/src/webui/app.js +++ b/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:
+ */ + toggleLoginModal() { + this.setState((prevState) => ({ + showLoginModal: !prevState.showLoginModal, + error: {} + })); + } + + /** + * handles login + * Required by:
+ */ + 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:
+ */ + handleLogout() { + storage.removeItem('username'); + storage.removeItem('token'); + this.setState({ + user: {}, + isUserLoggedIn: false + }); + } + + renderHeader() { + const { + logoUrl, + user, + scope, + } = this.state; + return
; + } + + renderLoginModal() { + const { + error, + showLoginModal + } = this.state; + return ; + } + render() { + const {isUserLoggedIn} = this.state; return ( - +
+ {this.renderHeader()} + {this.renderLoginModal()} + +
+
); } } diff --git a/src/webui/components/Header/header.scss b/src/webui/components/Header/header.scss index 230ba0acf..c4a1ebce4 100644 --- a/src/webui/components/Header/header.scss +++ b/src/webui/components/Header/header.scss @@ -41,5 +41,9 @@ .header { background-color: $saltpan; } } + + .usernameField { + margin-right: 10px; + } } diff --git a/src/webui/components/Header/index.js b/src/webui/components/Header/index.js index 83fb0b402..7226ba490 100644 --- a/src/webui/components/Header/index.js +++ b/src/webui/components/Header/index.js @@ -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 ( +
+
+ + + +
+ npm set {scope} + registry {registryUrl} +
+ npm adduser --registry {registryUrl} +
- 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 ( -
- - Hi, {username} - - -
- ); - } else { - return ( - - ); - } - } - - render() { - const registryURL = getRegistryURL(); - const {logo, scope, loginError, showLogin} = this.state; - return ( -
-
- - - -
- npm set { scope }registry { registryURL } -
- npm adduser --registry { registryURL } -
- -
- {this.renderUserActionButton()} -
-
- - -
- - {loginError && ( - - )} -
- -
-
- -
- - + Hi, {capitalize(username)} + - -
-
-
- ); - } -} +
+ ) : ( + + )} + + +
+ ); +}; + +Header.propTypes = { + logo: PropTypes.string, + scope: PropTypes.string, + username: PropTypes.string, + handleLogout: PropTypes.func.isRequired, + toggleLoginModal: PropTypes.func.isRequired +}; + +export default Header; diff --git a/src/webui/components/Login/index.js b/src/webui/components/Login/index.js new file mode 100644 index 000000000..5b6ea394d --- /dev/null +++ b/src/webui/components/Login/index.js @@ -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: + */ + 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 ? ( + + ) : ''; + } + + render() { + const {visibility, onCancel, error} = this.props; + const {username, password} = this.state; + return ( +
+ +
+ + {this.renderLoginError(error)} +
+ +
+
+ +
+ + + + +
+
+
+ ); + } +} diff --git a/src/webui/modules/detail/index.jsx b/src/webui/modules/detail/index.jsx index bfab25b34..77c20ae12 100644 --- a/src/webui/modules/detail/index.jsx +++ b/src/webui/modules/detail/index.jsx @@ -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 ; diff --git a/src/webui/modules/home/index.js b/src/webui/modules/home/index.js index edc86850a..261276a58 100644 --- a/src/webui/modules/home/index.js +++ b/src/webui/modules/home/index.js @@ -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 ( -
+ {this.renderSearchBar()} - {this.state.loading ? this.renderLoading() : this.renderPackageList()} -
+ {loading ? ( + + ) : ( + + )} + ); } @@ -112,12 +120,4 @@ export default class Home extends React.Component { } return ; } - - renderLoading() { - return ; - } - - renderPackageList() { - return ; - } } diff --git a/src/webui/router.js b/src/webui/router.js index 7dbd78662..cbf9f8428 100644 --- a/src/webui/router.js +++ b/src/webui/router.js @@ -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 ( - -
-
+class RouterApp extends Component { + static propTypes = { + isUserLoggedIn: PropTypes.bool + }; + render() { + const {isUserLoggedIn} = this.props; + return ( +
- - - + } + /> + ( + + )} + /> + ( + + )} + />
-
-
-
- ); -}; + + ); + } +} export default RouterApp; diff --git a/src/webui/utils/asyncComponent.js b/src/webui/utils/asyncComponent.js index ed11243a5..63a7c9085 100644 --- a/src/webui/utils/asyncComponent.js +++ b/src/webui/utils/asyncComponent.js @@ -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; diff --git a/src/webui/utils/login.js b/src/webui/utils/login.js new file mode 100644 index 000000000..10be9b4c3 --- /dev/null +++ b/src/webui/utils/login.js @@ -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}; + } +} diff --git a/src/webui/utils/logo.js b/src/webui/utils/logo.js new file mode 100644 index 000000000..92c9918c4 --- /dev/null +++ b/src/webui/utils/logo.js @@ -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); + } +} diff --git a/test/e2e/e2e.spec.js b/test/e2e/e2e.spec.js index 90e1d19ed..0984fba4f 100644 --- a/test/e2e/e2e.spec.js +++ b/test/e2e/e2e.spec.js @@ -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(); }) diff --git a/test/unit/setup.js b/test/unit/setup.js index ea569fe7e..12ae7e72e 100644 --- a/test/unit/setup.js +++ b/test/unit/setup.js @@ -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(); \ No newline at end of file +global.__APP_VERSION__ = '1.0.0'; \ No newline at end of file diff --git a/test/unit/webui/app.spec.js b/test/unit/webui/app.spec.js new file mode 100644 index 000000000..02dc1c3ad --- /dev/null +++ b/test/unit/webui/app.spec.js @@ -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(); + }); + 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); + }); +}); diff --git a/test/unit/webui/components/__mocks__/api.js b/test/unit/webui/components/__mocks__/api.js index afd8eeb44..b6eaeeeb6 100644 --- a/test/unit/webui/components/__mocks__/api.js +++ b/test/unit/webui/components/__mocks__/api.js @@ -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}`); }; /** diff --git a/test/unit/webui/components/__mocks__/token.js b/test/unit/webui/components/__mocks__/token.js new file mode 100644 index 000000000..cf88eb47f --- /dev/null +++ b/test/unit/webui/components/__mocks__/token.js @@ -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`; +} \ No newline at end of file diff --git a/test/unit/webui/components/__snapshots__/header.spec.js.snap b/test/unit/webui/components/__snapshots__/header.spec.js.snap index b151b5df3..f81729918 100644 --- a/test/unit/webui/components/__snapshots__/header.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/header.spec.js.snap @@ -1,48 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`
component shallow renderUserActionButton - should show login button 1`] = ` - -`; +exports[`
component shallow should load header component in login state 1`] = `"
npm set scope:registry http://localhost
npm adduser --registry http://localhost
Hi, Verdaccio
"`; -exports[`
component shallow renderUserActionButton - should show users as loggedin 1`] = ` -
- - Hi, - Sam - - -
-`; - -exports[`
snapshot for loggedin user should match snapshot 1`] = `"
npm set registry http://localhost
npm adduser --registry http://localhost
Hi, Verdaccio
Login



"`; - -exports[`
snapshot test shoud match snapshot 1`] = `"
npm set registry http://localhost
npm adduser --registry http://localhost
Login



"`; +exports[`
component shallow should load header component in logout state 1`] = `"
npm set scope:registry http://localhost
npm adduser --registry http://localhost
"`; diff --git a/test/unit/webui/components/__snapshots__/login.spec.js.snap b/test/unit/webui/components/__snapshots__/login.spec.js.snap new file mode 100644 index 000000000..b5a6c3987 --- /dev/null +++ b/test/unit/webui/components/__snapshots__/login.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should load the component in default state 1`] = `"
Login



"`; + +exports[` should load the component with props 1`] = `"
Login
Error Title

Error Description




"`; diff --git a/test/unit/webui/components/header.spec.js b/test/unit/webui/components/header.spec.js index 1de31b46f..290654102 100644 --- a/test/unit/webui/components/header.spec.js +++ b/test/unit/webui/components/header.spec.js @@ -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('
component shallow', () => { - let wrapper; - beforeEach(() => { - wrapper = shallow( - -
- - ); + it('should give error for required props', () => { + shallow(
); + 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( - - - ); - 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('
snapshot test', () => { - it('shoud match snapshot', () => { - const wrapper = mount( - -
- - ); + const wrapper = shallow(
); + wrapper.find('.header-button-logout').simulate('click'); + expect(props.handleLogout).toHaveBeenCalled(); expect(wrapper.html()).toMatchSnapshot(); }); -}); -describe('
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( - -
- ); + it('should load header component in logout state', () => { + const props = { + username: undefined, + logo: 'logo.png', + scope: 'scope:', + handleLogout: () => {}, + toggleLoginModal: jest.fn() + } + const wrapper = shallow(
); + wrapper.find('.header-button-login').simulate('click'); + expect(props.toggleLoginModal).toHaveBeenCalled(); expect(wrapper.html()).toMatchSnapshot(); }); }) diff --git a/test/unit/webui/components/login.spec.js b/test/unit/webui/components/login.spec.js new file mode 100644 index 000000000..850980a27 --- /dev/null +++ b/test/unit/webui/components/login.spec.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import LoginModal from '../../../../src/webui/components/Login'; + +describe('', () => { + it('should load the component in default state', () => { + const wrapper = mount(); + 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(); + 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(); + 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(); + 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(); + const { submitCredentials } = wrapper.instance(); + wrapper.setState({username: 'sam', password: 1234}) + await submitCredentials(event); + expect(props.onSubmit).toHaveBeenCalledWith('sam', 1234); + expect(event.preventDefault).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/webui/components/store/login.js b/test/unit/webui/components/store/login.js index bbe89e9e1..4c4d02772 100644 --- a/test/unit/webui/components/store/login.js +++ b/test/unit/webui/components/store/login.js @@ -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 }); } }); diff --git a/test/unit/webui/components/store/package.js b/test/unit/webui/components/store/package.js new file mode 100644 index 000000000..fe957a59a --- /dev/null +++ b/test/unit/webui/components/store/package.js @@ -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'] + } + } +]; diff --git a/test/unit/webui/utils/login.spec.js b/test/unit/webui/utils/login.spec.js new file mode 100644 index 000000000..c3ee2f6b6 --- /dev/null +++ b/test/unit/webui/utils/login.spec.js @@ -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); + }); +}); diff --git a/test/unit/webui/utils/logo.spec.js b/test/unit/webui/utils/logo.spec.js new file mode 100644 index 000000000..7ddce77af --- /dev/null +++ b/test/unit/webui/utils/logo.spec.js @@ -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'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 4d61bfa783a27ffada5fa4ed0853c4e00f690ebe..4390eae056e2f08e9a2e8fd0e8ffd3ebc53a8f0a 100644 GIT binary patch delta 361 zcmZ{dze~eF9L1?kMTHLjA{_*S&X?X@a@U+wcmD}5-@AlBo7P%IovKBOodOwd73v=# zE_L=laCGmcI4C%Y7zDS<f%EE_=fM*qTnpm1q6~AbEQK delta 222 zcmdngD*mTQe8Z;+%^H*1H6}3vG1GR9Nz5fef-&n(< zH$9+{MM4iKsaKL-rEF$tU|?)$ZeeC%m~4=oXlR*aZeW>YY@BFeY;0zkmTG2bWRh$! zeW5O^#P)TKEGC>djV)+kVX@ZN)h|j-OfJziG=p1_Xl$62Vw94YWMP?