mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 16:28:43 -03:30
Merge pull request #7 from ansible/testing
add unit and functional testing to the app
This commit is contained in:
16
.eslintrc
16
.eslintrc
@@ -9,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb",
|
"airbnb"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
@@ -18,22 +18,24 @@
|
|||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true
|
"node": true,
|
||||||
|
"jest": true
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"window": true,
|
"window": true
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"camelcase": "off",
|
"camelcase": "off",
|
||||||
"arrow-parens": "off",
|
"arrow-parens": "off",
|
||||||
"comma-dangle": "off",
|
"comma-dangle": "off",
|
||||||
|
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||||
"indent": ["error", 2, {
|
"indent": ["error", 2, {
|
||||||
"SwitchCase": 1
|
"SwitchCase": 1
|
||||||
}],
|
}],
|
||||||
"max-len": ['error', {
|
"max-len": ["error", {
|
||||||
"code": 100,
|
"code": 100,
|
||||||
"ignoreStrings": true,
|
"ignoreStrings": true,
|
||||||
"ignoreTemplateLiterals": true,
|
"ignoreTemplateLiterals": true
|
||||||
}],
|
}],
|
||||||
"no-continue": "off",
|
"no-continue": "off",
|
||||||
"no-debugger": "off",
|
"no-debugger": "off",
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
"no-plusplus": "off",
|
"no-plusplus": "off",
|
||||||
"no-underscore-dangle": "off",
|
"no-underscore-dangle": "off",
|
||||||
"no-use-before-define": "off",
|
"no-use-before-define": "off",
|
||||||
"no-multiple-empty-lines": ["error", { max: 1 }],
|
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||||
"object-curly-newline": "off",
|
"object-curly-newline": "off",
|
||||||
"space-before-function-paren": ["error", "always"],
|
"space-before-function-paren": ["error", "always"],
|
||||||
"no-trailing-spaces": ["error"],
|
"no-trailing-spaces": ["error"],
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
coverage/
|
||||||
21
__mocks__/axios.js
Normal file
21
__mocks__/axios.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
jest.genMockFromModule('axios');
|
||||||
|
|
||||||
|
axios.create = jest.fn(() => axios);
|
||||||
|
axios.get = jest.fn(() => axios);
|
||||||
|
axios.post = jest.fn(() => axios);
|
||||||
|
axios.create.mockReturnValue({
|
||||||
|
get: axios.get,
|
||||||
|
post: axios.post
|
||||||
|
});
|
||||||
|
axios.get.mockResolvedValue('get results');
|
||||||
|
axios.post.mockResolvedValue('post results');
|
||||||
|
|
||||||
|
axios.customClearMocks = () => {
|
||||||
|
axios.create.mockClear();
|
||||||
|
axios.get.mockClear();
|
||||||
|
axios.post.mockClear();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = axios;
|
||||||
1
__tests__/stubs/svgStub.js
Normal file
1
__tests__/stubs/svgStub.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = 'svg-stub';
|
||||||
37
__tests__/tests/App.test.jsx
Normal file
37
__tests__/tests/App.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow, mount } from 'enzyme';
|
||||||
|
import App from '../../src/App';
|
||||||
|
import api from '../../src/api';
|
||||||
|
import Dashboard from '../../src/pages/Dashboard';
|
||||||
|
import Login from '../../src/pages/Login';
|
||||||
|
|
||||||
|
describe('<App />', () => {
|
||||||
|
test('renders without crashing', () => {
|
||||||
|
const appWrapper = shallow(<App />);
|
||||||
|
expect(appWrapper.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders login page when not authenticated', () => {
|
||||||
|
api.isAuthenticated = jest.fn();
|
||||||
|
api.isAuthenticated.mockReturnValue(false);
|
||||||
|
|
||||||
|
const appWrapper = mount(<App />);
|
||||||
|
|
||||||
|
const login = appWrapper.find(Login);
|
||||||
|
expect(login.length).toBe(1);
|
||||||
|
const dashboard = appWrapper.find(Dashboard);
|
||||||
|
expect(dashboard.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders dashboard when authenticated', () => {
|
||||||
|
api.isAuthenticated = jest.fn();
|
||||||
|
api.isAuthenticated.mockReturnValue(true);
|
||||||
|
|
||||||
|
const appWrapper = mount(<App />);
|
||||||
|
|
||||||
|
const dashboard = appWrapper.find(Dashboard);
|
||||||
|
expect(dashboard.length).toBe(1);
|
||||||
|
const login = appWrapper.find(Login);
|
||||||
|
expect(login.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
__tests__/tests/ConditionalRedirect.test.jsx
Normal file
27
__tests__/tests/ConditionalRedirect.test.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
Redirect
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ConditionalRedirect from '../../src/components/ConditionalRedirect';
|
||||||
|
|
||||||
|
describe('<ConditionalRedirect />', () => {
|
||||||
|
test('renders Redirect when shouldRedirect is passed truthy func', () => {
|
||||||
|
const truthyFunc = () => true;
|
||||||
|
const shouldHaveRedirectChild = shallow(<ConditionalRedirect shouldRedirect={() => truthyFunc()} />);
|
||||||
|
const redirectChild = shouldHaveRedirectChild.find(Redirect);
|
||||||
|
expect(redirectChild.length).toBe(1);
|
||||||
|
const routeChild = shouldHaveRedirectChild.find(Route);
|
||||||
|
expect(routeChild.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders Route when shouldRedirect is passed falsy func', () => {
|
||||||
|
const falsyFunc = () => false;
|
||||||
|
const shouldHaveRouteChild = shallow(<ConditionalRedirect shouldRedirect={() => falsyFunc()} />);
|
||||||
|
const routeChild = shouldHaveRouteChild.find(Route);
|
||||||
|
expect(routeChild.length).toBe(1);
|
||||||
|
const redirectChild = shouldHaveRouteChild.find(Redirect);
|
||||||
|
expect(redirectChild.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
__tests__/tests/LoginPage.test.jsx
Normal file
37
__tests__/tests/LoginPage.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { shallow, mount } from 'enzyme';
|
||||||
|
import LoginPage from '../../src/pages/Login';
|
||||||
|
import api from '../../src/api';
|
||||||
|
import Dashboard from '../../src/pages/Dashboard';
|
||||||
|
|
||||||
|
describe('<LoginPage />', () => {
|
||||||
|
let loginWrapper, usernameInput, passwordInput, errorTextArea, submitButton;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loginWrapper = mount(<MemoryRouter><LoginPage /></MemoryRouter>);
|
||||||
|
usernameInput = loginWrapper.find('.pf-c-form__group#username TextInput');
|
||||||
|
passwordInput = loginWrapper.find('.pf-c-form__group#password TextInput');
|
||||||
|
errorTextArea = loginWrapper.find('.pf-c-form__helper-text.pf-m-error');
|
||||||
|
submitButton = loginWrapper.find('Button[type="submit"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders without crashing', () => {
|
||||||
|
expect(loginWrapper.length).toBe(1);
|
||||||
|
expect(usernameInput.length).toBe(1);
|
||||||
|
expect(passwordInput.length).toBe(1);
|
||||||
|
expect(errorTextArea.length).toBe(1);
|
||||||
|
expect(submitButton.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// initially renders empty username and password fields, sets empty error message and makes submit button not disabled
|
||||||
|
|
||||||
|
// typing into username and password fields (if the component is not unmounting) will clear out any error message
|
||||||
|
|
||||||
|
// when the submit Button is clicked, as long as it is not disabled state.loading is set to true
|
||||||
|
// api.login is called with param 1 username and param 2 password
|
||||||
|
// if api.login returns an error, the state.error should be set to LOGIN_ERROR_MESSAGE, if the error object returned has response.status set to 401.
|
||||||
|
// regardless of error or not, after api.login returns, state.loading should be set to false
|
||||||
|
|
||||||
|
// if api.isAuthenticated mock returns true, Redirect to / should be rendered
|
||||||
|
});
|
||||||
117
__tests__/tests/api.test.js
Normal file
117
__tests__/tests/api.test.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
import mockAxios from 'axios';
|
||||||
|
import APIClient from '../../src/api';
|
||||||
|
|
||||||
|
const API_ROOT = '/api/';
|
||||||
|
const API_LOGIN = `${API_ROOT}login/`;
|
||||||
|
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||||
|
const API_V2 = `${API_ROOT}v2/`;
|
||||||
|
const API_CONFIG = `${API_V2}config/`;
|
||||||
|
const API_PROJECTS = `${API_V2}projects/`;
|
||||||
|
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||||
|
|
||||||
|
const CSRF_COOKIE_NAME = 'csrftoken';
|
||||||
|
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
||||||
|
|
||||||
|
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
|
describe('APIClient (api.js)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.customClearMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructor calls axios create', () => {
|
||||||
|
const csrfObj = {
|
||||||
|
xsrfCookieName: CSRF_COOKIE_NAME,
|
||||||
|
xsrfHeaderName: CSRF_HEADER_NAME
|
||||||
|
};
|
||||||
|
expect(mockAxios.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.create).toHaveBeenCalledWith(csrfObj);
|
||||||
|
expect(APIClient.http).toHaveProperty('get');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isAuthenticated checks authentication and sets cookie from document', () => {
|
||||||
|
APIClient.getCookie = jest.fn();
|
||||||
|
const invalidCookie = 'invalid';
|
||||||
|
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||||
|
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
|
||||||
|
APIClient.getCookie.mockReturnValue(invalidCookie);
|
||||||
|
expect(APIClient.isAuthenticated()).toBe(false);
|
||||||
|
APIClient.getCookie.mockReturnValue(validLoggedOutCookie);
|
||||||
|
expect(APIClient.isAuthenticated()).toBe(false);
|
||||||
|
APIClient.getCookie.mockReturnValue(validLoggedInCookie);
|
||||||
|
expect(APIClient.isAuthenticated()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login calls get and post to login route, and sets cookie from document', (done) => {
|
||||||
|
const un = 'foo';
|
||||||
|
const pw = 'bar';
|
||||||
|
const next = 'baz';
|
||||||
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||||
|
APIClient.setCookie = jest.fn();
|
||||||
|
APIClient.login(un, pw, next).then(() => {
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_LOGIN, { headers });
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login encodes uri components for username, password and redirect', (done) => {
|
||||||
|
const un = '/foo/';
|
||||||
|
const pw = '/bar/';
|
||||||
|
const next = '/baz/';
|
||||||
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
const data = `username=${encodeURIComponent(un)}&password=${encodeURIComponent(pw)}&next=${encodeURIComponent(next)}`;
|
||||||
|
APIClient.login(un, pw, next).then(() => {
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login redirect defaults to config route when not explicitly passed', (done) => {
|
||||||
|
const un = 'foo';
|
||||||
|
const pw = 'bar';
|
||||||
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
const data = `username=${un}&password=${pw}&next=${encodeURIComponent(API_CONFIG)}`;
|
||||||
|
APIClient.setCookie = jest.fn();
|
||||||
|
APIClient.login(un, pw).then(() => {
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.post).toHaveBeenCalledWith(API_LOGIN, data, { headers });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout calls get to logout route', () => {
|
||||||
|
APIClient.logout();
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_LOGOUT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getConfig calls get to config route', () => {
|
||||||
|
APIClient.getConfig();
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_CONFIG);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getProjects calls get to projects route', () => {
|
||||||
|
APIClient.getProjects();
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_PROJECTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getOrganigzations calls get to organizations route', () => {
|
||||||
|
APIClient.getOrganizations();
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_ORGANIZATIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRoot calls get to root route', () => {
|
||||||
|
APIClient.getRoot();
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAxios.get).toHaveBeenCalledWith(API_ROOT);
|
||||||
|
});
|
||||||
|
});
|
||||||
4
enzyme.config.js
Normal file
4
enzyme.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const enzyme = require('enzyme');
|
||||||
|
const Adapter = require('enzyme-adapter-react-16');
|
||||||
|
|
||||||
|
enzyme.configure({ adapter: new Adapter() });
|
||||||
5237
package-lock.json
generated
5237
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -2,10 +2,10 @@
|
|||||||
"name": "awx-react",
|
"name": "awx-react",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.jsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --config ./webpack.config.js --mode development",
|
"start": "webpack-dev-server --config ./webpack.config.js --mode development",
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "jest --watchAll --coverage",
|
||||||
"lint": "./node_modules/eslint/bin/eslint.js src/**/*.js src/**/*.jsx"
|
"lint": "./node_modules/eslint/bin/eslint.js src/**/*.js src/**/*.jsx"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -14,16 +14,20 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^10.0.0",
|
"babel-eslint": "^10.0.0",
|
||||||
|
"babel-jest": "^23.6.0",
|
||||||
"babel-loader": "^7.1.5",
|
"babel-loader": "^7.1.5",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"babel-preset-stage-2": "^6.24.1",
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
|
"enzyme": "^3.7.0",
|
||||||
|
"enzyme-adapter-react-16": "^1.6.0",
|
||||||
"eslint": "^5.6.0",
|
"eslint": "^5.6.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"eslint-plugin-import": "^2.14.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.1.1",
|
"eslint-plugin-jsx-a11y": "^6.1.1",
|
||||||
"eslint-plugin-react": "^7.11.1",
|
"eslint-plugin-react": "^7.11.1",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
|
"jest": "^23.6.0",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.9.3",
|
||||||
"react-hot-loader": "^4.3.3",
|
"react-hot-loader": "^4.3.3",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
@@ -42,5 +46,25 @@
|
|||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"redux": "^4.0.0"
|
"redux": "^4.0.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx}"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^[./a-zA-Z0-9$_-]+\\.svg$": "<rootDir>/__tests__/stubs/svgStub.js"
|
||||||
|
},
|
||||||
|
"setupTestFrameworkScriptFile": "<rootDir>/enzyme.config.js",
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/__tests__/tests/**/*.{js,jsx}"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"testURL": "http://127.0.0.1:3001",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
src/App.jsx
95
src/App.jsx
@@ -14,6 +14,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
Nav,
|
Nav,
|
||||||
|
NavExpandable,
|
||||||
NavGroup,
|
NavGroup,
|
||||||
NavItem,
|
NavItem,
|
||||||
Page,
|
Page,
|
||||||
@@ -34,6 +35,7 @@ import api from './api';
|
|||||||
|
|
||||||
import About from './components/About';
|
import About from './components/About';
|
||||||
import TowerLogo from './components/TowerLogo';
|
import TowerLogo from './components/TowerLogo';
|
||||||
|
import ConditionalRedirect from './components/ConditionalRedirect';
|
||||||
|
|
||||||
import Applications from './pages/Applications';
|
import Applications from './pages/Applications';
|
||||||
import Credentials from './pages/Credentials';
|
import Credentials from './pages/Credentials';
|
||||||
@@ -55,32 +57,6 @@ import Teams from './pages/Teams';
|
|||||||
import Templates from './pages/Templates';
|
import Templates from './pages/Templates';
|
||||||
import Users from './pages/Users';
|
import Users from './pages/Users';
|
||||||
|
|
||||||
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
|
|
||||||
<Route {...rest} render={props => (
|
|
||||||
api.isAuthenticated() ? (
|
|
||||||
<Component {...props}/>
|
|
||||||
) : (
|
|
||||||
<Redirect to={{
|
|
||||||
pathname: '/login',
|
|
||||||
state: { from: props.location }
|
|
||||||
}}/>
|
|
||||||
)
|
|
||||||
)}/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UnauthenticatedRoute = ({ component: Component, ...rest }) => (
|
|
||||||
<Route {...rest} render={props => (
|
|
||||||
!api.isAuthenticated() ? (
|
|
||||||
<Component {...props}/>
|
|
||||||
) : (
|
|
||||||
<Redirect to={{
|
|
||||||
pathname: '/',
|
|
||||||
state: { from: props.location }
|
|
||||||
}}/>
|
|
||||||
)
|
|
||||||
)}/>
|
|
||||||
);
|
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -90,6 +66,8 @@ class App extends React.Component {
|
|||||||
isNavOpen: (typeof window !== 'undefined' &&
|
isNavOpen: (typeof window !== 'undefined' &&
|
||||||
window.innerWidth >= parseInt(breakpointMd.value, 10)),
|
window.innerWidth >= parseInt(breakpointMd.value, 10)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.state.activeGroup = this.state.activeItem.startsWith("settings_group_") ? "settings": "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onNavToggle = () => {
|
onNavToggle = () => {
|
||||||
@@ -98,8 +76,8 @@ class App extends React.Component {
|
|||||||
this.setState({ isNavOpen: !isNavOpen });
|
this.setState({ isNavOpen: !isNavOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
onNavSelect = ({ itemId }) => {
|
onNavSelect = ({ groupId, itemId }) => {
|
||||||
this.setState({ activeItem: itemId });
|
this.setState({ activeGroup: groupId || "", activeItem: itemId });
|
||||||
};
|
};
|
||||||
|
|
||||||
onLogoClick = () => {
|
onLogoClick = () => {
|
||||||
@@ -114,7 +92,7 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { activeItem, isNavOpen } = this.state;
|
const { activeItem, activeGroup, isNavOpen } = this.state;
|
||||||
const { logo, loginInfo } = this.props;
|
const { logo, loginInfo } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -132,8 +110,8 @@ class App extends React.Component {
|
|||||||
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
||||||
}} />
|
}} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<UnauthenticatedRoute path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
|
<ConditionalRedirect shouldRedirect={() => api.isAuthenticated()} redirectPath="/" path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
|
||||||
<AuthenticatedRoute component={() => (
|
<Fragment>
|
||||||
<Page
|
<Page
|
||||||
header={(
|
header={(
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -172,33 +150,46 @@ class App extends React.Component {
|
|||||||
<NavItem to="#/management_jobs" itemId="management_jobs" isActive={activeItem === 'management_jobs'}>Management Jobs</NavItem>
|
<NavItem to="#/management_jobs" itemId="management_jobs" isActive={activeItem === 'management_jobs'}>Management Jobs</NavItem>
|
||||||
<NavItem to="#/instance_groups" itemId="instance_groups" isActive={activeItem === 'instance_groups'}>Instance Groups</NavItem>
|
<NavItem to="#/instance_groups" itemId="instance_groups" isActive={activeItem === 'instance_groups'}>Instance Groups</NavItem>
|
||||||
<NavItem to="#/applications" itemId="applications" isActive={activeItem === 'applications'}>Applications</NavItem>
|
<NavItem to="#/applications" itemId="applications" isActive={activeItem === 'applications'}>Applications</NavItem>
|
||||||
<NavItem to="#/settings" itemId="settings" isActive={activeItem === 'settings'}>Settings</NavItem>
|
<NavExpandable title="Settings" groupId="settings_group" isActive={activeGroup === 'settings_group'}>
|
||||||
|
<NavItem to="#/settings/auth" groupId="settings_group" itemId="settings_group_auth" isActive={activeItem === 'settings_group_auth'}>
|
||||||
|
Authentication
|
||||||
|
</NavItem>
|
||||||
|
<NavItem to="#/settings/jobs" groupId="settings_group" itemId="settings_group_jobs" isActive={activeItem === 'settings_group_jobs'}>
|
||||||
|
Jobs
|
||||||
|
</NavItem>
|
||||||
|
<NavItem to="#/settings/system" groupId="settings_group" itemId="settings_group_system" isActive={activeItem === 'settings_group_system'}>
|
||||||
|
System
|
||||||
|
</NavItem>
|
||||||
|
<NavItem to="#/settings/ui" groupId="settings_group" itemId="settings_group_ui" isActive={activeItem === 'settings_group_ui'}>
|
||||||
|
User Interface
|
||||||
|
</NavItem>
|
||||||
|
</NavExpandable>
|
||||||
</NavGroup>
|
</NavGroup>
|
||||||
</Nav>
|
</Nav>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}>
|
)}>
|
||||||
<Route exact path="/" component={() => (<Redirect to="/home" />)} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} />
|
||||||
<Route path="/home" component={Dashboard} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} />
|
||||||
<Route path="/jobs" component={Jobs} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} />
|
||||||
<Route path="/schedules" component={Schedules} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
|
||||||
<Route path="/portal" component={Portal} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
|
||||||
<Route path="/templates" component={Templates} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
|
||||||
<Route path="/credentials" component={Credentials} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
|
||||||
<Route path="/projects" component={Projects} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
|
||||||
<Route path="/inventories" component={Inventories} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
|
||||||
<Route path="/inventory_scripts" component={InventoryScripts} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
|
||||||
<Route path="/organizations" component={Organizations} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
||||||
<Route path="/users" component={Users} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
|
||||||
<Route path="/teams" component={Teams} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
|
||||||
<Route path="/credential_types" component={CredentialTypes} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
|
||||||
<Route path="/notification_templates" component={NotificationTemplates} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
|
||||||
<Route path="/management_jobs" component={ManagementJobs} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
|
||||||
<Route path="/instance_groups" component={InstanceGroups} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
|
||||||
<Route path="/applications" component={Applications} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
|
||||||
<Route path="/settings" component={Settings} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/settings" component={Settings} />
|
||||||
</Page>
|
</Page>
|
||||||
)} />
|
</Fragment>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
11
src/api.js
11
src/api.js
@@ -11,6 +11,8 @@ const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
|||||||
const CSRF_COOKIE_NAME = 'csrftoken';
|
const CSRF_COOKIE_NAME = 'csrftoken';
|
||||||
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
const CSRF_HEADER_NAME = 'X-CSRFToken';
|
||||||
|
|
||||||
|
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.http = axios.create({
|
this.http = axios.create({
|
||||||
@@ -19,10 +21,15 @@ class APIClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint class-methods-use-this: ["error", { "exceptMethods": ["getCookie"] }] */
|
||||||
|
getCookie () {
|
||||||
|
return document.cookie;
|
||||||
|
}
|
||||||
|
|
||||||
isAuthenticated () {
|
isAuthenticated () {
|
||||||
let authenticated = false;
|
let authenticated = false;
|
||||||
|
|
||||||
const parsed = (`; ${document.cookie}`).split('; userLoggedIn=');
|
const parsed = (`; ${this.getCookie()}`).split('; userLoggedIn=');
|
||||||
|
|
||||||
if (parsed.length === 2) {
|
if (parsed.length === 2) {
|
||||||
authenticated = parsed.pop().split(';').shift() === 'true';
|
authenticated = parsed.pop().split(';').shift() === 'true';
|
||||||
@@ -37,7 +44,7 @@ class APIClient {
|
|||||||
const next = encodeURIComponent(redirect);
|
const next = encodeURIComponent(redirect);
|
||||||
|
|
||||||
const data = `username=${un}&password=${pw}&next=${next}`;
|
const data = `username=${un}&password=${pw}&next=${next}`;
|
||||||
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
|
||||||
|
|
||||||
return this.http.get(API_LOGIN, { headers })
|
return this.http.get(API_LOGIN, { headers })
|
||||||
.then(() => this.http.post(API_LOGIN, data, { headers }));
|
.then(() => this.http.post(API_LOGIN, data, { headers }));
|
||||||
|
|||||||
23
src/components/ConditionalRedirect.jsx
Normal file
23
src/components/ConditionalRedirect.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
Redirect
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
const ConditionalRedirect = ({
|
||||||
|
component: Component,
|
||||||
|
shouldRedirect,
|
||||||
|
redirectPath,
|
||||||
|
location,
|
||||||
|
...props
|
||||||
|
}) => (shouldRedirect() ? (
|
||||||
|
<Redirect to={{
|
||||||
|
pathname: redirectPath,
|
||||||
|
state: { from: location }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Route {...props} render={rest => (<Component {...rest} />)} />
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ConditionalRedirect;
|
||||||
@@ -97,7 +97,7 @@ class LoginPage extends Component {
|
|||||||
onChange={this.handleUsernameChange}
|
onChange={this.handleUsernameChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pf-c-form__group">
|
<div className="pf-c-form__group" id="password">
|
||||||
<label className="pf-c-form__label" htmlFor="pw">
|
<label className="pf-c-form__label" htmlFor="pw">
|
||||||
Password
|
Password
|
||||||
<span className="pf-c-form__label__required" aria-hidden="true">*</span>
|
<span className="pf-c-form__label__required" aria-hidden="true">*</span>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
const TARGET_PORT = 8043;
|
const TARGET_PORT = 8043;
|
||||||
const TARGET = `https://localhost:${TARGET_PORT}`;
|
const TARGET = `https://localhost:${TARGET_PORT}`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/index.js',
|
entry: './src/index.jsx',
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user