diff --git a/scripts/build_deploy.ts b/scripts/build_deploy.ts index 55664922..cbcd0840 100755 --- a/scripts/build_deploy.ts +++ b/scripts/build_deploy.ts @@ -16,6 +16,7 @@ interface EnvironmentConfig { public_url: string; backup_cookie?: { name: string; + domain: string; }; } @@ -60,6 +61,7 @@ const setEnvironment = ( REACT_APP_KBASE_DOMAIN: domain, REACT_APP_KBASE_LEGACY_DOMAIN: legacy, REACT_APP_KBASE_BACKUP_COOKIE_NAME: backupCookie?.name || '', + REACT_APP_KBASE_BACKUP_COOKIE_DOMAIN: backupCookie?.domain || '', }; Object.assign(process.env, envsNew); }; diff --git a/src/features/auth/authSlice.test.tsx b/src/features/auth/authSlice.test.tsx index 98d35352..f669ca13 100644 --- a/src/features/auth/authSlice.test.tsx +++ b/src/features/auth/authSlice.test.tsx @@ -123,27 +123,39 @@ describe('authSlice', () => { ReturnType, Parameters >; - let mockCookieVal = ''; - const setTokenCookieMock = jest.fn(); - const clearTokenCookieMock = jest.fn(); + let mockCookieVals: Record = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let setTokenCookieMocks: Record> = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let clearTokenCookieMocks: Record> = {}; beforeAll(() => { useCookieMock = jest.spyOn(cookies, 'useCookie'); - useCookieMock.mockImplementation(() => [ - mockCookieVal, - setTokenCookieMock, - clearTokenCookieMock, - ]); + useCookieMock.mockImplementation((name?: string) => { + const key = name || 'UNKNOWN'; + setTokenCookieMocks[key] ??= jest.fn((val) => { + if (val) mockCookieVals[key] = val; + }); + clearTokenCookieMocks[key] ??= jest.fn(() => { + mockCookieVals[key] = ''; + }); + return [ + mockCookieVals[key] || '', + setTokenCookieMocks[key], + clearTokenCookieMocks[key], + ]; + }); consoleErrorMock = jest.spyOn(console, 'error'); consoleErrorMock.mockImplementation(() => undefined); }); beforeEach(() => { - setTokenCookieMock.mockClear(); - clearTokenCookieMock.mockClear(); + mockCookieVals = {}; + setTokenCookieMocks = {}; + clearTokenCookieMocks = {}; + useCookieMock.mockClear(); consoleErrorMock.mockClear(); }); afterAll(() => { - setTokenCookieMock.mockRestore(); - clearTokenCookieMock.mockRestore(); + useCookieMock.mockRestore(); consoleErrorMock.mockRestore(); }); @@ -158,7 +170,7 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(clearTokenCookieMock).toHaveBeenCalledWith(); + expect(clearTokenCookieMocks['kbase_session']).toHaveBeenCalledWith(); expect(consoleErrorMock).not.toHaveBeenCalled(); }); }); @@ -182,13 +194,117 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).toHaveBeenCalledWith('some-token', { - expires: new Date(auth.tokenInfo.expires), + expect(setTokenCookieMocks['kbase_session']).toHaveBeenCalledWith( + 'some-token', + { + expires: new Date(auth.tokenInfo.expires), + } + ); + expect(consoleErrorMock).not.toHaveBeenCalled(); + }); + }); + + test('useTokenCookie sets backup cookie if auth token exists with expiration', async () => { + const auth = { + token: 'some-token', + username: 'some-user', + tokenInfo: { + expires: Date.now() + Math.floor(Math.random() * 10000), + } as TokenInfo, + initialized: true, + }; + const Component = () => { + useTokenCookie('kbase_session', 'backup_cookie', '.backup.domain'); + return <>; + }; + render( + + + + ); + await waitFor(() => { + expect(useCookieMock).toHaveBeenCalledWith('backup_cookie', { + domain: '.backup.domain', + path: '/', + secure: true, + }); + expect(setTokenCookieMocks['backup_cookie']).toHaveBeenCalledWith( + 'some-token', + { + expires: new Date(auth.tokenInfo.expires), + } + ); + expect(consoleErrorMock).not.toHaveBeenCalled(); + }); + }); + + test('useTokenCookie sets backup cookie if missing', async () => { + const auth = { + token: 'some-token', + username: 'some-user', + tokenInfo: { + expires: Date.now() + Math.floor(Math.random() * 10000), + } as TokenInfo, + initialized: true, + }; + mockCookieVals['kbase_session'] = 'some-token'; + const Component = () => { + useTokenCookie('kbase_session', 'backup_cookie', '.backup.domain'); + return <>; + }; + render( + + + + ); + await waitFor(() => { + expect(useCookieMock).toHaveBeenCalledWith('backup_cookie', { + domain: '.backup.domain', + path: '/', + secure: true, }); + expect(setTokenCookieMocks['backup_cookie']).toHaveBeenCalledWith( + 'some-token', + { + expires: new Date(auth.tokenInfo.expires), + } + ); expect(consoleErrorMock).not.toHaveBeenCalled(); }); }); + test('useTokenCookie wont set backup cookie if domain is empty', async () => { + const auth = { + token: 'some-token', + username: 'some-user', + tokenInfo: { + expires: Date.now() + Math.floor(Math.random() * 10000), + } as TokenInfo, + initialized: true, + }; + mockCookieVals['kbase_session'] = 'some-token'; + const Component = () => { + useTokenCookie('kbase_session', 'backup_cookie', ''); + return <>; + }; + render( + + + + ); + await waitFor(() => { + expect(useCookieMock).toHaveBeenCalledWith('backup_cookie', { + domain: '', + path: '/', + secure: true, + }); + expect(setTokenCookieMocks['backup_cookie']).not.toHaveBeenCalled(); + expect(consoleErrorMock).toHaveBeenCalledWith( + 'Backup cookie cannot be set due to bad configuration.' + ); + }); + }); + test('useTokenCookie sets cookie in development mode', async () => { const processEnv = process.env; process.env = { ...processEnv, NODE_ENV: 'development' }; @@ -210,9 +326,12 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).toHaveBeenCalledWith('some-token', { - expires: new Date(auth.tokenInfo.expires), - }); + expect(setTokenCookieMocks['kbase_session']).toHaveBeenCalledWith( + 'some-token', + { + expires: new Date(auth.tokenInfo.expires), + } + ); expect(consoleErrorMock).not.toHaveBeenCalled(); }); process.env = processEnv; @@ -239,13 +358,13 @@ describe('authSlice', () => { expect(consoleErrorMock).toHaveBeenCalledWith( 'Could not set token cookie, missing expire time' ); - expect(setTokenCookieMock).not.toHaveBeenCalled(); + expect(setTokenCookieMocks['kbase_session']).not.toHaveBeenCalled(); }); }); test('useTokenCookie clears cookie for bad cookie token and empty auth state', async () => { const auth = { initialized: false }; - mockCookieVal = 'AAAAAA'; + mockCookieVals['kbase_session'] = 'AAAAAA'; const mock = jest.spyOn(authFromToken, 'useQuery'); mock.mockImplementation(() => { return { @@ -264,8 +383,8 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).not.toBeCalled(); - expect(clearTokenCookieMock).toBeCalled(); + expect(setTokenCookieMocks['kbase_session']).not.toBeCalled(); + expect(clearTokenCookieMocks['kbase_session']).toBeCalled(); }); mock.mockClear(); }); @@ -279,7 +398,7 @@ describe('authSlice', () => { } as TokenInfo, initialized: true, }; - mockCookieVal = 'AAAAAA'; + mockCookieVals['kbase_session'] = 'AAAAAA'; const mock = jest.spyOn(authFromToken, 'useQuery'); mock.mockImplementation(() => { return { @@ -298,15 +417,15 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).toBeCalled(); - expect(clearTokenCookieMock).not.toBeCalled(); + expect(setTokenCookieMocks['kbase_session']).toBeCalled(); + expect(clearTokenCookieMocks['kbase_session']).not.toBeCalled(); }); mock.mockClear(); }); test('useTokenCookie does not set cookie while awaiting auth response', async () => { const auth = { initialized: false }; - mockCookieVal = 'AAAAAA'; + mockCookieVals['kbase_session'] = 'AAAAAA'; const mock = jest.spyOn(authFromToken, 'useQuery'); mock.mockImplementation(() => { return { @@ -325,8 +444,8 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).not.toBeCalled(); - expect(clearTokenCookieMock).not.toBeCalled(); + expect(setTokenCookieMocks['kbase_session']).not.toBeCalled(); + expect(clearTokenCookieMocks['kbase_session']).not.toBeCalled(); }); mock.mockImplementation(() => { return { @@ -342,10 +461,10 @@ describe('authSlice', () => { ); await waitFor(() => { - expect(setTokenCookieMock).toBeCalledWith('AAAAAA', { + expect(setTokenCookieMocks['kbase_session']).toBeCalledWith('AAAAAA', { expires: new Date(10), }); - expect(clearTokenCookieMock).not.toBeCalled(); + expect(clearTokenCookieMocks['kbase_session']).not.toBeCalled(); }); mock.mockClear(); }); diff --git a/src/features/auth/hooks.ts b/src/features/auth/hooks.ts index 6c5005ce..ec32def7 100644 --- a/src/features/auth/hooks.ts +++ b/src/features/auth/hooks.ts @@ -125,6 +125,7 @@ export const useTokenCookie = ( useEffect(() => { if ( Boolean(backupCookieName) && + Boolean(backupCookieDomain) && initialized && currentToken && backupCookieToken !== currentToken @@ -137,6 +138,12 @@ export const useTokenCookie = ( expires: new Date(currentExpires), }); } + } else if ( + (Boolean(backupCookieName) || Boolean(backupCookieDomain)) && + (!backupCookieDomain || !backupCookieName) + ) { + // eslint-disable-next-line no-console + console.error('Backup cookie cannot be set due to bad configuration.'); } }, [ backupCookieDomain,