import 'reflect-metadata'; import { Controller, Get } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthenticationGuard } from './AuthenticationGuard'; import { AuthorizationGuard } from './AuthorizationGuard'; import { AuthorizationService } from './AuthorizationService'; import { Public } from './Public'; import { RequireRoles } from './RequireRoles'; import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard'; import { PolicyService, type PolicySnapshot } from '../policy/PolicyService'; import { RequireCapability } from '../policy/RequireCapability'; @Controller('authz-test') class AuthzTestController { @Public() @Get('public') publicRoute() { return { ok: true }; } @Get('protected') protectedRoute() { return { ok: true }; } @RequireRoles('admin') @Get('admin') adminOnlyRoute() { return { ok: true }; } @RequireCapability('demo.feature', 'view') @Get('feature') featureGatedRoute() { return { ok: true }; } } type SessionPort = { getCurrentSession: () => Promise; }; describe('Auth guards (HTTP)', () => { let app: any; const sessionPort: SessionPort = { getCurrentSession: vi.fn(async () => null), }; const authorizationService: Pick = { getRolesForUser: vi.fn(() => []), }; const policyService: Pick = { getSnapshot: vi.fn(async (): Promise => ({ policyVersion: 1, operationalMode: 'normal', maintenanceAllowlist: { view: [], mutate: [] }, capabilities: { 'demo.feature': 'enabled' }, loadedFrom: 'defaults', loadedAtIso: new Date(0).toISOString(), })), }; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [AuthzTestController], }).compile(); app = module.createNestApplication(); const reflector = new Reflector(); app.useGlobalGuards( new AuthenticationGuard(sessionPort as any), new AuthorizationGuard(reflector, authorizationService as any), new FeatureAvailabilityGuard(reflector, policyService as any), ); await app.init(); }); afterEach(async () => { await app?.close(); vi.clearAllMocks(); }); it('allows `@Public()` routes without a session', async () => { await request(app.getHttpServer()).get('/authz-test/public').expect(200).expect({ ok: true }); expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1); }); it('denies non-public routes by default when not authenticated (401)', async () => { await request(app.getHttpServer()).get('/authz-test/protected').expect(401); }); it('allows non-public routes when authenticated via session port', async () => { vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({ token: 't', user: { id: 'user-1' }, }); await request(app.getHttpServer()).get('/authz-test/protected').expect(200).expect({ ok: true }); }); it('returns 403 when route requires a role and the user does not have it', async () => { vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({ token: 't', user: { id: 'user-1' }, }); vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['user']); await request(app.getHttpServer()).get('/authz-test/admin').expect(403); }); it('allows access when route requires a role and the user has it', async () => { vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({ token: 't', user: { id: 'user-1' }, }); vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']); await request(app.getHttpServer()).get('/authz-test/admin').expect(200).expect({ ok: true }); }); it('returns 404 when a `@RequireCapability()` feature is disabled', async () => { vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({ token: 't', user: { id: 'user-1' }, }); vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({ policyVersion: 1, operationalMode: 'normal', maintenanceAllowlist: { view: [], mutate: [] }, capabilities: { 'demo.feature': 'disabled' }, loadedFrom: 'env', loadedAtIso: new Date(0).toISOString(), }); await request(app.getHttpServer()).get('/authz-test/feature').expect(404); }); it('returns 503 during maintenance when capability is not allowlisted', async () => { vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({ token: 't', user: { id: 'user-1' }, }); vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({ policyVersion: 1, operationalMode: 'maintenance', maintenanceAllowlist: { view: [], mutate: [] }, capabilities: { 'demo.feature': 'enabled' }, loadedFrom: 'env', loadedAtIso: new Date(0).toISOString(), }); await request(app.getHttpServer()).get('/authz-test/feature').expect(503); }); });