import { AdminSessionService } from './admin-session.service'; import type { IamService } from '../iam/iam.service'; /** * Revocable admin sessions. * * Two contracts are locked down here. First, with tracking off nothing changes: * no `jti`, no row, and no extra query on an authenticated request. Second, with * it on, a revoked session stops working immediately — which is the whole point, * because before this table an issued token was usable for its full 12h no matter * what happened centrally. */ describe('AdminSessionService', () => { const ORIGINAL_ENV = process.env; const build = (opts: { iamEnabled?: boolean; tracking?: string | null } = {}) => { process.env = { ...ORIGINAL_ENV }; delete process.env.AUTH_SESSION_TRACKING; if (opts.tracking != null) process.env.AUTH_SESSION_TRACKING = opts.tracking; const prisma = { adminSession: { create: jest.fn().mockResolvedValue({}), findUnique: jest.fn(), updateMany: jest.fn().mockResolvedValue({ count: 2 }), }, }; const iam = { enabled: opts.iamEnabled ?? false } as IamService; return { service: new AdminSessionService(prisma as never, iam), prisma }; }; afterEach(() => { process.env = ORIGINAL_ENV; }); /* ─────────────── off by default ─────────────── */ it('is off in a deployment that does not use the identity service', () => { expect(build().service.enabled).toBe(false); }); it('turns itself on with the identity service, so central revocation reaches us', () => { expect(build({ iamEnabled: true }).service.enabled).toBe(true); }); it('can be turned on on its own', () => { expect(build({ tracking: 'true' }).service.enabled).toBe(true); }); it('can be turned off explicitly even with the identity service on', () => { // Opting out of the per-request lookup; central revocation then cannot reach here expect(build({ iamEnabled: true, tracking: 'false' }).service.enabled).toBe(false); }); /* ─────────────── unchanged behaviour while off ─────────────── */ it('issues no token id and writes no row while off', async () => { const { service, prisma } = build(); expect(service.newTokenId()).toBeNull(); await service.record({ jti: 'x', adminId: 'a' }); expect(prisma.adminSession.create).not.toHaveBeenCalled(); }); it('authenticates without a database lookup while off', async () => { const { service, prisma } = build(); await expect(service.isActive('some-jti')).resolves.toBe(true); expect(prisma.adminSession.findUnique).not.toHaveBeenCalled(); }); /* ─────────────── enforcement while on ─────────────── */ it('accepts a live session', async () => { const { service, prisma } = build({ iamEnabled: true }); prisma.adminSession.findUnique.mockResolvedValue({ revokedAt: null }); await expect(service.isActive('jti-1')).resolves.toBe(true); }); it('refuses a revoked session', async () => { const { service, prisma } = build({ iamEnabled: true }); prisma.adminSession.findUnique.mockResolvedValue({ revokedAt: new Date() }); // The gap this closes: previously the token kept working until it expired await expect(service.isActive('jti-1')).resolves.toBe(false); }); it('refuses a token whose session row is gone', async () => { const { service, prisma } = build({ iamEnabled: true }); prisma.adminSession.findUnique.mockResolvedValue(null); await expect(service.isActive('jti-1')).resolves.toBe(false); }); it('keeps tokens issued before tracking was switched on working', async () => { const { service, prisma } = build({ iamEnabled: true }); // Such a token carries no jti; refusing it would sign every admin out at once await expect(service.isActive(undefined)).resolves.toBe(true); expect(prisma.adminSession.findUnique).not.toHaveBeenCalled(); }); /* ─────────────── recording and revoking ─────────────── */ it('records the session with an expiry derived from the token lifetime', async () => { process.env.JWT_EXPIRES_IN = '12h'; const { service, prisma } = build({ iamEnabled: true }); process.env.JWT_EXPIRES_IN = '12h'; await service.record({ jti: 'jti-1', adminId: 'admin-1', iamSessionId: 'sid-1', iamSub: 'sub-1' }); const data = prisma.adminSession.create.mock.calls[0][0].data; expect(data).toMatchObject({ jti: 'jti-1', adminId: 'admin-1', iamSessionId: 'sid-1' }); expect(data.expiresAt.getTime()).toBeGreaterThan(Date.now() + 11 * 3600 * 1000); }); it('never breaks login when the session row cannot be written', async () => { const { service, prisma } = build({ iamEnabled: true }); prisma.adminSession.create.mockRejectedValue(new Error('unique violation')); // Worst case is a session that cannot be revoked early — not a failed login await expect(service.record({ jti: 'jti-1', adminId: 'a' })).resolves.toBeUndefined(); }); it('revokes by the identity service session id', async () => { const { service, prisma } = build({ iamEnabled: true }); await expect(service.revokeByIamSession('sid-1')).resolves.toBe(2); expect(prisma.adminSession.updateMany).toHaveBeenCalledWith({ where: { iamSessionId: 'sid-1', revokedAt: null }, data: expect.objectContaining({ revokedReason: 'iam_backchannel_logout' }), }); }); it('revokes every session of an identity when the token names only a subject', async () => { const { service, prisma } = build({ iamEnabled: true }); await service.revokeByIamSubject('sub-1'); expect(prisma.adminSession.updateMany).toHaveBeenCalledWith({ where: { iamSub: 'sub-1', revokedAt: null }, data: expect.objectContaining({ revokedReason: 'iam_backchannel_logout' }), }); }); it('revokes every session of a local admin', async () => { const { service, prisma } = build({ iamEnabled: true }); await service.revokeAllForAdmin('admin-1', 'account_disabled'); expect(prisma.adminSession.updateMany).toHaveBeenCalledWith({ where: { adminId: 'admin-1', revokedAt: null }, data: expect.objectContaining({ revokedReason: 'account_disabled' }), }); }); it('does not touch anything while off', async () => { const { service, prisma } = build(); await expect(service.revokeByIamSession('sid-1')).resolves.toBe(0); expect(prisma.adminSession.updateMany).not.toHaveBeenCalled(); }); });