Skip to content

Commit ebd8c76

Browse files
sumomomomomolinedoestrolling
authored andcommitted
Cleanup + Fix PersistenceSaga test
1 parent 1dd2ac5 commit ebd8c76

File tree

8 files changed

+94
-75
lines changed

8 files changed

+94
-75
lines changed

src/commons/application/actions/SessionActions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
REAUTOGRADE_ANSWER,
5959
REAUTOGRADE_SUBMISSION,
6060
REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN,
61+
REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN,
6162
SET_ADMIN_PANEL_COURSE_REGISTRATIONS,
6263
SET_ASSESSMENT_CONFIGURATIONS,
6364
SET_CONFIGURABLE_NOTIFICATION_CONFIGS,
@@ -224,6 +225,10 @@ export const removeGitHubOctokitObjectAndAccessToken = createAction(
224225
() => ({ payload: {} })
225226
);
226227

228+
export const removeGoogleUserAndAccessToken = createAction(
229+
REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN, () => ({ payload: {} })
230+
);
231+
227232
export const submitAnswer = createAction(
228233
SUBMIT_ANSWER,
229234
(id: number, answer: string | number | ContestEntry[]) => ({ payload: { id, answer } })

src/commons/application/reducers/SessionsReducer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { defaultSession } from '../ApplicationTypes';
99
import { LOG_OUT } from '../types/CommonsTypes';
1010
import {
1111
REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN,
12+
REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN,
1213
SessionState,
1314
SET_ADMIN_PANEL_COURSE_REGISTRATIONS,
1415
SET_ASSESSMENT_CONFIGURATIONS,
@@ -162,6 +163,12 @@ export const SessionsReducer: Reducer<SessionState, SourceActionType> = (
162163
githubOctokitObject: { octokit: undefined },
163164
githubAccessToken: undefined
164165
};
166+
case REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN:
167+
return {
168+
...state,
169+
googleUser: undefined,
170+
googleAccessToken: undefined
171+
};
165172
default:
166173
return state;
167174
}

src/commons/application/types/SessionTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const REAUTOGRADE_SUBMISSION = 'REAUTOGRADE_SUBMISSION';
5353
export const REAUTOGRADE_ANSWER = 'REAUTOGRADE_ANSWER';
5454
export const REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN =
5555
'REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN';
56+
export const REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN =
57+
'REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN';
5658
export const UNSUBMIT_SUBMISSION = 'UNSUBMIT_SUBMISSION';
5759
export const UPDATE_ASSESSMENT_OVERVIEWS = 'UPDATE_ASSESSMENT_OVERVIEWS';
5860
export const UPDATE_TOTAL_XP = 'UPDATE_TOTAL_XP';

src/commons/sagas/PersistenceSaga.tsx

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
PERSISTENCE_SAVE_FILE_AS,
1111
PersistenceFile
1212
} from '../../features/persistence/PersistenceTypes';
13+
import { getUserProfileDataEmail } from '../../features/persistence/PersistenceUtils';
1314
import { store } from '../../pages/createStore';
1415
import { OverallState } from '../application/ApplicationTypes';
1516
import { ExternalLibraryName } from '../application/types/ExternalTypes';
@@ -40,18 +41,28 @@ const MIME_SOURCE = 'text/plain';
4041
let tokenClient: google.accounts.oauth2.TokenClient;
4142

4243
export function* persistenceSaga(): SagaIterator {
43-
yield takeLatest(LOGOUT_GOOGLE, function* () {
44+
yield takeLatest(LOGOUT_GOOGLE, function* (): any {
4445
yield put(actions.playgroundUpdatePersistenceFile(undefined));
4546
yield call(ensureInitialised);
46-
yield call([gapi.client, "setToken"], null);
47-
yield call(handleUserChanged, null);
47+
yield call(gapi.client.setToken, null);
48+
yield put(actions.removeGoogleUserAndAccessToken());
4849
});
4950

50-
yield takeLatest(LOGIN_GOOGLE, function* () {
51+
yield takeLatest(LOGIN_GOOGLE, function* (): any {
5152
yield call(ensureInitialised);
5253
yield call(getToken);
5354
});
5455

56+
yield takeEvery(PERSISTENCE_INITIALISE, function* (): any {
57+
yield call(ensureInitialised);
58+
// check for stored token
59+
const accessToken = yield select((state: OverallState) => state.session.googleAccessToken);
60+
if (accessToken) {
61+
yield call(gapi.client.setToken, {access_token: accessToken});
62+
yield call(handleUserChanged, accessToken);
63+
}
64+
});
65+
5566
yield takeLatest(PERSISTENCE_OPEN_PICKER, function* (): any {
5667
let toastKey: string | undefined;
5768
try {
@@ -290,8 +301,6 @@ export function* persistenceSaga(): SagaIterator {
290301
}
291302
}
292303
);
293-
294-
yield takeEvery(PERSISTENCE_INITIALISE, ensureInitialised);
295304
}
296305

297306
interface IPlaygroundConfig {
@@ -315,38 +324,6 @@ const initialisationPromise: Promise<void> = new Promise(res => {
315324
startInitialisation = res;
316325
}).then(initialise);
317326

318-
/**
319-
* Calls Google useinfo API to get user's profile data, specifically email, using accessToken.
320-
* If email field does not exist in the JSON response (invalid access token), will return undefined.
321-
* Used with handleUserChanged to handle login/logout.
322-
* @param accessToken GIS access token
323-
* @returns string if email field exists in JSON response, undefined if not
324-
*/
325-
async function getUserProfileDataEmail(accessToken: string): Promise<string | undefined> {
326-
const headers = new Headers();
327-
headers.append('Authorization', `Bearer ${accessToken}`);
328-
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
329-
headers
330-
});
331-
const data = await response.json();
332-
return data.email;
333-
}
334-
335-
// only function that updates store
336-
async function handleUserChanged(accessToken: string | null) {
337-
// logs out if null
338-
if (accessToken === null) { // clear store
339-
store.dispatch(actions.setGoogleUser(undefined));
340-
store.dispatch(actions.setGoogleAccessToken(undefined));
341-
} else {
342-
const email = await getUserProfileDataEmail(accessToken);
343-
// if access token is invalid, const email will be undefined
344-
// so stores will also be cleared here if it is invalid
345-
store.dispatch(actions.setGoogleUser(email));
346-
store.dispatch(actions.setGoogleAccessToken(accessToken));
347-
}
348-
}
349-
350327
async function initialise() { // only called once
351328
// load GIS script
352329
// adapted from https://github.com/MomenSherif/react-oauth
@@ -384,11 +361,19 @@ async function initialise() { // only called once
384361
tokenClient = c;
385362
});
386363

387-
// check for stored token
388-
const accessToken = store.getState().session.googleAccessToken;
389-
if (accessToken) {
390-
gapi.client.setToken({access_token: accessToken});
391-
handleUserChanged(accessToken); // this also logs out user if stored token is invalid
364+
}
365+
366+
function* handleUserChanged(accessToken: string | null) {
367+
if (accessToken === null) {
368+
yield put(actions.removeGoogleUserAndAccessToken());
369+
} else {
370+
const email: string | undefined = yield call(getUserProfileDataEmail, accessToken);
371+
if (!email) {
372+
yield put(actions.removeGoogleUserAndAccessToken());
373+
} else {
374+
yield put(store.dispatch(actions.setGoogleUser(email)));
375+
yield put(store.dispatch(actions.setGoogleAccessToken(accessToken)));
376+
}
392377
}
393378
}
394379

@@ -403,14 +388,14 @@ function* getToken() {
403388
}
404389
// GIS has already automatically updated gapi.client
405390
// with the newly issued access token by this point
406-
handleUserChanged(gapi.client.getToken().access_token);
407391
resolve(resp);
408392
};
409393
tokenClient.requestAccessToken();
410394
} catch (err) {
411395
reject(err);
412396
}
413397
});
398+
yield call(handleUserChanged, gapi.client.getToken().access_token);
414399
}
415400

416401
function* ensureInitialised() {

src/commons/sagas/__tests__/PersistenceSaga.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Chapter, Variant } from 'js-slang/dist/types';
22
import { expectSaga } from 'redux-saga-test-plan';
33

44
import { PLAYGROUND_UPDATE_PERSISTENCE_FILE } from '../../../features/playground/PlaygroundTypes';
5+
import { REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN } from '../../application/types/SessionTypes';
56
import { ExternalLibraryName } from '../../application/types/ExternalTypes';
67
import { actions } from '../../utils/ActionsHelper';
78
import {
@@ -19,7 +20,6 @@ jest.mock('../../../pages/createStore');
1920
// eslint-disable-next-line @typescript-eslint/no-var-requires
2021
const PersistenceSaga = require('../PersistenceSaga').default;
2122

22-
const USER_EMAIL = 'test@email.com';
2323
const FILE_ID = '123';
2424
const FILE_NAME = 'file';
2525
const FILE_DATA = '// Hello world';
@@ -28,46 +28,44 @@ const SOURCE_VARIANT = Variant.LAZY;
2828
const SOURCE_LIBRARY = ExternalLibraryName.SOUNDS;
2929

3030
beforeAll(() => {
31-
// TODO: rewrite
32-
const authInstance: gapi.auth2.GoogleAuth = {
33-
signOut: () => {},
34-
isSignedIn: {
35-
get: () => true,
36-
listen: () => {}
37-
},
38-
currentUser: {
39-
listen: () => {},
40-
get: () => ({
41-
isSignedIn: () => true,
42-
getBasicProfile: () => ({
43-
getEmail: () => USER_EMAIL
44-
})
45-
})
46-
}
47-
} as any;
48-
4931
window.gapi = {
5032
client: {
5133
request: () => {},
5234
init: () => Promise.resolve(),
35+
getToken: () => {},
36+
setToken: () => {},
5337
drive: {
5438
files: {
5539
get: () => {}
5640
}
5741
}
5842
},
5943
load: (apiName: string, callbackOrConfig: gapi.CallbackOrConfig) =>
60-
typeof callbackOrConfig === 'function' ? callbackOrConfig() : callbackOrConfig.callback(),
61-
auth2: {
62-
getAuthInstance: () => authInstance
63-
}
44+
typeof callbackOrConfig === 'function' ? callbackOrConfig() : callbackOrConfig.callback()
6445
} as any;
6546
});
66-
// TODO: rewrite test
67-
test('LOGOUT_GOOGLE causes logout', async () => {
68-
const signOut = jest.spyOn(window.gapi.auth2.getAuthInstance(), 'signOut');
69-
await expectSaga(PersistenceSaga).dispatch(actions.logoutGoogle()).silentRun();
70-
expect(signOut).toBeCalled();
47+
48+
test('LOGOUT_GOOGLE results in REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN being dispatched', async () => {
49+
await expectSaga(PersistenceSaga)
50+
.put({
51+
type: PLAYGROUND_UPDATE_PERSISTENCE_FILE,
52+
payload: undefined,
53+
meta: undefined,
54+
error: undefined
55+
})
56+
.put({
57+
type: REMOVE_GOOGLE_USER_AND_ACCESS_TOKEN,
58+
payload: undefined,
59+
meta: undefined,
60+
error: undefined
61+
})
62+
.provide({
63+
call(effect, next) {
64+
return;
65+
}
66+
})
67+
.dispatch(actions.logoutGoogle())
68+
.silentRun();
7169
});
7270

7371
describe('PERSISTENCE_OPEN_PICKER', () => {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Calls Google useinfo API to get user's profile data, specifically email, using accessToken.
3+
* If email field does not exist in the JSON response (invalid access token), will return undefined.
4+
* Used with handleUserChanged to handle login.
5+
* @param accessToken GIS access token
6+
* @returns string if email field exists in JSON response, undefined if not
7+
*/
8+
export async function getUserProfileDataEmail(accessToken: string): Promise<string | undefined> {
9+
const headers = new Headers();
10+
headers.append('Authorization', `Bearer ${accessToken}`);
11+
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
12+
headers
13+
});
14+
const data = await response.json();
15+
return data.email;
16+
}

src/pages/__tests__/createStore.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const mockChangedStoredState: SavedState = {
2020
role: undefined,
2121
name: 'Jeff',
2222
userId: 1,
23-
githubAccessToken: 'githubAccessToken'
23+
githubAccessToken: 'githubAccessToken',
24+
googleAccessToken: 'googleAccessToken'
2425
},
2526
playgroundIsFolderModeEnabled: true,
2627
playgroundActiveEditorTabIndex: {
@@ -57,7 +58,8 @@ const mockChangedState: OverallState = {
5758
role: undefined,
5859
name: 'Jeff',
5960
userId: 1,
60-
githubAccessToken: 'githubAccessToken'
61+
githubAccessToken: 'githubAccessToken',
62+
googleAccessToken: 'googleAccessToken'
6163
},
6264
workspaces: {
6365
...defaultState.workspaces,
@@ -102,8 +104,12 @@ describe('createStore() function', () => {
102104
const octokit = received.session.githubOctokitObject.octokit;
103105
delete received.session.githubOctokitObject.octokit;
104106

107+
const googleUser = received.session.googleUser;
108+
delete received.session.googleUser;
109+
105110
expect(received).toEqual(mockChangedState);
106111
expect(octokit).toBeDefined();
112+
expect(googleUser).toEqual("placeholder");
107113
localStorage.removeItem('storedState');
108114
});
109115
});

src/pages/createStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function loadStore(loadedStore: SavedState | undefined) {
5555
: undefined
5656
},
5757
googleUser: loadedStore.session.googleAccessToken
58-
? 'placeholder'
58+
? 'placeholder' // updates in PersistenceSaga
5959
: undefined
6060
},
6161
workspaces: {

0 commit comments

Comments
 (0)