Skip to content

Commit

Permalink
feat(logout): implement logout functionality
Browse files Browse the repository at this point in the history
- add logout component to Nav
- implement logic to log user out and cleanup cart using redux
- modify login action to get cart items upon authentication
- adequatly test all new functionality

[Finishes #163503750]
  • Loading branch information
akhilome committed Jan 27, 2019
1 parent 25d096b commit 85ad8b8
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 11 deletions.
21 changes: 15 additions & 6 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toast } from 'react-toastify';

import axios from '../services/axios';
import types from './types';
import { saveToken, getToken } from '../utils/localStorage';
import { saveToken, getToken, removeToken } from '../utils/localStorage';
import jwt from '../utils/jwt';

export const startFetching = () => ({ type: types.START_FETCHING });
Expand All @@ -15,6 +15,11 @@ export const stopFetching = (fetchSuccess = true, message = '') => ({
},
});

export const getCart = () => {
const cart = JSON.parse(localStorage.getItem('cart')) || {};
return { type: types.GET_CART, payload: cart };
};

export const signUpUser = userData => async (dispatch) => {
dispatch(startFetching());
try {
Expand Down Expand Up @@ -50,6 +55,7 @@ export const logInUser = userData => async (dispatch) => {
role,
},
});
dispatch(getCart());
return dispatch(stopFetching());
} catch (error) {
toast.error(error.response ? error.response.data.message : 'something went wrong');
Expand Down Expand Up @@ -100,18 +106,15 @@ export const addToCart = ({ foodId, foodName, foodPrice }) => {
return { type: types.ADD_TO_CART_FAIL };
};

export const getCart = () => {
const cart = JSON.parse(localStorage.getItem('cart')) || {};
return { type: types.GET_CART, payload: cart };
};

export const removeFromCart = (foodId) => {
const cart = JSON.parse(localStorage.getItem('cart')) || {};
const updatedCart = omit(cart, foodId);
localStorage.setItem('cart', JSON.stringify(updatedCart));
return { type: types.REMOVE_FROM_CART, payload: { foodId } };
};

export const emptyCart = () => ({ type: types.EMPTY_CART });

export const checkout = foodIds => async (dispatch) => {
dispatch(startFetching());
try {
Expand All @@ -127,3 +130,9 @@ export const checkout = foodIds => async (dispatch) => {
);
}
};

export const logout = () => (dispatch) => {
dispatch(emptyCart());
removeToken();
return dispatch({ type: types.LOGOUT });
};
2 changes: 2 additions & 0 deletions src/actions/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export default {
REMOVE_FROM_CART: 'REMOVE_FROM_CART',
CHECKOUT: 'CHECKOUT',
CHECKOUT_FAIL: 'CHECKOUT_FAIL',
LOGOUT: 'LOGOUT',
EMPTY_CART: 'EMPTY_CART',
};
4 changes: 2 additions & 2 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Menu from './MenuPage';
import CartPage from './Cart';
import NotFoundPage from './NotFoundPage';
import Loader from './Loader';
import Nav from './Nav';
import NavBar from './Nav';

export class App extends Component {
async componentDidMount() {
Expand All @@ -27,7 +27,7 @@ export class App extends Component {
<Router>
<Fragment>
{loading && <Loader />}
<Nav />
<NavBar />
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/login" component={LoginPage} />
Expand Down
32 changes: 32 additions & 0 deletions src/components/Logout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { logout } from '../actions';

export class Logout extends Component {
onLogoutClick = async (e) => {
e.preventDefault();
const { logout: logoutUser, history } = this.props;
await logoutUser();
return history.push('/login');
};

render() {
return (
<a onClick={this.onLogoutClick} href="!#">
logout
</a>
);
}
}

Logout.propTypes = {
logout: PropTypes.func.isRequired,
history: PropTypes.instanceOf(Object).isRequired,
};

export default connect(
null,
{ logout },
)(withRouter(Logout));
5 changes: 3 additions & 2 deletions src/components/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import LogoutLink from './Logout';

const Nav = ({ isLoggedIn, role }) => {
export const Nav = ({ isLoggedIn, role }) => {
const navContent = () => {
if (isLoggedIn && role === 'customer') {
return (
Expand All @@ -15,7 +16,7 @@ const Nav = ({ isLoggedIn, role }) => {
<Link to="/cart">Cart</Link>
</li>
<li>
<Link to="/logout">Logout</Link>
<LogoutLink />
</li>
</Fragment>
);
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/authReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export default (state = initialState, action) => {
name: action.payload.name,
role: action.payload.role,
};
case types.LOGOUT:
return initialState;
case types.CHECK_AUTH_STATUS_FAIL:
default:
return state;
Expand Down
1 change: 1 addition & 0 deletions src/reducers/cartReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default (state = {}, action) => {
case types.REMOVE_FROM_CART:
return omit(state, action.payload.foodId);
case types.CHECKOUT:
case types.EMPTY_CART:
return {};
case types.ADD_TO_CART_FAIL:
case types.CHECKOUT_FAIL:
Expand Down
2 changes: 1 addition & 1 deletion src/tests/actions/logInUser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('logInUser()', () => {
it('should login user successfully', async () => {
jest.spyOn(axios, 'post').mockImplementation(() => Promise.resolve(response));
await logInUser()(dispatch);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(4);
expect(dispatch.mock.calls[1][0]).toEqual({
payload: { name: 'jamjum', role: 'customer' },
type: 'LOG_IN',
Expand Down
10 changes: 10 additions & 0 deletions src/tests/actions/logout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { logout } from '../../actions';

describe('logout()', () => {
const dispatch = jest.fn();
it('should successfully log out user', () => {
logout()(dispatch);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenLastCalledWith({ type: 'LOGOUT' });
});
});
17 changes: 17 additions & 0 deletions src/tests/components/Logout.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { Logout } from '../../components/Logout';

describe('<Logout />', () => {
const props = {
logout: jest.fn(),
history: { push: jest.fn() },
};

it('should successfully logout the user', () => {
const wrapper = mount(<Logout {...props} />);
wrapper.find('a').simulate('click');
expect(props.logout).toHaveBeenCalled();
wrapper.unmount();
});
});
45 changes: 45 additions & 0 deletions src/tests/components/Nav.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { mount } from 'enzyme';
import { Nav } from '../../components/Nav';
import { routerWrap, reduxWrap } from '../helpers';

describe('<Nav />', () => {
const adminProps = {
role: 'admin',
isLoggedIn: true,
};

const customerProps = {
role: 'customer',
isLoggedIn: true,
};

const noAuthProps = {
role: 'admin',
isLoggedIn: false,
};

it('should render correctly for admin', () => {
const wrapper = mount(routerWrap(<Nav {...adminProps} />));
const links = wrapper.find('li');
expect(links.length).toEqual(2);
wrapper.unmount();
});

it('should render correctly for customer', () => {
const connectedComponent = reduxWrap(<Nav {...customerProps} />);
const wrapper = mount(routerWrap(connectedComponent));
const links = wrapper.find('li');
expect(links.length).toEqual(3);
wrapper.unmount();
});

it('should render correctly for unauthenticated user', () => {
const wrapper = mount(routerWrap(<Nav {...noAuthProps} />));
const links = wrapper.find('li a');
expect(links.length).toEqual(2);
expect(links.first().text()).toEqual('Log In');
expect(links.last().text()).toEqual('Sign Up');
wrapper.unmount();
});
});
13 changes: 13 additions & 0 deletions src/tests/reducers/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ const initialState = {
role: '',
};

const loggedInState = {
isLoggedIn: true,
name: 'Mike',
role: 'customer',
};

const signupAction = {
type: 'SIGN_UP',
payload: {
Expand All @@ -30,6 +36,8 @@ const checkAuthStatusSuccessAction = {
},
};

const logoutAction = { type: 'LOGOUT' };

const checkAuthStatusFailureAction = { type: 'CHECK_AUTH_STATUS_FAIL' };

describe('auth reducer', () => {
Expand All @@ -55,4 +63,9 @@ describe('auth reducer', () => {
const state = authReducer(initialState, checkAuthStatusFailureAction);
expect({ ...state }).toEqual({ ...initialState });
});

it('should return correct state for user logout', () => {
const state = authReducer(loggedInState, logoutAction);
expect({ ...state }).toEqual({ ...initialState });
});
});
1 change: 1 addition & 0 deletions src/utils/localStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const saveToken = (token) => {
};

export const getToken = () => window.localStorage.getItem('kiakiafoodToken') || '';
export const removeToken = () => window.localStorage.removeItem('kiakiafoodToken');

0 comments on commit 85ad8b8

Please sign in to comment.