import { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import type { Request } from 'express'; import { AuditWriter } from '../audit/audit.service'; import { ADMIN_ROLE, AdminRoleGuard } from './admin-role.guard'; interface SessionStub { user?: { oid: string; tid: string; username: string; displayName: string; amr: readonly string[]; roles: readonly string[]; }; } function makeRequest(opts: { session?: SessionStub; method?: string; originalUrl?: string; }): Request { return { session: opts.session, method: opts.method ?? 'GET', originalUrl: opts.originalUrl ?? '/api/admin/me', } as unknown as Request; } function makeContext(req: Request): ExecutionContext { return { switchToHttp: () => ({ getRequest: () => req as T }), } as unknown as ExecutionContext; } function makeAuditStub(): jest.Mocked> { return { adminAccessDenied: jest.fn().mockResolvedValue(undefined), }; } describe('AdminRoleGuard', () => { it('throws 401 when the request has no session at all', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext(makeRequest({ session: undefined })); await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException); expect(audit.adminAccessDenied).not.toHaveBeenCalled(); }); it('throws 401 when the session exists but no user is bound to it', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext(makeRequest({ session: {} })); await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException); expect(audit.adminAccessDenied).not.toHaveBeenCalled(); }); it('throws 403 + records admin.access_denied when the user has no roles at all', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext( makeRequest({ session: { user: { oid: 'user-oid', tid: 't', username: 'jane@example', displayName: 'Jane', amr: ['pwd'], roles: [], }, }, method: 'GET', originalUrl: '/api/admin/me', }), ); await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException); expect(audit.adminAccessDenied).toHaveBeenCalledWith({ actor: { oid: 'user-oid' }, attemptedRoute: 'GET /api/admin/me', rolesHeld: [], }); }); it('throws 403 + records admin.access_denied when the user has unrelated roles only', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext( makeRequest({ session: { user: { oid: 'user-oid', tid: 't', username: 'jane@example', displayName: 'Jane', amr: ['pwd'], roles: ['editor', 'auditor'], }, }, method: 'POST', originalUrl: '/api/admin/cms/pages', }), ); await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException); expect(audit.adminAccessDenied).toHaveBeenCalledWith({ actor: { oid: 'user-oid' }, attemptedRoute: 'POST /api/admin/cms/pages', rolesHeld: ['editor', 'auditor'], }); }); it('returns true and does not audit when the user has the admin role', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext( makeRequest({ session: { user: { oid: 'user-oid', tid: 't', username: 'jane@example', displayName: 'Jane', amr: ['pwd', 'mfa'], roles: [ADMIN_ROLE], }, }, }), ); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(audit.adminAccessDenied).not.toHaveBeenCalled(); }); it('returns true when admin is one of several roles', async () => { const audit = makeAuditStub(); const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext( makeRequest({ session: { user: { oid: 'user-oid', tid: 't', username: 'jane@example', displayName: 'Jane', amr: ['pwd', 'mfa'], roles: ['editor', ADMIN_ROLE, 'auditor'], }, }, }), ); await expect(guard.canActivate(ctx)).resolves.toBe(true); }); it('propagates audit write failures (no audit ⇒ no action, per ADR-0013)', async () => { const audit = { adminAccessDenied: jest.fn().mockRejectedValue(new Error('audit_writer denied')), }; const guard = new AdminRoleGuard(audit as unknown as AuditWriter); const ctx = makeContext( makeRequest({ session: { user: { oid: 'user-oid', tid: 't', username: 'jane@example', displayName: 'Jane', amr: ['pwd'], roles: [], }, }, }), ); // The 403 path goes through audit first; if audit throws, the // guard surfaces the underlying error rather than the // ForbiddenException — the caller sees a 500 and the audit // invariant is preserved. await expect(guard.canActivate(ctx)).rejects.toThrow('audit_writer denied'); }); });