Commit b0d86886 authored by Gijs Hendriksen's avatar Gijs Hendriksen
Browse files

Merge branch 'tc/introduce-session-saga' into 'master'

Introduce session saga

See merge request !185
parents 8747421c 34de57b1
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`login actions should create an action for a successful login 1`] = `
exports[`session actions should create an action for a successful login 1`] = `
Object {
"payload": Object {
"token": "token",
"username": "username",
},
"type": "LOGIN_SUCCESS",
"type": "SESSION_SUCCESS",
}
`;
exports[`login actions should create an action for a successful user profile load 1`] = `
exports[`session actions should create an action for a successful user profile load 1`] = `
Object {
"payload": Object {
"displayName": "displayName",
"photo": "photo",
},
"type": "LOGIN_PROFILE_SUCCESS",
"type": "SESSION_PROFILE_SUCCESS",
}
`;
exports[`login actions should create an action to load the user profile 1`] = `
exports[`session actions should create an action to init the session 1`] = `
Object {
"type": "SESSION_INIT",
}
`;
exports[`session actions should create an action to load the user profile 1`] = `
Object {
"payload": Object {
"token": "token",
},
"type": "LOGIN_PROFILE",
"type": "SESSION_PROFILE",
}
`;
exports[`login actions should create an action to log the user in 1`] = `
exports[`session actions should create an action to log the user in 1`] = `
Object {
"payload": Object {
"pass": "password",
"user": "username",
},
"type": "LOGIN_LOGIN",
"type": "SESSION_LOGIN",
}
`;
exports[`session actions should create an action to log the user out 1`] = `
Object {
"type": "SESSION_LOGOUT",
}
`;
exports[`login actions should create an action to log the user out 1`] = `
exports[`session actions should create an action to notify invalid token 1`] = `
Object {
"type": "LOGIN_LOGOUT",
"type": "SESSION_TOKEN_INVALID",
}
`;
import * as actions from '../../app/actions/login';
describe('login actions', () => {
it('should expose the login actions', () => {
expect(actions.SUCCESS).toEqual('LOGIN_SUCCESS');
expect(actions.LOGIN).toEqual('LOGIN_LOGIN');
expect(actions.LOGOUT).toEqual('LOGIN_LOGOUT');
expect(actions.PROFILE).toEqual('LOGIN_PROFILE');
expect(actions.PROFILE_SUCCESS).toEqual('LOGIN_PROFILE_SUCCESS');
import * as actions from '../../app/actions/session';
describe('session actions', () => {
it('should expose the session actions', () => {
expect(actions.INIT).toEqual('SESSION_INIT');
expect(actions.SUCCESS).toEqual('SESSION_SUCCESS');
expect(actions.LOGIN).toEqual('SESSION_LOGIN');
expect(actions.TOKEN_INVALID).toEqual('SESSION_TOKEN_INVALID');
expect(actions.LOGOUT).toEqual('SESSION_LOGOUT');
expect(actions.PROFILE).toEqual('SESSION_PROFILE');
expect(actions.PROFILE_SUCCESS).toEqual('SESSION_PROFILE_SUCCESS');
});
it('should create an action to init the session', () => {
expect(actions.init()).toMatchSnapshot();
});
it('should create an action to notify invalid token', () => {
expect(actions.tokenInvalid()).toMatchSnapshot();
});
it('should create an action to log the user in', () => {
......
......@@ -8,6 +8,7 @@ import calendarSaga from '../../app/sagas/calendar';
import * as calendarActions from '../../app/actions/calendar';
import * as navActions from '../../app/actions/navigation';
import { EVENT_LIST_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
......@@ -31,7 +32,7 @@ describe('calendar saga', () => {
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []],
])
.dispatch(navActions.navigate('eventList'))
.dispatch(navActions.navigate(EVENT_LIST_SCENE))
.put(calendarActions.fetching())
.silentRun());
......
......@@ -6,8 +6,9 @@ import * as deepLinkingActions from '../../app/actions/deepLinking';
import { url as siteURL, apiRequest, loggedInSelector } from '../../app/utils/url';
import * as navigationActions from '../../app/actions/navigation';
import * as eventActions from '../../app/actions/event';
import * as loginActions from '../../app/actions/login';
import * as loginActions from '../../app/actions/session';
import * as pizzaActions from '../../app/actions/pizza';
import { EVENT_LIST_SCENE } from '../../app/ui/components/navigator/scenes';
describe('calendar saga', () => {
it('should parse a URL correctly', () => {
......@@ -40,7 +41,7 @@ describe('calendar saga', () => {
[matchers.call.fn(apiRequest), []],
])
.dispatch(deepLinkingActions.deepLink(`${siteURL}/events/`))
.put(navigationActions.navigate('eventList'))
.put(navigationActions.navigate(EVENT_LIST_SCENE))
.silentRun());
it('shouldl load event on /events/{id} deeplink', () => expectSaga(deepLinkingSaga)
......
......@@ -8,6 +8,7 @@ import eventSaga from '../../app/sagas/event';
import * as eventActions from '../../app/actions/event';
import * as navActions from '../../app/actions/navigation';
import { EVENT_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
......@@ -18,13 +19,13 @@ describe('event saga', () => {
const error = new Error('error');
it('should start fetching', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []],
])
.dispatch(eventActions.event(1))
.put(eventActions.fetching())
.silentRun());
.provide([
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []],
])
.dispatch(eventActions.event(1))
.put(eventActions.fetching())
.silentRun());
it('should navigate to the event scene', () => expectSaga(eventSaga)
.provide([
......@@ -32,7 +33,7 @@ describe('event saga', () => {
[matchers.call.fn(apiRequest), []],
])
.dispatch(eventActions.event(1))
.put(navActions.navigate('event'))
.put(navActions.navigate(EVENT_SCENE))
.silentRun());
it('should put an error when the api request fails', () => expectSaga(eventSaga)
......
......@@ -6,6 +6,7 @@ import { apiRequest, tokenSelector } from '../../app/utils/url';
import pizzaSaga from '../../app/sagas/pizza';
import * as pizzaActions from '../../app/actions/pizza';
import * as navigationActions from '../../app/actions/navigation';
import { PIZZA_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
......@@ -28,7 +29,7 @@ describe('pizza saga', () => {
])
.dispatch(pizzaActions.retrievePizzaInfo())
.put(pizzaActions.fetching())
.put(navigationActions.navigate('pizza'))
.put(navigationActions.navigate(PIZZA_SCENE))
.silentRun());
describe('failures', () => {
......
......@@ -6,6 +6,7 @@ import profileSaga from '../../app/sagas/profile';
import { apiRequest, tokenSelector } from '../../app/utils/url';
import * as profileActions from '../../app/actions/profile';
import * as navActions from '../../app/actions/navigation';
import { PROFILE_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
......@@ -22,7 +23,7 @@ describe('profile saga', () => {
])
.dispatch(profileActions.profile('token', 1))
.put(profileActions.fetching())
.put(navActions.navigate('profile'))
.put(navActions.navigate(PROFILE_SCENE))
.silentRun());
it('should put success when the request succeeds', () => expectSaga(profileSaga)
......
......@@ -8,6 +8,7 @@ import registrationSaga, { eventSelector } from '../../app/sagas/registration';
import { apiRequest, tokenSelector } from '../../app/utils/url';
import * as eventActions from '../../app/actions/event';
import * as navigationActions from '../../app/actions/navigation';
import { REGISTRATION_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('react-native-snackbar', () => ({
LENGTH_LONG: 100,
......@@ -61,19 +62,19 @@ describe('registration saga', () => {
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Registration successful!' });
{ title: 'Registration successful!' },
);
}));
it('should put a retrieve fields action when they are available', () =>
expectSaga(registrationSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }),
{ fields: {}, pk: 2 }],
])
.dispatch(registrationActions.register(1))
.put(registrationActions.retrieveFields(2))
.silentRun());
it('should put a retrieve fields action when they are available', () => expectSaga(registrationSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }),
{ fields: {}, pk: 2 }],
])
.dispatch(registrationActions.register(1))
.put(registrationActions.retrieveFields(2))
.silentRun());
it('should show a failure action when the request fails', () => expectSaga(registrationSaga)
.provide([
......@@ -139,7 +140,8 @@ describe('registration saga', () => {
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Successfully updated registration' });
{ title: 'Successfully updated registration' },
);
}));
it('should put failure action when the request fails', () => expectSaga(registrationSaga)
......@@ -152,22 +154,22 @@ describe('registration saga', () => {
.silentRun());
it('should do a PUT request with fields', () => expectSaga(registrationSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(registrationActions.update(2, { key: 'value' }))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('registrations/2', {
body: '{"fields[key]":"value"}',
headers: {
Accept: 'application/json',
Authorization: 'Token token',
'Content-Type': 'application/json',
},
method: 'PUT',
});
}));
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(registrationActions.update(2, { key: 'value' }))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('registrations/2', {
body: '{"fields[key]":"value"}',
headers: {
Accept: 'application/json',
Authorization: 'Token token',
'Content-Type': 'application/json',
},
method: 'PUT',
});
}));
});
describe('cancelling', () => {
......@@ -191,7 +193,8 @@ describe('registration saga', () => {
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Successfully cancelled registration' });
{ title: 'Successfully cancelled registration' },
);
}));
it('should put event action when the request succeeds', () => expectSaga(registrationSaga)
......@@ -252,7 +255,7 @@ describe('registration saga', () => {
it('should navigate to the registration screen', () => expectSaga(registrationSaga)
.dispatch(registrationActions.retrieveFields(1))
.put(navigationActions.navigate('registration'))
.put(navigationActions.navigate(REGISTRATION_SCENE))
.silentRun());
it('should put showFields action when the request succeeds', () => expectSaga(registrationSaga)
......
......@@ -5,9 +5,11 @@ import Snackbar from 'react-native-snackbar';
import { AsyncStorage } from 'react-native';
import { Sentry } from 'react-native-sentry';
import loginSaga, { DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY } from '../../app/sagas/login';
import sessionSaga, {
DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY
} from '../../app/sagas/session';
import { apiRequest } from '../../app/utils/url';
import * as loginActions from '../../app/actions/login';
import * as sessionActions from '../../app/actions/session';
import * as pushNotificationsActions from '../../app/actions/pushNotifications';
jest.mock('react-native-snackbar', () => ({
......@@ -35,48 +37,49 @@ jest.mock('react-native-sentry', () => ({
},
}));
describe('login saga', () => {
describe('session saga', () => {
const error = new Error('error');
describe('logging in', () => {
it('should show a snackbar on start', () => expectSaga(loginSaga)
.dispatch(loginActions.login('username', 'password'))
it('should show a snackbar on start', () => expectSaga(sessionSaga)
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Logging in', duration: Snackbar.LENGTH_INDEFINITE });
{ title: 'Logging in', duration: Snackbar.LENGTH_INDEFINITE },
);
}));
it('should put the result data when the request succeeds', () => expectSaga(loginSaga)
it('should put the result data when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.put(loginActions.success('username', 'abc123'))
.put(loginActions.profile('abc123'))
.dispatch(loginActions.login('username', 'password'))
.put(sessionActions.success('username', 'abc123'))
.put(sessionActions.profile('abc123'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun());
it('should show a snackbar when the request succeeds', () => expectSaga(loginSaga)
it('should show a snackbar when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith(
{ title: 'Login successful' });
{ title: 'Login successful' },
);
}));
it('should save the token in the AsyncStorage when the request succeeds', () =>
expectSaga(loginSaga)
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([
......@@ -85,20 +88,21 @@ describe('login saga', () => {
]);
}));
it('should show a snackbar when the request fails', () => expectSaga(loginSaga)
it('should show a snackbar when the request fails', () => expectSaga(sessionSaga)
.provide([
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith(
{ title: 'Login failed' });
{ title: 'Login failed' },
);
}));
it('should do a POST request', () => expectSaga(loginSaga)
.dispatch(loginActions.login('username', 'password'))
it('should do a POST request', () => expectSaga(sessionSaga)
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('token-auth', {
......@@ -113,29 +117,30 @@ describe('login saga', () => {
});
describe('logging out', () => {
it('should remove the token from the AsyncStorage', () => expectSaga(loginSaga)
.dispatch(loginActions.logout())
it('should remove the token from the AsyncStorage', () => expectSaga(sessionSaga)
.dispatch(sessionActions.logout())
.silentRun()
.then(() => {
expect(AsyncStorage.multiRemove).toBeCalledWith([USERNAMEKEY, TOKENKEY]);
}));
it('should put a push notification invalidation action', () => expectSaga(loginSaga)
it('should put a push notification invalidation action', () => expectSaga(sessionSaga)
.put(pushNotificationsActions.invalidate())
.dispatch(loginActions.logout())
.dispatch(sessionActions.logout())
.silentRun());
it('should remove the token from the AsyncStorage', () => expectSaga(loginSaga)
.dispatch(loginActions.logout())
it('should remove the token from the AsyncStorage', () => expectSaga(sessionSaga)
.dispatch(sessionActions.logout())
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Logout successful' });
{ title: 'Logout successful' },
);
}));
});
describe('getting profile', () => {
it('should put the result data when the request succeeds', () => expectSaga(loginSaga)
it('should put the result data when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
display_name: 'Johnny Test',
......@@ -144,11 +149,11 @@ describe('login saga', () => {
},
}],
])
.put(loginActions.profileSuccess('Johnny Test', 'http://example.org/photo.png'))
.dispatch(loginActions.profile('abc123'))
.put(sessionActions.profileSuccess('Johnny Test', 'http://example.org/photo.png'))
.dispatch(sessionActions.profile('abc123'))
.silentRun());
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(loginSaga)
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
display_name: 'Johnny Test',
......@@ -157,7 +162,7 @@ describe('login saga', () => {
},
}],
])
.dispatch(loginActions.profile('abc123'))
.dispatch(sessionActions.profile('abc123'))
.silentRun()
.then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([
......@@ -166,15 +171,15 @@ describe('login saga', () => {
]);
}));
it('should not care about errors', () => expectSaga(loginSaga)
it('should not care about errors', () => expectSaga(sessionSaga)
.provide([
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(loginActions.profile('token'))
.dispatch(sessionActions.profile('token'))
.silentRun());
it('should do a GET request', () => expectSaga(loginSaga)
.dispatch(loginActions.profile('abc123'))
it('should do a GET request', () => expectSaga(sessionSaga)
.dispatch(sessionActions.profile('abc123'))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('members/me', {
......
......@@ -4,12 +4,13 @@ import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer';
import ReduxNavigator from '../../../../app/ui/components/navigator/ReduxNavigator';
import reducer from '../../../../app/reducers';
import { LOGIN_SCENE } from '../../../../app/ui/components/navigator/scenes';
describe('ReduxNavigator component', () => {
const mockStore = configureStore(reducer);
const initialState = {
navigation: {
currentScene: 'home',
currentScene: LOGIN_SCENE,
previousScenes: [],
drawerOpen: false,
},
......@@ -18,8 +19,12 @@ describe('ReduxNavigator component', () => {
it('renders correctly', () => {
const tree = renderer
.create(<Provider store={store}><ReduxNavigator /></Provider>)
.create(
<Provider store={store}>
<ReduxNavigator />
</Provider>,
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
\ No newline at end of file
});
export const LOGIN = 'LOGIN_LOGIN';
export const SUCCESS = 'LOGIN_SUCCESS';
export const LOGOUT = 'LOGIN_LOGOUT';
export const TOKEN_INVALID = 'LOGIN_TOKEN_INVALID';
export const PROFILE = 'LOGIN_PROFILE';
export const PROFILE_SUCCESS = 'LOGIN_PROFILE_SUCCESS';
export const LOGIN = 'SESSION_LOGIN';
export const INIT = 'SESSION_INIT';
export const SUCCESS = 'SESSION_SUCCESS';
export const LOGOUT = 'SESSION_LOGOUT';
export const TOKEN_INVALID = 'SESSION_TOKEN_INVALID';
export const PROFILE = 'SESSION_PROFILE';
export const PROFILE_SUCCESS = 'SESSION_PROFILE_SUCCESS';
export function success(username, token) {
return { type: SUCCESS, payload: { username, token } };
......@@ -13,6 +14,10 @@ export function login(user, pass) {
return { type: LOGIN, payload: { user, pass } };
}
export function init() {
return { type: INIT };
}
export function logout() {
return { type: LOGOUT };
}
......
import React, { Component } from 'react';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { Linking, Platform } from 'react-native';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
......@@ -14,7 +14,7 @@ import reducers from './reducers';
import i18n from './utils/i18n';
import sagas from './sagas';
import ReduxNavigator from './ui/components/navigator/ReduxNavigator';
import * as loginActions from './actions/login';
import * as sessionActions from './actions/session';
import * as deepLinkingActions from './actions/deepLinking';
import { register } from './actions/pushNotifications';
......@@ -22,18 +22,6 @@ const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(sagas);
const USERNAMEKEY = '@MyStore:username';
const TOKENKEY = '@MyStore:token';
const DISPLAYNAMEKEY = '@MyStore:displayName';
const PHOTOKEY = '@MyStore:photo';
const PUSHCATEGORYKEY = '@MyStore:pushCategories';
const pairsToObject = (obj, pair) => {
const obj2 = { ...obj };
obj2[pair[0]] = pair[1];
return obj2;
};