From f77d3bfbf20869596af8434e0f7cb0397e3d0650 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 28 Aug 2023 21:11:33 +0200 Subject: [PATCH] test(auth): refactor to be compliant with new session management --- .github/workflows/ci.yml | 2 +- .../utils/__tests__/page-helpers.test.ts | 6 +- src/server/core/Logger/Logger.ts | 48 ++- src/server/routers/auth/auth.router.test.ts | 364 ------------------ src/server/services/auth/auth.service.test.ts | 108 ++++-- 5 files changed, 107 insertions(+), 421 deletions(-) delete mode 100644 src/server/routers/auth/auth.router.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67fffb98..ee3beb04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: run: pnpm install - name: Build client - run: npm run build:next + run: npm run build - name: Run tsc run: pnpm run tsc diff --git a/src/client/utils/__tests__/page-helpers.test.ts b/src/client/utils/__tests__/page-helpers.test.ts index 36119202..669076e3 100644 --- a/src/client/utils/__tests__/page-helpers.test.ts +++ b/src/client/utils/__tests__/page-helpers.test.ts @@ -1,6 +1,7 @@ import merge from 'lodash.merge'; import { deleteCookie, setCookie } from 'cookies-next'; import { fromPartial } from '@total-typescript/shoehorn'; +import TipiCache from '@/server/core/TipiCache/TipiCache'; import { getAuthedPageProps, getMessagesPageProps } from '../page-helpers'; import englishMessages from '../../messages/en.json'; import frenchMessages from '../../messages/fr-FR.json'; @@ -8,7 +9,7 @@ import frenchMessages from '../../messages/fr-FR.json'; describe('test: getAuthedPageProps()', () => { it('should redirect to /login if there is no user id in session', async () => { // arrange - const ctx = { req: { session: {} } }; + const ctx = { req: { headers: {} } }; // act // @ts-expect-error - we're passing in a partial context @@ -21,7 +22,8 @@ describe('test: getAuthedPageProps()', () => { it('should return props if there is a user id in session', async () => { // arrange - const ctx = { req: { session: { userId: '123' } } }; + const ctx = { req: { headers: { 'x-session-id': '123' } } }; + await TipiCache.set('session:123', '456'); // act // @ts-expect-error - we're passing in a partial context diff --git a/src/server/core/Logger/Logger.ts b/src/server/core/Logger/Logger.ts index a401a37f..54fe26d2 100644 --- a/src/server/core/Logger/Logger.ts +++ b/src/server/core/Logger/Logger.ts @@ -23,27 +23,35 @@ const combinedLogFormatDev = combine( const productionLogger = () => { const logsFolder = '/app/logs'; - if (!fs.existsSync(logsFolder)) { - fs.mkdirSync(logsFolder); + try { + if (!fs.existsSync(logsFolder)) { + fs.mkdirSync(logsFolder); + } + return createLogger({ + level: 'info', + format: combinedLogFormat, + transports: [ + // + // - Write to all logs with level `info` and below to `app.log` + // - Write all logs error (and below) to `error.log`. + // + new transports.File({ + filename: path.join(logsFolder, 'error.log'), + level: 'error', + }), + new transports.File({ + filename: path.join(logsFolder, 'app.log'), + }), + ], + exceptionHandlers: [new transports.File({ filename: path.join(logsFolder, 'error.log') })], + }); + } catch (e) { + return createLogger({ + level: 'info', + format: combinedLogFormat, + transports: [], + }); } - return createLogger({ - level: 'info', - format: combinedLogFormat, - transports: [ - // - // - Write to all logs with level `info` and below to `app.log` - // - Write all logs error (and below) to `error.log`. - // - new transports.File({ - filename: path.join(logsFolder, 'error.log'), - level: 'error', - }), - new transports.File({ - filename: path.join(logsFolder, 'app.log'), - }), - ], - exceptionHandlers: [new transports.File({ filename: path.join(logsFolder, 'error.log') })], - }); }; // diff --git a/src/server/routers/auth/auth.router.test.ts b/src/server/routers/auth/auth.router.test.ts deleted file mode 100644 index 450eda28..00000000 --- a/src/server/routers/auth/auth.router.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { fromPartial } from '@total-typescript/shoehorn'; -import { TestDatabase, clearDatabase, closeDatabase, setupTestSuite } from '@/server/tests/test-utils'; -import { createUser } from '@/server/tests/user.factory'; -import { AuthRouter } from './auth.router'; - -let db: TestDatabase; -let authRouter: AuthRouter; -const TEST_SUITE = 'authrouter'; -jest.mock('fs-extra'); - -beforeAll(async () => { - const testSuite = await setupTestSuite(TEST_SUITE); - db = testSuite; - authRouter = (await import('./auth.router')).authRouter; -}); - -beforeEach(async () => { - await clearDatabase(db); -}); - -afterAll(async () => { - await closeDatabase(db); -}); - -describe('Test: login', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.login({ password: '123456', username: 'test' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: logout', () => { - it('should not be accessible without an account', async () => { - // arrange - // @ts-expect-error - we're testing the error case - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456, destroy: (cb) => cb() } } })); - let error; - - // act - try { - await caller.logout(); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 123456 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } })); - let error; - - // act - try { - await caller.logout(); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: register', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.register({ username: 'test@test.com', password: '123', locale: 'en' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: me', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - - // act - const result = await caller.me(); - - // assert - expect(result).toBe(null); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 123456 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } })); - - // act - const result = await caller.me(); - - // assert - expect(result).not.toBe(null); - expect(result?.id).toBe(123456); - }); -}); - -describe('Test: isConfigured', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - - // act - const result = await caller.isConfigured(); - - // assert - expect(result).toBe(false); - }); -}); - -describe('Test: verifyTotp', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.verifyTotp({ totpCode: '123456', totpSessionId: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - expect(error?.code).toBeDefined(); - expect(error?.code).not.toBe(null); - }); -}); - -describe('Test: getTotpUri', () => { - it('should not be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.getTotpUri({ password: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 123456 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } })); - let error; - - // act - try { - await caller.getTotpUri({ password: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: setupTotp', () => { - it('should not be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.setupTotp({ totpCode: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 123456 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } })); - let error; - - // act - try { - await caller.setupTotp({ totpCode: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: disableTotp', () => { - it('should not be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.disableTotp({ password: '123456' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 123456 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } })); - let error; - - // act - - try { - await caller.disableTotp({ password: '112321' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: changeOperatorPassword', () => { - it('should be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.changeOperatorPassword({ newPassword: '222' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 122 } } })); - let error; - - // act - try { - await caller.changeOperatorPassword({ newPassword: '222' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: resetPassword', () => { - it('should not be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.changePassword({ currentPassword: '111', newPassword: '222' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 122 }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 122 } } })); - let error; - - // act - try { - await caller.changePassword({ currentPassword: '111', newPassword: '222' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); - -describe('Test: changeLocale', () => { - it('should not be accessible without an account', async () => { - // arrange - const caller = authRouter.createCaller(fromPartial({ req: { session: {} } })); - let error; - - // act - try { - await caller.changeLocale({ locale: 'en' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).toBe('UNAUTHORIZED'); - }); - - it('should be accessible with an account', async () => { - // arrange - await createUser({ id: 122, locale: 'en' }, db); - const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 122 } } })); - let error; - - // act - try { - await caller.changeLocale({ locale: 'fr-FR' }); - } catch (e) { - error = e as { code: string }; - } - - // assert - expect(error?.code).not.toBe('UNAUTHORIZED'); - }); -}); diff --git a/src/server/services/auth/auth.service.test.ts b/src/server/services/auth/auth.service.test.ts index 3b8a8fd9..817b3f87 100644 --- a/src/server/services/auth/auth.service.test.ts +++ b/src/server/services/auth/auth.service.test.ts @@ -1,5 +1,4 @@ import fs from 'fs-extra'; -import { vi } from 'vitest'; import * as argon2 from 'argon2'; import { faker } from '@faker-js/faker'; import { TotpAuthenticator } from '@/server/utils/totp'; @@ -7,6 +6,7 @@ import { generateSessionId } from '@/server/common/session.helpers'; import { fromAny, fromPartial } from '@total-typescript/shoehorn'; import { mockInsert, mockQuery, mockSelect } from '@/tests/mocks/drizzle'; import { createDatabase, clearDatabase, closeDatabase, TestDatabase } from '@/server/tests/test-utils'; +import { v4 } from 'uuid'; import { encrypt } from '../../utils/encryption'; import { setConfig } from '../../core/TipiConfig'; import { createUser, getUserByEmail, getUserById } from '../../tests/user.factory'; @@ -35,25 +35,38 @@ afterAll(async () => { describe('Login', () => { it('Should correclty set session on request object', async () => { // arrange - const req = { session: { userId: undefined } }; + let session = ''; + const res = { + getHeader: () => {}, + setHeader: (_: unknown, o: string[]) => { + // eslint-disable-next-line prefer-destructuring + session = o[0] as string; + }, + }; const email = faker.internet.email(); const user = await createUser({ email }, database); // act - await AuthService.login({ username: email, password: 'password' }, fromPartial(req)); + await AuthService.login({ username: email, password: 'password' }, fromPartial({}), fromPartial(res)); + + const sessionId = session.split(';')[0]?.split('=')[1]; + const sessionKey = `session:${sessionId}`; + const userId = await TipiCache.get(sessionKey); // assert - expect(req.session.userId).toBe(user.id); + expect(userId).toBeDefined(); + expect(userId).not.toBeNull(); + expect(userId).toBe(user.id.toString()); }); it('Should throw if user does not exist', async () => { - await expect(AuthService.login({ username: 'test', password: 'test' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.user-not-found'); + await expect(AuthService.login({ username: 'test', password: 'test' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.user-not-found'); }); it('Should throw if password is incorrect', async () => { const email = faker.internet.email(); await createUser({ email }, database); - await expect(AuthService.login({ username: email, password: 'wrong' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.invalid-credentials'); + await expect(AuthService.login({ username: email, password: 'wrong' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.invalid-credentials'); }); // TOTP @@ -64,7 +77,7 @@ describe('Login', () => { await createUser({ email, totpEnabled: true, totpSecret }, database); // act - const { totpSessionId } = await AuthService.login({ username: email, password: 'password' }, fromPartial({})); + const { totpSessionId } = await AuthService.login({ username: email, password: 'password' }, fromPartial({}), fromPartial({})); // assert expect(totpSessionId).toBeDefined(); @@ -75,7 +88,14 @@ describe('Login', () => { describe('Test: verifyTotp', () => { it('should correctly log in user after totp is verified', async () => { // arrange - const req = { session: { userId: undefined } }; + let session = ''; + const res = { + getHeader: () => {}, + setHeader: (_: unknown, o: string[]) => { + // eslint-disable-next-line prefer-destructuring + session = o[0] as string; + }, + }; const email = faker.internet.email(); const salt = faker.lorem.word(); const totpSecret = TotpAuthenticator.generateSecret(); @@ -88,14 +108,16 @@ describe('Test: verifyTotp', () => { await TipiCache.set(totpSessionId, user.id.toString()); // act - const result = await AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial(req)); + const result = await AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial({}), fromPartial(res)); + const sessionId = session.split(';')[0]?.split('=')[1]; + const userId = await TipiCache.get(`session:${sessionId}`); // assert expect(result).toBeTruthy(); expect(result).not.toBeNull(); - expect(req.session.userId).toBeDefined(); - expect(req.session.userId).not.toBeNull(); - expect(req.session.userId).toBe(user.id); + expect(sessionId).toBeDefined(); + expect(sessionId).not.toBeNull(); + expect(userId).toBe(user.id.toString()); }); it('should throw if the totp is incorrect', async () => { @@ -109,7 +131,7 @@ describe('Test: verifyTotp', () => { await TipiCache.set(totpSessionId, user.id.toString()); // act & assert - await expect(AuthService.verifyTotp({ totpSessionId, totpCode: 'wrong' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-invalid-code'); + await expect(AuthService.verifyTotp({ totpSessionId, totpCode: 'wrong' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-invalid-code'); }); it('should throw if the totpSessionId is invalid', async () => { @@ -125,7 +147,7 @@ describe('Test: verifyTotp', () => { await TipiCache.set(totpSessionId, user.id.toString()); // act & assert - await expect(AuthService.verifyTotp({ totpSessionId: 'wrong', totpCode: otp }, fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-session-not-found'); + await expect(AuthService.verifyTotp({ totpSessionId: 'wrong', totpCode: otp }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-session-not-found'); }); it('should throw if the user does not exist', async () => { @@ -134,7 +156,7 @@ describe('Test: verifyTotp', () => { await TipiCache.set(totpSessionId, '1234'); // act & assert - await expect(AuthService.verifyTotp({ totpSessionId, totpCode: '1234' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.user-not-found'); + await expect(AuthService.verifyTotp({ totpSessionId, totpCode: '1234' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.user-not-found'); }); it('should throw if the user totpEnabled is false', async () => { @@ -150,7 +172,7 @@ describe('Test: verifyTotp', () => { await TipiCache.set(totpSessionId, user.id.toString()); // act & assert - await expect(AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-not-enabled'); + await expect(AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-not-enabled'); }); }); @@ -353,26 +375,39 @@ describe('Test: disableTotp', () => { }); describe('Register', () => { - it('Should correctly set session on request object', async () => { + it('Should correctly set session on response object', async () => { // arrange - const req = { session: { userId: undefined } }; + let session = ''; + const res = { + getHeader: () => {}, + setHeader: (_: unknown, o: string[]) => { + // eslint-disable-next-line prefer-destructuring + session = o[0] as string; + }, + }; const email = faker.internet.email(); // act - const result = await AuthService.register({ username: email, password: 'password' }, fromPartial(req)); + const result = await AuthService.register({ username: email, password: 'password' }, fromPartial({}), fromPartial(res)); + const sessionId = session.split(';')[0]?.split('=')[1]; // assert expect(result).toBeTruthy(); expect(result).not.toBeNull(); - expect(req.session.userId).toBeDefined(); + expect(sessionId).toBeDefined(); + expect(sessionId).not.toBeNull(); }); it('Should correctly trim and lowercase email', async () => { // arrange const email = faker.internet.email(); + const res = { + getHeader: () => {}, + setHeader: () => {}, + }; // act - await AuthService.register({ username: email, password: 'test' }, fromPartial({ session: {} })); + await AuthService.register({ username: email, password: 'test' }, fromPartial({}), fromPartial(res)); const user = await getUserByEmail(email.toLowerCase().trim(), database); // assert @@ -386,7 +421,7 @@ describe('Register', () => { // Act & Assert await createUser({ email, operator: true }, database); - await expect(AuthService.register({ username: email, password: 'test' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.admin-already-exists'); + await expect(AuthService.register({ username: email, password: 'test' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.admin-already-exists'); }); it('Should throw if user already exists', async () => { @@ -395,23 +430,27 @@ describe('Register', () => { // Act & Assert await createUser({ email, operator: false }, database); - await expect(AuthService.register({ username: email, password: 'test' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.user-already-exists'); + await expect(AuthService.register({ username: email, password: 'test' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.user-already-exists'); }); it('Should throw if email is not provided', async () => { - await expect(AuthService.register({ username: '', password: 'test' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.missing-email-or-password'); + await expect(AuthService.register({ username: '', password: 'test' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.missing-email-or-password'); }); it('Should throw if password is not provided', async () => { - await expect(AuthService.register({ username: faker.internet.email(), password: '' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.missing-email-or-password'); + await expect(AuthService.register({ username: faker.internet.email(), password: '' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.missing-email-or-password'); }); it('Password is correctly hashed', async () => { // arrange const email = faker.internet.email().toLowerCase().trim(); + const res = { + getHeader: () => {}, + setHeader: () => {}, + }; // act - await AuthService.register({ username: email, password: 'test' }, fromPartial({ session: {} })); + await AuthService.register({ username: email, password: 'test' }, fromPartial({}), fromPartial(res)); const user = await getUserByEmail(email, database); const isPasswordValid = await argon2.verify(user?.password || '', 'test'); @@ -420,7 +459,7 @@ describe('Register', () => { }); it('Should throw if email is invalid', async () => { - await expect(AuthService.register({ username: 'test', password: 'test' }, fromPartial({}))).rejects.toThrowError('server-messages.errors.invalid-username'); + await expect(AuthService.register({ username: 'test', password: 'test' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.invalid-username'); }); it('should throw if db fails to insert user', async () => { @@ -431,15 +470,14 @@ describe('Register', () => { const newAuthService = new AuthServiceClass(fromAny(mockDatabase)); // Act & Assert - await expect(newAuthService.register({ username: email, password: 'test' }, fromPartial(req))).rejects.toThrowError('server-messages.errors.error-creating-user'); + await expect(newAuthService.register({ username: email, password: 'test' }, fromPartial(req), fromPartial({}))).rejects.toThrowError('server-messages.errors.error-creating-user'); }); }); describe('Test: logout', () => { it('Should return true if there is no session to delete', async () => { // act - const req = {}; - const result = await AuthServiceClass.logout(fromPartial(req)); + const result = await AuthServiceClass.logout('session'); // assert expect(result).toBe(true); @@ -447,15 +485,17 @@ describe('Test: logout', () => { it('Should destroy session upon logount', async () => { // arrange - const destroy = vi.fn(); - const req = { session: { userId: 1, destroy } }; + const sessionId = v4(); + + await TipiCache.set(`session:${sessionId}`, '1'); // act - const result = await AuthServiceClass.logout(fromPartial(req)); + const result = await AuthServiceClass.logout(sessionId); + const session = await TipiCache.get(`session:${sessionId}`); // assert expect(result).toBe(true); - expect(destroy).toHaveBeenCalled(); + expect(session).toBeUndefined(); }); });