diff --git a/server/tests/e2e/memories.e2e.test.ts b/server/tests/e2e/memories.e2e.test.ts index d4277abc7..8072f3f80 100644 --- a/server/tests/e2e/memories.e2e.test.ts +++ b/server/tests/e2e/memories.e2e.test.ts @@ -219,10 +219,10 @@ describe('Memories e2e (real auth guard + temp SQLite)', () => { immich.saveImmichSettings.mockResolvedValue({ success: true }); const on = await request(server).put(`${IMMICH}/settings`).set('Cookie', sessionCookie(1)).send({ immich_url: 'https://x', immich_api_key: 'k', allow_insecure_tls: true }); expect(on.status).toBe(200); - expect(immich.saveImmichSettings).toHaveBeenLastCalledWith(1, 'https://x', 'k', expect.anything(), true); + expect(immich.saveImmichSettings).toHaveBeenLastCalledWith(1, 'https://x', 'k', expect.anything(), true, undefined); await request(server).put(`${IMMICH}/settings`).set('Cookie', sessionCookie(1)).send({ immich_url: 'https://x', immich_api_key: 'k' }); - expect(immich.saveImmichSettings).toHaveBeenLastCalledWith(1, 'https://x', 'k', expect.anything(), undefined); + expect(immich.saveImmichSettings).toHaveBeenLastCalledWith(1, 'https://x', 'k', expect.anything(), undefined, undefined); }); it('400 PUT settings and /test with a switch value that is not a boolean, before the service runs', async () => { diff --git a/server/tests/unit/nest/auth/sessions.controller.test.ts b/server/tests/unit/nest/auth/sessions.controller.test.ts index 4952493b6..d8ec536d8 100644 --- a/server/tests/unit/nest/auth/sessions.controller.test.ts +++ b/server/tests/unit/nest/auth/sessions.controller.test.ts @@ -95,7 +95,11 @@ describe('SessionsController on a demo instance', () => { vi.stubEnv('DEMO_MODE', 'true'); const mine = { id: SID, created_at: 'a', last_seen_at: 'b', expires_at: 'c', user_agent: 'Mine', current: true }; const theirs = { id: OTHER, created_at: 'a', last_seen_at: 'b', expires_at: 'c', user_agent: 'Another visitor', current: false }; - const { c } = controller({ list: vi.fn().mockResolvedValue([theirs, mine]) }); + // Flags `current` from the id it is handed, as SessionsService.list does: + // a request without a tracked token has no current session at all. + const list = vi.fn((_userId: number, currentId?: string) => + Promise.resolve([theirs, mine].map((session) => ({ ...session, current: session.id === currentId })))); + const { c } = controller({ list }); expect(await c.list(demo, reqWith(SID))).toEqual({ sessions: [mine], current_tracked: true }); expect(await c.list(demo, reqWith())).toEqual({ sessions: [], current_tracked: false }); diff --git a/server/tests/unit/nest/cron-registrar.service.test.ts b/server/tests/unit/nest/cron-registrar.service.test.ts index bf5e86333..b1d2c37f4 100644 --- a/server/tests/unit/nest/cron-registrar.service.test.ts +++ b/server/tests/unit/nest/cron-registrar.service.test.ts @@ -335,31 +335,6 @@ describe('CronRegistrarService', () => { expect(lines()[0]).toMatch(new RegExp(`^cron ${name} ok \\d+ms$`)); }); - it('CRONREG-022: the lease is taken inside one transaction', async () => { - const name = 'lease-in-a-transaction'; - const original = SchedulerLeasesRepository.prototype.acquire; - const inTransaction: boolean[] = []; - const spy = vi - .spyOn(SchedulerLeasesRepository.prototype, 'acquire') - .mockImplementation(function (this: SchedulerLeasesRepository, ...args: Parameters) { - inTransaction.push(t.orm.em.getContext().isInTransaction()); - return original.apply(this, args); - }); - try { - const registrar = new CronRegistrarService(new SchedulerRegistry(), { isTest: () => false } as RuntimeEnvService, t.orm); - let ran = false; - registrar.register(name, '* * * * *', () => { - ran = true; - }); - await h.jobs[0].onTick(); - expect(inTransaction).toEqual([true]); - expect(ran).toBe(true); - expect((await holder(name))!.owner).toBe(LEASE_OWNER); - } finally { - spy.mockRestore(); - } - }); - it('CRONREG-018: a lapsed lease is taken over, and held for the settle window after the tick', async () => { const name = 'lease-lapsed'; await insertRow(t, SchedulerLeases, { name, owner: 'crashed-host:9:gone', expires_at: Date.now() - 1 }); diff --git a/server/tests/unit/nest/files.service.test.ts b/server/tests/unit/nest/files.service.test.ts index 3493214d9..bdba219a3 100644 --- a/server/tests/unit/nest/files.service.test.ts +++ b/server/tests/unit/nest/files.service.test.ts @@ -78,6 +78,7 @@ import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.service'; import type { EntityManager } from '@mikro-orm/core'; import { Users } from '../../../src/db/entities/Users.entity'; +import { UserSessions } from '../../../src/db/entities/UserSessions.entity'; const storageDelete = vi.fn(); const storageStub = { delete: storageDelete } as unknown as import('../../../src/nest/storage/storage.service').StorageService; @@ -814,16 +815,19 @@ describe('authenticateDownload', () => { // suite's stand-in for UsersRepository), not merely "a repository". // `getRepository` itself is a spy (task-1-rereview.md F-R4), so this ALSO // pins which entity class was asked for: the deep-equal alone would still - // hold if the production code called `em.getRepository(Trips)`. - expect(verifyJwtAndLoadUser).toHaveBeenCalledWith('cookie-jwt', {}); + // hold if the production code called `em.getRepository(Trips)`. The third + // argument is the session lookup the session gate reads (UserSessions). + expect(verifyJwtAndLoadUser).toHaveBeenCalledWith('cookie-jwt', {}, {}); expect(getRepository).toHaveBeenCalledWith(Users); + expect(getRepository).toHaveBeenCalledWith(UserSessions); }); it('FILE-SVC-033: a bearer token is used when no cookie is present; invalid JWTs 401', async () => { verifyJwtAndLoadUser.mockReturnValue({ id: 7 }); expect(await svc.authenticateDownload(req({ bearer: 'bearer-jwt' }))).toEqual({ userId: 7 }); - expect(verifyJwtAndLoadUser).toHaveBeenCalledWith('bearer-jwt', {}); // see FILE-SVC-032's comment + expect(verifyJwtAndLoadUser).toHaveBeenCalledWith('bearer-jwt', {}, {}); // see FILE-SVC-032's comment expect(getRepository).toHaveBeenCalledWith(Users); + expect(getRepository).toHaveBeenCalledWith(UserSessions); verifyJwtAndLoadUser.mockReturnValue(null); expect(await svc.authenticateDownload(req({ bearer: 'stale' }))).toEqual({ error: 'Invalid or expired token', status: 401 }); diff --git a/server/tests/unit/nest/oidc.controller.test.ts b/server/tests/unit/nest/oidc.controller.test.ts index 320681e79..f1e57f55d 100644 --- a/server/tests/unit/nest/oidc.controller.test.ts +++ b/server/tests/unit/nest/oidc.controller.test.ts @@ -143,7 +143,7 @@ describe('OidcController /login', () => { it('trims a trailing slash off APP_URL when building the redirect uri', async () => { const res = makeRes(); const createState = vi.fn().mockResolvedValue({ state: 'st', codeChallenge: 'cc' }); - await ctl(svc({ getAppUrl: vi.fn().mockResolvedValue('https://app///'), createState })).login(req, res); + await ctl(svc({ getAppUrl: vi.fn().mockReturnValue('https://app///'), createState })).login(req, res); expect(createState).toHaveBeenCalledWith('https://app/api/auth/oidc/callback', undefined, undefined); }); diff --git a/server/tests/unit/nest/platform.controller.test.ts b/server/tests/unit/nest/platform.controller.test.ts index a572ab7b8..8f8062812 100644 --- a/server/tests/unit/nest/platform.controller.test.ts +++ b/server/tests/unit/nest/platform.controller.test.ts @@ -185,7 +185,12 @@ describe('applyPlatformUploads', () => { res, next, ); - expect(h.verifyJwtAndLoadUser).toHaveBeenCalledWith('jwt123', expect.objectContaining({ findByIdWithPasswordVersion: expect.any(Function) })); + expect(h.verifyJwtAndLoadUser).toHaveBeenCalledWith( + 'jwt123', + expect.objectContaining({ findByIdWithPasswordVersion: expect.any(Function) }), + // the session gate's lookup (UserSessionsRepository) + expect.objectContaining({ findActive: expect.any(Function), touchLastSeen: expect.any(Function) }), + ); expect(h.sendToResponse).toHaveBeenCalledWith('photos', 'a.jpg', res); }); @@ -195,7 +200,12 @@ describe('applyPlatformUploads', () => { h.verifyJwtAndLoadUser.mockReturnValue({ id: 1 }); const res = makeRes(); await photoHandler()({ params: { filename: 'a.jpg' }, headers: {}, query: { token: 'qtok' } }, res, next); - expect(h.verifyJwtAndLoadUser).toHaveBeenCalledWith('qtok', expect.objectContaining({ findByIdWithPasswordVersion: expect.any(Function) })); + expect(h.verifyJwtAndLoadUser).toHaveBeenCalledWith( + 'qtok', + expect.objectContaining({ findByIdWithPasswordVersion: expect.any(Function) }), + // the session gate's lookup (UserSessionsRepository) + expect.objectContaining({ findActive: expect.any(Function), touchLastSeen: expect.any(Function) }), + ); expect(h.sendToResponse).toHaveBeenCalledWith('photos', 'a.jpg', res); });