authentication authorization
This commit is contained in:
164
apps/api/src/domain/auth/AuthGuards.http.test.ts
Normal file
164
apps/api/src/domain/auth/AuthGuards.http.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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<null | { token: string; user: { id: string } }>;
|
||||
};
|
||||
|
||||
describe('Auth guards (HTTP)', () => {
|
||||
let app: any;
|
||||
|
||||
const sessionPort: SessionPort = {
|
||||
getCurrentSession: vi.fn(async () => null),
|
||||
};
|
||||
|
||||
const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
|
||||
getRolesForUser: vi.fn(() => []),
|
||||
};
|
||||
|
||||
const policyService: Pick<PolicyService, 'getSnapshot'> = {
|
||||
getSnapshot: vi.fn(async (): Promise<PolicySnapshot> => ({
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user