diff --git a/src/commons/application/actions/SessionActions.ts b/src/commons/application/actions/SessionActions.ts index b5d47662b6..9176862b62 100644 --- a/src/commons/application/actions/SessionActions.ts +++ b/src/commons/application/actions/SessionActions.ts @@ -22,7 +22,8 @@ import { LOGOUT_GOOGLE, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, - REMOVE_GITHUB_OCTOKIT_OBJECT, + REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, + SET_GITHUB_ACCESS_TOKEN, SET_GITHUB_ASSESSMENT, SET_GITHUB_OCTOKIT_OBJECT, SET_GOOGLE_USER, @@ -88,7 +89,11 @@ export const setGitHubAssessment = (missionRepoData: MissionRepoData) => export const setGitHubOctokitObject = (authToken?: string) => action(SET_GITHUB_OCTOKIT_OBJECT, generateOctokitInstance(authToken || '')); -export const removeGitHubOctokitObject = () => action(REMOVE_GITHUB_OCTOKIT_OBJECT); +export const setGitHubAccessToken = (authToken?: string) => + action(SET_GITHUB_ACCESS_TOKEN, authToken); + +export const removeGitHubOctokitObjectAndAccessToken = () => + action(REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN); export const submitAnswer = (id: number, answer: string | number | ContestEntry[]) => action(SUBMIT_ANSWER, { diff --git a/src/commons/application/actions/__tests__/SessionActions.ts b/src/commons/application/actions/__tests__/SessionActions.ts index 56695f9987..26ee9fc95f 100644 --- a/src/commons/application/actions/__tests__/SessionActions.ts +++ b/src/commons/application/actions/__tests__/SessionActions.ts @@ -13,6 +13,7 @@ import { LOGIN, REAUTOGRADE_ANSWER, REAUTOGRADE_SUBMISSION, + SET_GITHUB_ACCESS_TOKEN, SET_GITHUB_OCTOKIT_OBJECT, SET_TOKENS, SET_USER, @@ -39,6 +40,7 @@ import { login, reautogradeAnswer, reautogradeSubmission, + setGitHubAccessToken, setGitHubOctokitObject, setTokens, setUser, @@ -173,6 +175,15 @@ test('setGitHubOctokitInstance generates correct action object', async () => { expect(authObject.tokenType).toBe('oauth'); }); +test('setGitHubAccessToken generates correct action object', () => { + const authToken = 'testAuthToken12345'; + const action = setGitHubAccessToken(authToken); + expect(action).toEqual({ + type: SET_GITHUB_ACCESS_TOKEN, + payload: authToken + }); +}); + test('submitAnswer generates correct action object', () => { const id = 3; const answer = 'test-answer-here'; diff --git a/src/commons/application/reducers/SessionsReducer.ts b/src/commons/application/reducers/SessionsReducer.ts index 5efc886d5e..ab6dc8aea2 100644 --- a/src/commons/application/reducers/SessionsReducer.ts +++ b/src/commons/application/reducers/SessionsReducer.ts @@ -8,8 +8,9 @@ import { SourceActionType } from '../../utils/ActionsHelper'; import { defaultSession } from '../ApplicationTypes'; import { LOG_OUT } from '../types/CommonsTypes'; import { - REMOVE_GITHUB_OCTOKIT_OBJECT, + REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, SessionState, + SET_GITHUB_ACCESS_TOKEN, SET_GITHUB_ASSESSMENT, SET_GITHUB_OCTOKIT_OBJECT, SET_GOOGLE_USER, @@ -40,6 +41,11 @@ export const SessionsReducer: Reducer = ( ...state, githubOctokitObject: { octokit: action.payload } }; + case SET_GITHUB_ACCESS_TOKEN: + return { + ...state, + githubAccessToken: action.payload + }; case SET_GOOGLE_USER: return { ...state, @@ -109,10 +115,11 @@ export const SessionsReducer: Reducer = ( ...state, remoteExecutionSession: action.payload }; - case REMOVE_GITHUB_OCTOKIT_OBJECT: + case REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN: return { ...state, - githubOctokitObject: { octokit: undefined } + githubOctokitObject: { octokit: undefined }, + githubAccessToken: undefined }; default: return state; diff --git a/src/commons/application/reducers/__tests__/SessionReducer.ts b/src/commons/application/reducers/__tests__/SessionReducer.ts index 3f264c7b57..aa885d4d81 100644 --- a/src/commons/application/reducers/__tests__/SessionReducer.ts +++ b/src/commons/application/reducers/__tests__/SessionReducer.ts @@ -12,6 +12,7 @@ import { defaultSession, GameState, Role, Story } from '../../ApplicationTypes'; import { LOG_OUT } from '../../types/CommonsTypes'; import { SessionState, + SET_GITHUB_ACCESS_TOKEN, SET_TOKENS, SET_USER, UPDATE_ASSESSMENT, @@ -82,6 +83,20 @@ test('SET_USER works correctly', () => { }); }); +test('SET_GITHUB_ACCESS_TOKEN works correctly', () => { + const token = 'githubAccessToken'; + const action = { + type: SET_GITHUB_ACCESS_TOKEN, + payload: token + }; + const result: SessionState = SessionsReducer(defaultSession, action); + + expect(result).toEqual({ + ...defaultSession, + githubAccessToken: token + }); +}); + test('UPDATE_HISTORY_HELPERS works on non-academy location', () => { const payload = '/playground'; const historyHelper: HistoryHelper = { diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index 9171b424bc..bf47d456b8 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -22,13 +22,15 @@ export const SET_USER = 'SET_USER'; export const SET_GOOGLE_USER = 'SET_GOOGLE_USER'; export const SET_GITHUB_ASSESSMENT = 'SET_GITHUB_ASSESSMENT'; export const SET_GITHUB_OCTOKIT_OBJECT = 'SET_GITHUB_OCTOKIT_OBJECT'; +export const SET_GITHUB_ACCESS_TOKEN = 'SET_GITHUB_ACCESS_TOKEN'; export const SUBMIT_ANSWER = 'SUBMIT_ANSWER'; export const SUBMIT_ASSESSMENT = 'SUBMIT_ASSESSMENT'; export const SUBMIT_GRADING = 'SUBMIT_GRADING'; export const SUBMIT_GRADING_AND_CONTINUE = 'SUBMIT_GRADING_AND_CONTINUE'; export const REAUTOGRADE_SUBMISSION = 'REAUTOGRADE_SUBMISSION'; export const REAUTOGRADE_ANSWER = 'REAUTOGRADE_ANSWER'; -export const REMOVE_GITHUB_OCTOKIT_OBJECT = 'REMOVE_GITHUB_OCTOKIT_OBJECT'; +export const REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN = + 'REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN'; export const UNSUBMIT_SUBMISSION = 'UNSUBMIT_SUBMISSION'; export const UPDATE_HISTORY_HELPERS = 'UPDATE_HISTORY_HELPERS'; export const UPDATE_ASSESSMENT_OVERVIEWS = 'UPDATE_ASSESSMENT_OVERVIEWS'; @@ -64,6 +66,7 @@ export type SessionState = { readonly googleUser?: string; readonly githubAssessment?: MissionRepoData; readonly githubOctokitObject: { octokit: Octokit | undefined }; + readonly githubAccessToken?: string; readonly remoteExecutionDevices?: Device[]; readonly remoteExecutionSession?: DeviceSession; }; diff --git a/src/commons/sagas/GitHubPersistenceSaga.ts b/src/commons/sagas/GitHubPersistenceSaga.ts index 1d531a76d8..95100f763e 100644 --- a/src/commons/sagas/GitHubPersistenceSaga.ts +++ b/src/commons/sagas/GitHubPersistenceSaga.ts @@ -41,6 +41,7 @@ function* githubLoginSaga() { broadcastChannel.onmessage = receivedMessage => { store.dispatch(actions.setGitHubOctokitObject(receivedMessage.data)); + store.dispatch(actions.setGitHubAccessToken(receivedMessage.data)); showSuccessMessage('Logged in to GitHub', 1000); }; @@ -61,7 +62,7 @@ function* githubLogoutSaga() { return; } - yield put(actions.removeGitHubOctokitObject()); + yield put(actions.removeGitHubOctokitObjectAndAccessToken()); yield call(showSuccessMessage, `Logged out from GitHub`, 1000); yield call(history.push, '/githubassessments/missions'); } diff --git a/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts b/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts index bbb6d91f88..69094a4f2f 100644 --- a/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts +++ b/src/commons/sagas/__tests__/GitHubPersistenceSaga.ts @@ -1,7 +1,7 @@ import { expectSaga } from 'redux-saga-test-plan'; import { actions } from '../../../commons/utils/ActionsHelper'; -import { REMOVE_GITHUB_OCTOKIT_OBJECT } from '../../application/types/SessionTypes'; +import { REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN } from '../../application/types/SessionTypes'; // mock away the store jest.mock('../../../pages/createStore'); @@ -12,7 +12,7 @@ const GitHubPersistenceSaga = require('../GitHubPersistenceSaga').default; test('logoutGitHub results in REMOVE_GITHUB_OCTOKIT_OBJECT being dispatched', async () => { await expectSaga(GitHubPersistenceSaga) .put({ - type: REMOVE_GITHUB_OCTOKIT_OBJECT, + type: REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN, payload: undefined, meta: undefined, error: undefined diff --git a/src/pages/__tests__/createStore.test.ts b/src/pages/__tests__/createStore.test.ts index e2eb8ab990..2f0c3a57b9 100644 --- a/src/pages/__tests__/createStore.test.ts +++ b/src/pages/__tests__/createStore.test.ts @@ -14,7 +14,9 @@ const mockChangedStoredState: SavedState = { accessToken: 'yep', refreshToken: 'refresherOrb', role: undefined, - name: 'Jeff' + name: 'Jeff', + userId: 1, + githubAccessToken: 'githubAccessToken' }, playgroundEditorValue: 'Nihao everybody', playgroundIsEditorAutorun: true, @@ -30,7 +32,9 @@ const mockChangedState: OverallState = { accessToken: 'yep', refreshToken: 'refresherOrb', role: undefined, - name: 'Jeff' + name: 'Jeff', + userId: 1, + githubAccessToken: 'githubAccessToken' }, workspaces: { ...defaultState.workspaces, @@ -62,10 +66,22 @@ describe('createStore() function', () => { }); test('has correct getState() when called with storedState', () => { localStorage.setItem('storedState', compressToUTF16(JSON.stringify(mockChangedStoredState))); - expect(createStore(history).getState()).toEqual({ + + /** + * Jest toEqual is unable to compare equality of functions (in the Octokit object). + * Thus we simply check that it is defined when loading storedState. + * + * See https://github.com/facebook/jest/issues/8166 + */ + const received = createStore(history).getState() as any; + const octokit = received.session.githubOctokitObject.octokit; + delete received.session.githubOctokitObject.octokit; + + expect(received).toEqual({ ...mockChangedState, router: defaultRouter }); + expect(octokit).toBeDefined(); localStorage.removeItem('storedState'); }); }); diff --git a/src/pages/createStore.ts b/src/pages/createStore.ts index f8a747ecb6..4f936d56a1 100644 --- a/src/pages/createStore.ts +++ b/src/pages/createStore.ts @@ -3,6 +3,7 @@ import { History } from 'history'; import { throttle } from 'lodash'; import { applyMiddleware, compose, createStore as _createStore } from 'redux'; import createSagaMiddleware from 'redux-saga'; +import { generateOctokitInstance } from 'src/commons/utils/GitHubPersistenceHelper'; import { defaultState } from '../commons/application/ApplicationTypes'; import createRootReducer from '../commons/application/reducers/RootReducer'; @@ -47,7 +48,12 @@ function loadStore(loadedStore: SavedState | undefined) { ...defaultState, session: { ...defaultState.session, - ...(loadedStore.session ? loadedStore.session : {}) + ...(loadedStore.session ? loadedStore.session : {}), + githubOctokitObject: { + octokit: loadedStore.session.githubAccessToken + ? generateOctokitInstance(loadedStore.session.githubAccessToken) + : undefined + } }, workspaces: { ...defaultState.workspaces, diff --git a/src/pages/localStorage.ts b/src/pages/localStorage.ts index 657ac421e8..c27deeca64 100644 --- a/src/pages/localStorage.ts +++ b/src/pages/localStorage.ts @@ -42,7 +42,8 @@ export const saveState = (state: OverallState) => { refreshToken: state.session.refreshToken, role: state.session.role, name: state.session.name, - userId: state.session.userId + userId: state.session.userId, + githubAccessToken: state.session.githubAccessToken }, achievements: state.achievement.achievements, playgroundEditorValue: state.workspaces.playground.editorValue,