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 // 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 { Object {
"payload": Object { "payload": Object {
"token": "token", "token": "token",
"username": "username", "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 { Object {
"payload": Object { "payload": Object {
"displayName": "displayName", "displayName": "displayName",
"photo": "photo", "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 { Object {
"payload": Object { "payload": Object {
"token": "token", "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 { Object {
"payload": Object { "payload": Object {
"pass": "password", "pass": "password",
"user": "username", "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 { Object {
"type": "LOGIN_LOGOUT", "type": "SESSION_TOKEN_INVALID",
} }
`; `;
import * as actions from '../../app/actions/login'; import * as actions from '../../app/actions/session';
describe('login actions', () => { describe('session actions', () => {
it('should expose the login actions', () => { it('should expose the session actions', () => {
expect(actions.SUCCESS).toEqual('LOGIN_SUCCESS'); expect(actions.INIT).toEqual('SESSION_INIT');
expect(actions.LOGIN).toEqual('LOGIN_LOGIN'); expect(actions.SUCCESS).toEqual('SESSION_SUCCESS');
expect(actions.LOGOUT).toEqual('LOGIN_LOGOUT'); expect(actions.LOGIN).toEqual('SESSION_LOGIN');
expect(actions.PROFILE).toEqual('LOGIN_PROFILE'); expect(actions.TOKEN_INVALID).toEqual('SESSION_TOKEN_INVALID');
expect(actions.PROFILE_SUCCESS).toEqual('LOGIN_PROFILE_SUCCESS'); 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', () => { it('should create an action to log the user in', () => {
......
...@@ -8,6 +8,7 @@ import calendarSaga from '../../app/sagas/calendar'; ...@@ -8,6 +8,7 @@ import calendarSaga from '../../app/sagas/calendar';
import * as calendarActions from '../../app/actions/calendar'; import * as calendarActions from '../../app/actions/calendar';
import * as navActions from '../../app/actions/navigation'; import * as navActions from '../../app/actions/navigation';
import { EVENT_LIST_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({ jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}), apiRequest: jest.fn(() => {}),
...@@ -31,7 +32,7 @@ describe('calendar saga', () => { ...@@ -31,7 +32,7 @@ describe('calendar saga', () => {
[select(tokenSelector), 'token'], [select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []], [matchers.call.fn(apiRequest), []],
]) ])
.dispatch(navActions.navigate('eventList')) .dispatch(navActions.navigate(EVENT_LIST_SCENE))
.put(calendarActions.fetching()) .put(calendarActions.fetching())
.silentRun()); .silentRun());
......
...@@ -6,8 +6,9 @@ import * as deepLinkingActions from '../../app/actions/deepLinking'; ...@@ -6,8 +6,9 @@ import * as deepLinkingActions from '../../app/actions/deepLinking';
import { url as siteURL, apiRequest, loggedInSelector } from '../../app/utils/url'; import { url as siteURL, apiRequest, loggedInSelector } from '../../app/utils/url';
import * as navigationActions from '../../app/actions/navigation'; import * as navigationActions from '../../app/actions/navigation';
import * as eventActions from '../../app/actions/event'; 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 * as pizzaActions from '../../app/actions/pizza';
import { EVENT_LIST_SCENE } from '../../app/ui/components/navigator/scenes';
describe('calendar saga', () => { describe('calendar saga', () => {
it('should parse a URL correctly', () => { it('should parse a URL correctly', () => {
...@@ -40,7 +41,7 @@ describe('calendar saga', () => { ...@@ -40,7 +41,7 @@ describe('calendar saga', () => {
[matchers.call.fn(apiRequest), []], [matchers.call.fn(apiRequest), []],
]) ])
.dispatch(deepLinkingActions.deepLink(`${siteURL}/events/`)) .dispatch(deepLinkingActions.deepLink(`${siteURL}/events/`))
.put(navigationActions.navigate('eventList')) .put(navigationActions.navigate(EVENT_LIST_SCENE))
.silentRun()); .silentRun());
it('shouldl load event on /events/{id} deeplink', () => expectSaga(deepLinkingSaga) it('shouldl load event on /events/{id} deeplink', () => expectSaga(deepLinkingSaga)
......
...@@ -8,6 +8,7 @@ import eventSaga from '../../app/sagas/event'; ...@@ -8,6 +8,7 @@ import eventSaga from '../../app/sagas/event';
import * as eventActions from '../../app/actions/event'; import * as eventActions from '../../app/actions/event';
import * as navActions from '../../app/actions/navigation'; import * as navActions from '../../app/actions/navigation';
import { EVENT_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({ jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}), apiRequest: jest.fn(() => {}),
...@@ -18,13 +19,13 @@ describe('event saga', () => { ...@@ -18,13 +19,13 @@ describe('event saga', () => {
const error = new Error('error'); const error = new Error('error');
it('should start fetching', () => expectSaga(eventSaga) it('should start fetching', () => expectSaga(eventSaga)
.provide([ .provide([
[select(tokenSelector), 'token'], [select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []], [matchers.call.fn(apiRequest), []],
]) ])
.dispatch(eventActions.event(1)) .dispatch(eventActions.event(1))
.put(eventActions.fetching()) .put(eventActions.fetching())
.silentRun()); .silentRun());
it('should navigate to the event scene', () => expectSaga(eventSaga) it('should navigate to the event scene', () => expectSaga(eventSaga)
.provide([ .provide([
...@@ -32,7 +33,7 @@ describe('event saga', () => { ...@@ -32,7 +33,7 @@ describe('event saga', () => {
[matchers.call.fn(apiRequest), []], [matchers.call.fn(apiRequest), []],
]) ])
.dispatch(eventActions.event(1)) .dispatch(eventActions.event(1))
.put(navActions.navigate('event')) .put(navActions.navigate(EVENT_SCENE))
.silentRun()); .silentRun());
it('should put an error when the api request fails', () => expectSaga(eventSaga) it('should put an error when the api request fails', () => expectSaga(eventSaga)
......
...@@ -6,6 +6,7 @@ import { apiRequest, tokenSelector } from '../../app/utils/url'; ...@@ -6,6 +6,7 @@ import { apiRequest, tokenSelector } from '../../app/utils/url';
import pizzaSaga from '../../app/sagas/pizza'; import pizzaSaga from '../../app/sagas/pizza';
import * as pizzaActions from '../../app/actions/pizza'; import * as pizzaActions from '../../app/actions/pizza';
import * as navigationActions from '../../app/actions/navigation'; import * as navigationActions from '../../app/actions/navigation';
import { PIZZA_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({ jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}), apiRequest: jest.fn(() => {}),
...@@ -28,7 +29,7 @@ describe('pizza saga', () => { ...@@ -28,7 +29,7 @@ describe('pizza saga', () => {
]) ])
.dispatch(pizzaActions.retrievePizzaInfo()) .dispatch(pizzaActions.retrievePizzaInfo())
.put(pizzaActions.fetching()) .put(pizzaActions.fetching())
.put(navigationActions.navigate('pizza')) .put(navigationActions.navigate(PIZZA_SCENE))
.silentRun()); .silentRun());
describe('failures', () => { describe('failures', () => {
......
...@@ -6,6 +6,7 @@ import profileSaga from '../../app/sagas/profile'; ...@@ -6,6 +6,7 @@ import profileSaga from '../../app/sagas/profile';
import { apiRequest, tokenSelector } from '../../app/utils/url'; import { apiRequest, tokenSelector } from '../../app/utils/url';
import * as profileActions from '../../app/actions/profile'; import * as profileActions from '../../app/actions/profile';
import * as navActions from '../../app/actions/navigation'; import * as navActions from '../../app/actions/navigation';
import { PROFILE_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('../../app/utils/url', () => ({ jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}), apiRequest: jest.fn(() => {}),
...@@ -22,7 +23,7 @@ describe('profile saga', () => { ...@@ -22,7 +23,7 @@ describe('profile saga', () => {
]) ])
.dispatch(profileActions.profile('token', 1)) .dispatch(profileActions.profile('token', 1))
.put(profileActions.fetching()) .put(profileActions.fetching())
.put(navActions.navigate('profile')) .put(navActions.navigate(PROFILE_SCENE))
.silentRun()); .silentRun());
it('should put success when the request succeeds', () => expectSaga(profileSaga) it('should put success when the request succeeds', () => expectSaga(profileSaga)
......
...@@ -8,6 +8,7 @@ import registrationSaga, { eventSelector } from '../../app/sagas/registration'; ...@@ -8,6 +8,7 @@ import registrationSaga, { eventSelector } from '../../app/sagas/registration';
import { apiRequest, tokenSelector } from '../../app/utils/url'; import { apiRequest, tokenSelector } from '../../app/utils/url';
import * as eventActions from '../../app/actions/event'; import * as eventActions from '../../app/actions/event';
import * as navigationActions from '../../app/actions/navigation'; import * as navigationActions from '../../app/actions/navigation';
import { REGISTRATION_SCENE } from '../../app/ui/components/navigator/scenes';
jest.mock('react-native-snackbar', () => ({ jest.mock('react-native-snackbar', () => ({
LENGTH_LONG: 100, LENGTH_LONG: 100,
...@@ -61,19 +62,19 @@ describe('registration saga', () => { ...@@ -61,19 +62,19 @@ describe('registration saga', () => {
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.show).toBeCalledWith( expect(Snackbar.show).toBeCalledWith(
{ title: 'Registration successful!' }); { title: 'Registration successful!' },
);
})); }));
it('should put a retrieve fields action when they are available', () => it('should put a retrieve fields action when they are available', () => expectSaga(registrationSaga)
expectSaga(registrationSaga) .provide([
.provide([ [select(tokenSelector), 'token'],
[select(tokenSelector), 'token'], [matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }),
[matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }), { fields: {}, pk: 2 }],
{ fields: {}, pk: 2 }], ])
]) .dispatch(registrationActions.register(1))
.dispatch(registrationActions.register(1)) .put(registrationActions.retrieveFields(2))
.put(registrationActions.retrieveFields(2)) .silentRun());
.silentRun());
it('should show a failure action when the request fails', () => expectSaga(registrationSaga) it('should show a failure action when the request fails', () => expectSaga(registrationSaga)
.provide([ .provide([
...@@ -139,7 +140,8 @@ describe('registration saga', () => { ...@@ -139,7 +140,8 @@ describe('registration saga', () => {
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.show).toBeCalledWith( expect(Snackbar.show).toBeCalledWith(
{ title: 'Successfully updated registration' }); { title: 'Successfully updated registration' },
);
})); }));
it('should put failure action when the request fails', () => expectSaga(registrationSaga) it('should put failure action when the request fails', () => expectSaga(registrationSaga)
...@@ -152,22 +154,22 @@ describe('registration saga', () => { ...@@ -152,22 +154,22 @@ describe('registration saga', () => {
.silentRun()); .silentRun());
it('should do a PUT request with fields', () => expectSaga(registrationSaga) it('should do a PUT request with fields', () => expectSaga(registrationSaga)
.provide([ .provide([
[select(tokenSelector), 'token'], [select(tokenSelector), 'token'],
]) ])
.dispatch(registrationActions.update(2, { key: 'value' })) .dispatch(registrationActions.update(2, { key: 'value' }))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(apiRequest).toBeCalledWith('registrations/2', { expect(apiRequest).toBeCalledWith('registrations/2', {
body: '{"fields[key]":"value"}', body: '{"fields[key]":"value"}',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
Authorization: 'Token token', Authorization: 'Token token',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
method: 'PUT', method: 'PUT',
}); });
})); }));
}); });
describe('cancelling', () => { describe('cancelling', () => {
...@@ -191,7 +193,8 @@ describe('registration saga', () => { ...@@ -191,7 +193,8 @@ describe('registration saga', () => {
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.show).toBeCalledWith( expect(Snackbar.show).toBeCalledWith(
{ title: 'Successfully cancelled registration' }); { title: 'Successfully cancelled registration' },
);
})); }));
it('should put event action when the request succeeds', () => expectSaga(registrationSaga) it('should put event action when the request succeeds', () => expectSaga(registrationSaga)
...@@ -252,7 +255,7 @@ describe('registration saga', () => { ...@@ -252,7 +255,7 @@ describe('registration saga', () => {
it('should navigate to the registration screen', () => expectSaga(registrationSaga) it('should navigate to the registration screen', () => expectSaga(registrationSaga)
.dispatch(registrationActions.retrieveFields(1)) .dispatch(registrationActions.retrieveFields(1))
.put(navigationActions.navigate('registration')) .put(navigationActions.navigate(REGISTRATION_SCENE))
.silentRun()); .silentRun());
it('should put showFields action when the request succeeds', () => expectSaga(registrationSaga) it('should put showFields action when the request succeeds', () => expectSaga(registrationSaga)
......
...@@ -5,9 +5,11 @@ import Snackbar from 'react-native-snackbar'; ...@@ -5,9 +5,11 @@ import Snackbar from 'react-native-snackbar';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { Sentry } from 'react-native-sentry'; 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 { 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'; import * as pushNotificationsActions from '../../app/actions/pushNotifications';
jest.mock('react-native-snackbar', () => ({ jest.mock('react-native-snackbar', () => ({
...@@ -35,48 +37,49 @@ jest.mock('react-native-sentry', () => ({ ...@@ -35,48 +37,49 @@ jest.mock('react-native-sentry', () => ({
}, },
})); }));
describe('login saga', () => { describe('session saga', () => {
const error = new Error('error'); const error = new Error('error');
describe('logging in', () => { describe('logging in', () => {
it('should show a snackbar on start', () => expectSaga(loginSaga) it('should show a snackbar on start', () => expectSaga(sessionSaga)
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.show).toBeCalledWith( 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([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}], [matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.put(loginActions.success('username', 'abc123')) .put(sessionActions.success('username', 'abc123'))
.put(loginActions.profile('abc123')) .put(sessionActions.profile('abc123'))
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun()); .silentRun());
it('should show a snackbar when the request succeeds', () => expectSaga(loginSaga) it('should show a snackbar when the request succeeds', () => expectSaga(sessionSaga)
.provide([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}], [matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.dismiss).toBeCalled(); expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith( expect(Snackbar.show).toBeCalledWith(
{ title: 'Login successful' }); { title: 'Login successful' },
);
})); }));
it('should save the token in the AsyncStorage when the request succeeds', () => it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
expectSaga(loginSaga)
.provide([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}], [matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([ expect(AsyncStorage.multiSet).toBeCalledWith([
...@@ -85,20 +88,21 @@ describe('login saga', () => { ...@@ -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([ .provide([
[matchers.call.fn(apiRequest), throwError(error)], [matchers.call.fn(apiRequest), throwError(error)],
]) ])
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(Snackbar.dismiss).toBeCalled(); expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith( expect(Snackbar.show).toBeCalledWith(
{ title: 'Login failed' }); { title: 'Login failed' },
);
})); }));
it('should do a POST request', () => expectSaga(loginSaga) it('should do a POST request', () => expectSaga(sessionSaga)
.dispatch(loginActions.login('username', 'password')) .dispatch(sessionActions.login('username', 'password'))
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(apiRequest).toBeCalledWith('token-auth', { expect(apiRequest).toBeCalledWith('token-auth', {
...@@ -113,29 +117,30 @@ describe('login saga', () => { ...@@ -113,29 +117,30 @@ describe('login saga', () => {
}); });
describe('logging out', () => { describe('logging out', () => {
it('should remove the token from the AsyncStorage', () => expectSaga(loginSaga) it('should remove the token from the AsyncStorage', () => expectSaga(sessionSaga)
.dispatch(loginActions.logout()) .dispatch(sessionActions.logout())
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(AsyncStorage.multiRemove).toBeCalledWith([USERNAMEKEY, TOKENKEY]); 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())