diff --git a/app/actions/authActions.js b/app/actions/authActions.js index 82f4248..2af9f4b 100644 --- a/app/actions/authActions.js +++ b/app/actions/authActions.js @@ -1,13 +1,23 @@ import alt from '../alt'; +import login from '../services/authService'; class AuthActions { constructor() { this.generateActions( 'loginBegin', 'loginSuccess', - 'loginFailed', + 'onError', ); } + + login(data) { + this.loginBegin(); + + login(data) + .then((response) => { + this.loginSuccess(response); + }, error => this.onError(error)); + } } export default alt.createActions(AuthActions); diff --git a/app/actions/authActions.test.js b/app/actions/authActions.test.js deleted file mode 100644 index c6b0aa0..0000000 --- a/app/actions/authActions.test.js +++ /dev/null @@ -1,98 +0,0 @@ -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import * as authActions from './authActions'; -import { AUTH } from './actionTypes'; -import * as authService from '../services/authService'; - -const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); - -describe('authActions', () => { - it('should create an action to start a login request', () => { - // Arrange - const expectedAction = { - type: AUTH.LOGIN_BEGIN, - }; - - // Act - const result = authActions.loginRequest(); - - // Assert - expect(result).toEqual(expectedAction); - }); - - it('should create an action to handle a successfully login', () => { - // Arrange - const user = { - name: 'John', - }; - const expectedAction = { - type: AUTH.LOGIN_SUCCESS, - user, - }; - - // Act - const result = authActions.loginSuccess(user); - - // Assert - expect(result).toEqual(expectedAction); - }); - - it('should create an action to handle a failed login', () => { - // Arrange - const message = 'Testing an error.'; - const expectedAction = { - type: AUTH.LOGIN_FAILED, - message, - }; - - // Act - const result = authActions.loginFailed(message); - - // Assert - expect(result).toEqual(expectedAction); - }); - - it('should handle successful login when valid credentials provided', (done) => { - // Arrange - authService.login = jest.fn(() => Promise.resolve({ name: 'John' })); - const username = 'username'; - const password = 'password'; - const user = { - name: 'John', - }; - const store = mockStore({ - auth: {}, - }); - - // Act - return store.dispatch(authActions.login({ username, password })).then(() => { - const actions = store.getActions(); - // Assert - expect(actions[0]).toEqual({ type: AUTH.LOGIN_BEGIN }); - expect(actions[1]).toEqual({ type: AUTH.LOGIN_SUCCESS, user }); - done(); - }); - }); - - it('should handle failed login when invalid credentials provided', (done) => { - // Arrange - authService.login = jest.fn(() => Promise.reject('Invalid credentials.')); - - const username = 'invalid'; - const password = 'invalid'; - const message = 'Invalid credentials.'; - const store = mockStore({ - auth: {}, - }); - - // Act - return store.dispatch(authActions.login(username, password)).then(() => { - const actions = store.getActions(); - // Assert - expect(actions[0]).toEqual({ type: AUTH.LOGIN_BEGIN }); - expect(actions[1]).toEqual({ type: AUTH.LOGIN_FAILED, message }); - done(); - }); - }); -}); diff --git a/app/actions/usersActions.js b/app/actions/usersActions.js index 77166e2..1ef1510 100644 --- a/app/actions/usersActions.js +++ b/app/actions/usersActions.js @@ -6,10 +6,7 @@ import { deleteUsers, } from '../services/userService'; import { omit } from '../utils/functions'; -import { - DEFAULT_USER_VALID_ID_PATHS, - DEFAULT_PAGINATION_QUERY, -} from '../constants'; +import { DEFAULT_USER_VALID_ID_PATHS } from '../constants'; import getUserId from '../utils/user'; class UserActions { diff --git a/app/actions/usersActions.test.js b/app/actions/usersActions.test.js deleted file mode 100644 index 90e2c6a..0000000 --- a/app/actions/usersActions.test.js +++ /dev/null @@ -1,349 +0,0 @@ -import configureStore from 'redux-mock-store'; -import thunkMiddleware from 'redux-thunk'; -import { omit } from '../utils/functions'; -import { - createUser, - getUsers, - selectUser, - updateUser, - deleteUser, -} from './usersActions'; -import * as userService from '../services/userService'; -import { USERS } from './actionTypes'; - -describe('usersActions', () => { - const middlewares = [ - thunkMiddleware, - ]; - const mockStore = configureStore(middlewares); - const defaultReponseStatusProps = { - status: 200, - statusText: 'OK', - ok: true, - }; - const defaultPaginatedResponse = { - count: 1, - page: 0, - limit: 0, - totalPages: 1, - docs: [{ foo: 'bar' }], - }; - let store; - - function createResponse(response) { - return { - ...omit(response, ['data']), - data: { ...response.data }, - }; - } - - describe('When service call is successful', () => { - beforeEach(() => { - store = mockStore({}); - - userService.createUsers = jest.fn(data => Promise.resolve(createResponse({ - ...defaultReponseStatusProps, - data, - }))); - - userService.deleteUsers = jest.fn(() => Promise.resolve(createResponse({ - ...defaultReponseStatusProps, - statusCode: 204, - }))); - - userService.fetchUsers = jest.fn(() => Promise.resolve(createResponse({ - ...defaultReponseStatusProps, - data: defaultPaginatedResponse, - }))); - - userService.updateUsers = jest.fn((userId, user) => Promise.resolve(createResponse({ - ...defaultReponseStatusProps, - data: { - _id: userId, - ...user, - }, - }))); - }); - - afterEach(() => { - userService.createUsers.mockClear(); - userService.deleteUsers.mockClear(); - userService.fetchUsers.mockClear(); - userService.updateUsers.mockClear(); - }); - describe('getUsers', () => { - it('should be defined', () => { - expect(userService.fetchUsers).toBeDefined(); - }); - - it('should be a function', () => { - expect(userService.fetchUsers).toEqual(expect.any(Function)); - }); - - describe('when the service call is successful', () => { - it('should create an action to get users', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { type: USERS.LOADING_COMPLETE }, - { - type: USERS.GET_ALL_SUCCESS, - payload: { - count: 1, - page: 0, - limit: 0, - totalPages: 1, - users: [{ foo: 'bar' }], - }, - }, - ]; - - // Act - await store.dispatch(getUsers()); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); - - describe('selectUser', () => { - describe('when the service call is successful', () => { - it('should create an action to select a user', async () => { - // Arrange - const expectedActions = [{ - payload: { - _id: 'fake.id.john', - name: 'John Doe', - }, - type: USERS.SELECT_SUCCESS, - }]; - - // Act - await store.dispatch(selectUser({ - _id: 'fake.id.john', - name: 'John Doe', - })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); - - describe('createUsers', () => { - it('should be defined', () => { - expect(userService.createUsers).toBeDefined(); - }); - - it('should be a function', () => { - expect(userService.createUsers).toEqual(expect.any(Function)); - }); - - describe('when the service call is successful', () => { - it('should create an action to create a user', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { type: USERS.LOADING_COMPLETE }, - { - type: USERS.CREATE_SUCCESS, - payload: { - name: 'John Doe', - }, - }, - ]; - // Act - await store.dispatch(createUser({ name: 'John Doe' })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); - - describe('updateUsers', () => { - it('should be defined', () => { - expect(userService.updateUsers).toBeDefined(); - }); - - it('should be a function', () => { - expect(userService.updateUsers).toEqual(expect.any(Function)); - }); - - describe('when the service call is successful', () => { - it('should create an action to update a user', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { type: USERS.LOADING_COMPLETE }, - { - type: USERS.UPDATE_SUCCESS, - payload: { - _id: 'fake.id.john', - name: 'John Doe Jr.', - }, - }, - ]; - - // Act - await store.dispatch(updateUser({ - _id: 'fake.id.john', - name: 'John Doe Jr.', - })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); - - describe('deleteUsers', () => { - it('should be defined', () => { - expect(userService.deleteUsers).toBeDefined(); - }); - - it('should be a function', () => { - expect(userService.deleteUsers).toEqual(expect.any(Function)); - }); - - describe('when the service call is successful', () => { - it('should create an action to delete a user', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { type: USERS.LOADING_COMPLETE }, - { - type: USERS.DELETE_SUCCESS, - payload: { - _id: 'fake.id.john', - name: 'John Doe', - }, - }, - ]; - - // Act - await store.dispatch(deleteUser({ - _id: 'fake.id.john', - name: 'John Doe', - })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); - }); - - describe('when the service call fails', () => { - beforeEach(() => { - store = mockStore({}); - - userService.createUsers = jest.fn(() => Promise.reject('Test Error')); - - userService.deleteUsers = jest.fn(() => Promise.reject('Test Error')); - - userService.fetchUsers = jest.fn(() => Promise.reject('Test Error')); - - userService.updateUsers = jest.fn(() => Promise.reject('Test Error')); - }); - - afterEach(() => { - userService.createUsers.mockClear(); - userService.deleteUsers.mockClear(); - userService.fetchUsers.mockClear(); - userService.updateUsers.mockClear(); - }); - - describe('getUsers service fails', () => { - it('should inform when the getUser request fails', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { - type: USERS.LOADING_FAILED, - payload: { - error: 'Test Error', - }, - }, - ]; - - // Act - const getUsersActionResult = getUsers(); - await store.dispatch(getUsersActionResult); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - describe('createUsers service fails', () => { - it('should inform when the createUsers request fails', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { - type: USERS.LOADING_FAILED, - payload: { - error: 'Test Error', - }, - }, - ]; - - // Act - await store.dispatch(createUser({ name: 'John Doe' })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - describe('updateUsers service fails', () => { - it('should inform when the updateUsers request fails', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { - type: USERS.LOADING_FAILED, - payload: { - error: 'Test Error', - }, - }, - ]; - - // Act; - await store.dispatch(updateUser({ - _id: 'fake.id.john', - name: 'John Doe Jr.', - })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - describe('deleteUsers service fails', () => { - it('should inform when the deleteUsers request fails', async () => { - // Arrange - const expectedActions = [ - { type: USERS.LOADING_BEGIN }, - { - type: USERS.LOADING_FAILED, - payload: { - error: 'Test Error', - }, - }, - ]; - - // Act - await store.dispatch(deleteUser({ - _id: 'fake.id.john', - name: 'John Doe Jr.', - })); - - // Assert - expect(store.getActions()).toEqual(expectedActions); - }); - }); - }); -}); diff --git a/app/components/App.test.js b/app/components/App.test.js index f2373ca..7d3cb39 100644 --- a/app/components/App.test.js +++ b/app/components/App.test.js @@ -9,17 +9,11 @@ function setup(props) { describe(' component', () => { it('renders itself', () => { // Arrange Act - const wrapper = setup({ - store: { - subscribe() { }, - dispatch() { }, - getState() { }, - }, - history: {}, - }); + const wrapper = setup({}); // Assert - expect(wrapper.find('Provider')).toHaveLength(1); - expect(wrapper.find('ConnectedRouter')).toHaveLength(1); + expect(wrapper.find('BrowserRouter')).toHaveLength(1); + expect(wrapper.find('Switch')).toHaveLength(1); + expect(wrapper.find('Route')).toHaveLength(3); }); }); diff --git a/app/components/pages/ActionButtons/ActionButtons.jsx b/app/components/pages/ActionButtons/ActionButtons.jsx index f329725..9c622dc 100644 --- a/app/components/pages/ActionButtons/ActionButtons.jsx +++ b/app/components/pages/ActionButtons/ActionButtons.jsx @@ -25,6 +25,7 @@ function getStateFromStore() { user: UserStore.getState().selectedUser, }; } + export class ActionButtons extends React.Component { static propTypes = { user: PropTypes.object, diff --git a/app/components/pages/ActionButtons/ActionButtons.test.js b/app/components/pages/ActionButtons/ActionButtons.test.js index 7564a48..5f6fb22 100644 --- a/app/components/pages/ActionButtons/ActionButtons.test.js +++ b/app/components/pages/ActionButtons/ActionButtons.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ActionButtons, mapStateToProps } from './ActionButtons'; -import initialState from '../../../reducers/initialState'; +import { ActionButtons } from './ActionButtons'; +import UserStore from '../../../stores/UsersStore'; const defaultProps = { user: {}, @@ -11,11 +11,8 @@ const defaultProps = { function setup(props) { const componentProps = { ...defaultProps, ...props }; - return shallow( - , - ); + + return shallow(); } describe('', () => { @@ -30,19 +27,33 @@ describe('', () => { expect(wrapper.find('MsModal')).toHaveLength(1); }); - describe('mapStateToProps function', () => { - it('should return the initial state', () => { - // Arrange - const expectedProps = { - user: {}, - }; + it('should subscribe to store event when component is mounted', () => { + // Arrange + const lintenSpy = spyOn(UserStore, 'listen').and.callThrough(); + const componentWillMountSpy = spyOn(ActionButtons.prototype, 'componentDidMount').and.callThrough(); + const wrapper = setup({ + user: {}, + }); - // Act - const props = mapStateToProps(Object.assign({}, initialState)); + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(lintenSpy).toHaveBeenCalledTimes(1); + }); - // Assert - expect(props).toEqual(expectedProps); + it('should unsubscribe to sor event when component is unmounted', () => { + const unlistenSpy = spyOn(UserStore, 'unlisten').and.callThrough(); + const componentWillMountSpy = spyOn(ActionButtons.prototype, 'componentWillUnmount').and.callThrough(); + const wrapper = setup({ + user: {}, }); + + // Act + wrapper.instance().componentWillUnmount(); + wrapper.update(); + + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(unlistenSpy).toHaveBeenCalledTimes(1); }); describe('componentWillReceiveProps function', () => { diff --git a/app/components/pages/home/HomePage.jsx b/app/components/pages/home/HomePage.jsx index 6358b0c..754c945 100644 --- a/app/components/pages/home/HomePage.jsx +++ b/app/components/pages/home/HomePage.jsx @@ -14,6 +14,7 @@ function getStateFromStore() { users: UserStore.getState().users, }; } + class HomePage extends React.Component { state = { selectedRow: [], @@ -40,8 +41,12 @@ class HomePage extends React.Component { setSelectedRow = (user) => { this.setState({ selectedRow: [user.id], - }, () => UsersActions.selectUser(user)); - }; + }, this.selectUser(user)); + } + + selectUser = (user) => { + UsersActions.selectUser(user); + } handleUserActionType = (type = 'add', user) => { let action = () => {}; diff --git a/app/components/pages/home/HomePage.test.js b/app/components/pages/home/HomePage.test.js index 148415c..b6d6414 100644 --- a/app/components/pages/home/HomePage.test.js +++ b/app/components/pages/home/HomePage.test.js @@ -1,254 +1,145 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { HomePage, mapDispatchToProps, mapStateToProps } from './HomePage'; -import initialState from '../../../reducers/initialState'; +import HomePage from './HomePage'; +import UserStore from '../../../stores/UsersStore'; +import UsersActions from '../../../actions/usersActions'; function setup(props) { return shallow(); } describe(' component', () => { + it('renders itself', () => { - // Arrange Act - const wrapper = setup({ - usersActions: { - getUsers: jest.fn(), - selectUser: jest.fn(), - }, - }); + // Arrange + const wrapper = setup(); // Assert expect(wrapper.find('Header')).toHaveLength(1); expect(wrapper.find('.container')).toHaveLength(1); }); - describe('mapStateToProps functions', () => { - it('should return the initial state of users module', () => { - // Arrange - const expectedProps = { - users: [], - }; + it('should call getAll action when component is mounted', () => { + // Arrange + const getAllSpy = jest.spyOn(UsersActions, 'getAll'); + + const wrapper = setup(); - // Act - const props = mapStateToProps(Object.assign({}, initialState)); - - // Assert - expect(props).toEqual(expectedProps); - }); + // Assert + expect(getAllSpy).toHaveBeenCalledTimes(1); }); - describe('mapDispatchToProps functions', () => { - it('usersActions prop should be defined', () => { - // Arrange - const dispatch = () => {}; + it('should subscribe to store event when component is mounted', () => { + // Arrange + const lintenSpy = spyOn(UserStore, 'listen').and.callThrough(); + const componentWillMountSpy = spyOn(HomePage.prototype, 'componentDidMount').and.callThrough(); + const wrapper = setup({ + user: {}, + }); - // Act - const props = mapDispatchToProps(dispatch); + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(lintenSpy).toHaveBeenCalledTimes(1); + }); - // Assert - expect(props.usersActions).toBeDefined(); + it('should unsubscribe to sor event when component is unmounted', () => { + // Arrange + const unlistenSpy = spyOn(UserStore, 'unlisten').and.callThrough(); + const componentWillMountSpy = spyOn(HomePage.prototype, 'componentWillUnmount').and.callThrough(); + const wrapper = setup({ + user: {}, }); - it('should return the binded actions', () => { - // Arrange - const dispatch = () => {}; - const expectedActions = [ - 'loadingUsersBegin', - 'loadingUsersComplete', - 'loadingUsersFailed', - 'createUsersSuccess', - 'selectUsersSuccess', - 'getUsersSuccess', - 'updateUsersSuccess', - 'deleteUsersSuccess', - 'selectUser', - 'deleteUser', - 'updateUser', - 'createUser', - 'getUsers', - ]; + // Act + wrapper.instance().componentWillUnmount(); + wrapper.update(); - // Act - const props = mapDispatchToProps(dispatch); + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(unlistenSpy).toHaveBeenCalledTimes(1); + }); - // Assert - expect(Object.keys(props.usersActions)).toEqual(expectedActions); + it('should call selectUser action when row is selected', () => { + // Act + const selectUserSpy = spyOn(UsersActions, 'selectUser').and.callThrough(); + const wrapper = setup({ + user: {}, }); - }); - describe('setSelectedRow handler', () => { - it('should select user', () => { - // Arrange - const user = { - id: 'id', - }; - const selectUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser, - getUsers: () => {}, - createUser: () => {}, - updateUser: () => {}, - deleteUser: () => {}, - }, - }); + wrapper.instance().setSelectedRow({ id: 'id' }); - // Act - wrapper.instance().setSelectedRow(user); - - // Assert - expect(selectUser).toHaveBeenCalledTimes(1); - }); + // Assert + expect(wrapper.state().selectedRow).toEqual(['id']); + expect(selectUserSpy).toHaveBeenCalledTimes(1); + }); - it('should select user', () => { + describe('should handle user action by type', () => { + it('should call errorService when type is not assigned', () => { // Arrange + const type = 'test'; const user = { - id: 'id', + id: 'id' }; - const selectUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser: 'text', - getUsers: () => {}, - createUser: () => {}, - updateUser: () => {}, - deleteUser: () => {}, - }, - }); + const createUserSpy = spyOn(UsersActions, 'createUser').and.callThrough(); + const updateUserSpy = spyOn(UsersActions, 'updateUser').and.callThrough(); + const deleteUserSpy = spyOn(UsersActions, 'deleteUser').and.callThrough(); + const wrapper = setup(); // Act - wrapper.instance().setSelectedRow(user); + wrapper.instance().handleUserActionType(type, user); // Assert - expect(selectUser).toHaveBeenCalledTimes(0); + expect(createUserSpy).toHaveBeenCalledTimes(0); + expect(updateUserSpy).toHaveBeenCalledTimes(0); + expect(deleteUserSpy).toHaveBeenCalledTimes(0); }); - }); - describe('handleUserActionType handler', () => { it('should return Add handler', () => { // Arrange const type = 'add'; const user = { - id: 'id', + id: 'id' }; - const createUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser: () => {}, - getUsers: () => {}, - createUser, - updateUser: () => {}, - deleteUser: () => {}, - }, - }); + const createUserSpy = spyOn(UsersActions, 'createUser').and.callThrough(); + const wrapper = setup(); // Act wrapper.instance().handleUserActionType(type, user); // Assert - expect(createUser).toHaveBeenCalledTimes(1); + expect(createUserSpy).toHaveBeenCalledTimes(1); }); - it('should return Edit handler', () => { + it('should return Update handler', () => { // Arrange const type = 'edit'; const user = { - id: 'id', + id: 'id' }; - const updateUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser: () => {}, - getUsers: () => {}, - createUser: () => {}, - updateUser, - deleteUser: () => {}, - }, - }); + const updateUserSpy = spyOn(UsersActions, 'updateUser').and.callThrough(); + const wrapper = setup(); // Act wrapper.instance().handleUserActionType(type, user); // Assert - expect(updateUser).toHaveBeenCalledTimes(1); + expect(updateUserSpy).toHaveBeenCalledTimes(1); }); it('should return Delete handler', () => { // Arrange const type = 'delete'; const user = { - id: 'id', - }; - const deleteUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser: () => {}, - getUsers: () => {}, - createUser: () => {}, - updateUser: () => {}, - deleteUser, - }, - }); - - // Act - wrapper.instance().handleUserActionType(type, user); - - // Assert - expect(deleteUser).toHaveBeenCalledTimes(1); - }); - - it('should return add Handler when type is other than add, edit, delete', () => { - // Arrange - const type = 'other'; - const user = { - id: 'id', - }; - const selectUser = jest.fn(); - const createUser = jest.fn(); - const deleteUser = jest.fn(); - const updateUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser, - getUsers: () => {}, - createUser, - updateUser, - deleteUser, - }, - }); - - // Act - wrapper.instance().handleUserActionType(type, user); - - // Assert - expect(selectUser).toHaveBeenCalledTimes(0); - expect(createUser).toHaveBeenCalledTimes(0); - expect(deleteUser).toHaveBeenCalledTimes(0); - expect(updateUser).toHaveBeenCalledTimes(0); - }); - - it('should return Add as default handler when no type is passed', () => { - // Arrange - const type = undefined; - const user = { - id: 'id', + id: 'id' }; - const createUser = jest.fn(); - const wrapper = setup({ - usersActions: { - selectUser: () => {}, - getUsers: () => {}, - createUser, - updateUser: () => {}, - deleteUser: () => {}, - }, - }); + const deleteUserSpy = spyOn(UsersActions, 'deleteUser').and.callThrough(); + const wrapper = setup(); // Act wrapper.instance().handleUserActionType(type, user); // Assert - expect(createUser).toHaveBeenCalledTimes(1); + expect(deleteUserSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/app/components/pages/login/LoginForm.jsx b/app/components/pages/login/LoginForm.jsx index e3ee3e4..42a37ee 100644 --- a/app/components/pages/login/LoginForm.jsx +++ b/app/components/pages/login/LoginForm.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Alert } from 'reactstrap'; import PropTypes from 'prop-types'; import FormInput from '../../common/form/FormInput'; @@ -6,6 +7,10 @@ import './LoginForm.scss'; class LoginForm extends React.Component { static propTypes = { + error: PropTypes.shape({ + code: PropTypes.number, + message: PropTypes.string, + }).isRequired, onSubmit: PropTypes.func.isRequired, }; @@ -25,6 +30,21 @@ class LoginForm extends React.Component { this.props.onSubmit(this.state.username, this.state.password); } + renderError = () => { + let dataToRender = null; + const { error } = this.props; + + if (error && error.message) { + dataToRender = ( + + {error.message} + + ); + } + + return dataToRender; + } + render() { return (
@@ -55,6 +75,7 @@ class LoginForm extends React.Component { username/password + {this.renderError()} diff --git a/app/components/pages/login/LoginForm.test.js b/app/components/pages/login/LoginForm.test.js index ce435c7..32322bb 100644 --- a/app/components/pages/login/LoginForm.test.js +++ b/app/components/pages/login/LoginForm.test.js @@ -10,7 +10,11 @@ describe(' component', () => { it('renders itself', () => { // Arrange Act const wrapper = setup({ - onSubmit() { }, + error: { + code: null, + message: null, + }, + onSubmit() {}, }); // Assert @@ -21,7 +25,11 @@ describe(' component', () => { it('should handle username Changes', () => { // Arrange const wrapper = setup({ - onSubmit() { }, + error: { + code: null, + message: null, + }, + onSubmit() {}, }); const event = { target: { @@ -44,7 +52,11 @@ describe(' component', () => { it('should handle password Changes', () => { // Arrange const wrapper = setup({ - onSubmit() { }, + error: { + code: null, + message: null, + }, + onSubmit() {}, }); const event = { target: { @@ -67,6 +79,10 @@ describe(' component', () => { it('should handle form submit', () => { // Arrange const props = { + error: { + code: null, + message: null, + }, onSubmit: jest.fn(), }; const wrapper = setup(props); diff --git a/app/components/pages/login/LoginPage.jsx b/app/components/pages/login/LoginPage.jsx index cfc5ad9..0965518 100644 --- a/app/components/pages/login/LoginPage.jsx +++ b/app/components/pages/login/LoginPage.jsx @@ -1,28 +1,67 @@ import React from 'react'; +import { withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; -import * as authActions from '../../../actions/authActions'; import LoginForm from './LoginForm'; +import authActions from '../../../actions/authActions'; +import AuthStore from '../../../stores/AuthStore'; import './LoginPage.scss'; +function getStateFromStore() { + return { + auth: AuthStore.getState().auth, + }; +} + export class LoginPage extends React.Component { static propTypes = { - actions: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, }; - handleOnSubmit = (username, password) => { - this.props.actions.login({ username, password }); + state = { + auth: { + error: { + code: null, + message: null, + }, + user: null, + }, }; + componentDidMount() { + AuthStore.listen(this.onStoreChange); + } + + componentWillUnmount() { + AuthStore.unlisten(this.onStoreChange); + } + + onStoreChange = () => { + const { auth } = getStateFromStore(); + this.setState({ + auth, + }, this.authenticate(auth.authenticated)); + } + + authenticate = (authenticated) => { + if (authenticated) { + this.props.history.push('/'); + } + } + + handleOnSubmit = (username, password) => { + authActions.login({ username, password }); + } + render() { return (
- +
); } } -export default LoginPage; +export default withRouter(LoginPage); diff --git a/app/components/pages/login/LoginPage.test.js b/app/components/pages/login/LoginPage.test.js index 2ea2cba..fbb2bb0 100644 --- a/app/components/pages/login/LoginPage.test.js +++ b/app/components/pages/login/LoginPage.test.js @@ -1,7 +1,8 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import initialState from '../../../reducers/initialState'; -import { LoginPage, mapDispatchToProps, mapStateToProps } from './LoginPage'; +import { shallow, mount } from 'enzyme'; +import { LoginPage } from './LoginPage'; +import authActions from '../../../actions/authActions'; +import AuthStore from '../../../stores/AuthStore'; function setup(props) { return shallow(); @@ -9,65 +10,81 @@ function setup(props) { describe(' component', () => { it('renders itself', () => { + // Arrange const wrapper = setup({ - actions: {}, + history: { + push: jest.fn, + }, }); + // Assert expect(wrapper.find('section')).toHaveLength(1); expect(wrapper.find('LoginForm')).toHaveLength(1); }); - it('should handle form submit itself', () => { - const login = jest.fn(); + it('should subscribe to store event when component is mounted', () => { + // Arrange + const lintenSpy = spyOn(AuthStore, 'listen').and.callThrough(); + const componentWillMountSpy = spyOn(LoginPage.prototype, 'componentDidMount').and.callThrough(); const wrapper = setup({ - actions: { - login, + history: { + push: jest.fn, }, }); - const form = wrapper.find('LoginForm'); - - form.simulate('submit'); - expect(login).toHaveBeenCalledTimes(1); + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(lintenSpy).toHaveBeenCalledTimes(1); }); - describe('mapStateToProps functions', () => { - it('should return the initial state of auth module', () => { - const expectedProps = { - authenticating: false, - isAuthenticated: false, - error: false, - errorMessage: null, - user: null, - }; + it('should unsubscribe to sor event when component is unmounted', () => { + // Arrange + const unlistenSpy = spyOn(AuthStore, 'unlisten').and.callThrough(); + const componentWillMountSpy = spyOn(LoginPage.prototype, 'componentWillUnmount').and.callThrough(); + const wrapper = setup({ + history: { + push: jest.fn, + }, + }); - const props = mapStateToProps(Object.assign({}, initialState)); + // Act + wrapper.instance().componentWillUnmount(); + wrapper.update(); - expect(props).toEqual(expectedProps); - }); + // Assert + expect(componentWillMountSpy).toHaveBeenCalledTimes(1); + expect(unlistenSpy).toHaveBeenCalledTimes(1); }); + + it('should handle form submit', () => { + // Arrange + const loginSpy = jest.spyOn(authActions, 'login'); + const wrapper = setup({ + history: { + push: jest.fn, + }, + }); - describe('mapDispatchToProps functions', () => { - it('actions prop should be defined', () => { - const dispatch = () => {}; - - const props = mapDispatchToProps(dispatch); + // Act + wrapper.instance().handleOnSubmit('username', 'password'); + + // Assert + expect(loginSpy).toHaveBeenCalledTimes(1); + }); - expect(props.actions).toBeDefined(); + it('should redirect to home page when user is authenticated', () => { + // Arrange + const pushSpy = jest.fn(); + const wrapper = setup({ + history: { + push: pushSpy, + } }); - it('should return the binded actions', () => { - const dispatch = () => {}; - const expectedActions = [ - 'loginRequest', - 'loginSuccess', - 'loginFailed', - 'login', - ]; - - const props = mapDispatchToProps(dispatch); + // Act + wrapper.instance().authenticate(true); - expect(Object.keys(props.actions)).toEqual(expectedActions); - }); + // Assert + expect(pushSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/app/reducers/rootReducer.js b/app/reducers/rootReducer.js deleted file mode 100644 index d6bc577..0000000 --- a/app/reducers/rootReducer.js +++ /dev/null @@ -1,12 +0,0 @@ -import { combineReducers } from 'redux'; -import { routerReducer as routing } from 'react-router-redux'; -import auth from './authReducer'; -import users from './usersReducer'; - -const rootReducer = combineReducers({ - auth, - users, - routing, -}); - -export default rootReducer; diff --git a/app/reducers/rootReducer.test.js b/app/reducers/rootReducer.test.js deleted file mode 100644 index 5278d10..0000000 --- a/app/reducers/rootReducer.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createStore } from 'redux'; -import rootReducer from './rootReducer'; -import initialState from './initialState'; - -describe('rootReducer', () => { - it('should set the inital state', () => { - const expectedState = { - ...initialState, - routing: { - location: null, - }, - }; - - const store = createStore(rootReducer); - - expect(store.getState()).toEqual(expectedState); - }); -}); diff --git a/app/services/authService.js b/app/services/authService.js index f84dc47..5d2f2cb 100644 --- a/app/services/authService.js +++ b/app/services/authService.js @@ -2,14 +2,20 @@ export default function login({ username = '', password = '' }) { if (username === 'username' && password === 'password') { const user = { name: 'John', + email: 'john@gmail.com', + phone: '1234', + skypeId: 'johndoe', }; + return new Promise(((resolve) => { setTimeout(resolve, Math.random() * 200, user); })); } - const message = 'Invalid credentials.'; return new Promise(((resolve, reject) => { - setTimeout(reject, Math.random() * 200, message); + setTimeout(reject, Math.random() * 200, { + code: 400, + message: 'Invalid credentials.', + }); })); } diff --git a/app/stores/AuthStore.js b/app/stores/AuthStore.js new file mode 100644 index 0000000..158a911 --- /dev/null +++ b/app/stores/AuthStore.js @@ -0,0 +1,53 @@ +import alt from '../alt'; +import AuthActions from '../actions/authActions'; + +export class AuthStore { + constructor() { + // handle store listeners + this.bindListeners({ + onLoginBegin: AuthActions.LOGIN_BEGIN, + onLoginSuccess: AuthActions.LOGIN_SUCCESS, + onError: AuthActions.ON_ERROR, + }); + + this.auth = { + authenticating: false, + authenticated: false, + error: { + code: null, + message: null, + }, + user: null, + }; + } + + onLoginBegin() { + this.auth = { + ...this.auth, + authenticating: true, + }; + } + + onLoginSuccess(data) { + this.auth = { + ...this.auth, + authenticating: false, + authenticated: true, + user: { + name: data.name, + email: data.email, + phone: data.phone, + skypeId: data.skypeId, + }, + }; + } + + onError(error) { + this.auth = { + ...this.auth, + error, + }; + } +} + +export default alt.createStore(AuthStore); diff --git a/app/stores/AuthStore.test.js b/app/stores/AuthStore.test.js new file mode 100644 index 0000000..748e74f --- /dev/null +++ b/app/stores/AuthStore.test.js @@ -0,0 +1,67 @@ +import alt from '../alt'; +import AuthStore from './AuthStore'; +import AuthActions from '../actions/authActions'; + +describe('AuthStore', () => { + + it('get initial state from auth store', () => { + const authState = AuthStore.getState().auth; + + expect(authState).toEqual({ + authenticating: false, + authenticated: false, + error: { + code: null, + message: null, + }, + user: null, + }); + }); + + it('should update store on login begin action', () => { + alt.dispatcher.dispatch({ + action: AuthActions.LOGIN_BEGIN + }); + + expect(AuthStore.getState().auth.authenticating).toEqual(true); + }); + + it('should update store on login success action', () => { + alt.dispatcher.dispatch({ + action: AuthActions.LOGIN_SUCCESS, + data: { + name: 'John', + email: 'john@gmail.com', + phone: '1234', + skypeId: 'johndoe', + } + }); + + expect(AuthStore.getState().auth).toMatchObject({ + authenticating: false, + authenticated: true, + user: { + name: 'John', + email: 'john@gmail.com', + phone: '1234', + skypeId: 'johndoe', + }, + }); + }); + + it('should update store on error', () => { + alt.dispatcher.dispatch({ + action: AuthActions.ON_ERROR, + data: { + code: 400, + message: 'Invalid credentials', + } + }); + + expect(AuthStore.getState().auth.error).toEqual({ + code: 400, + message: 'Invalid credentials' + }); + }); +}); + diff --git a/app/stores/UsersStore.js b/app/stores/UsersStore.js index a750143..54c094e 100644 --- a/app/stores/UsersStore.js +++ b/app/stores/UsersStore.js @@ -2,6 +2,7 @@ import alt from '../alt'; import { completeAssign } from '../utils/functions'; import UserActions from '../actions/usersActions'; import getUserId from '../utils/user'; +import errorService from '../utils/errorService'; class UserStore { constructor() {