authentication authorization

This commit is contained in:
2025-12-26 15:32:22 +01:00
parent 68ae9da22a
commit 64377de548
54 changed files with 2833 additions and 95 deletions

View File

@@ -1,14 +1,23 @@
import 'reflect-metadata';
import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { vi } from 'vitest';
import { SponsorController } from './SponsorController';
import { SponsorService } from './SponsorService';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';
describe('SponsorController', () => {
let controller: SponsorController;
let sponsorService: ReturnType<typeof vi.mocked<SponsorService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
const moduleBuilder = Test.createTestingModule({
controllers: [SponsorController],
providers: [
{
@@ -31,7 +40,15 @@ describe('SponsorController', () => {
},
},
],
}).compile();
})
.overrideGuard(AuthenticationGuard)
.useValue({ canActivate: vi.fn().mockResolvedValue(true) })
.overrideGuard(AuthorizationGuard)
.useValue({ canActivate: vi.fn().mockResolvedValue(true) })
.overrideGuard(FeatureAvailabilityGuard)
.useValue({ canActivate: vi.fn().mockResolvedValue(true) });
const module: TestingModule = await moduleBuilder.compile();
controller = module.get<SponsorController>(SponsorController);
sponsorService = vi.mocked(module.get(SponsorService));
@@ -309,4 +326,112 @@ describe('SponsorController', () => {
expect(sponsorService.updateSponsorSettings).toHaveBeenCalledWith(sponsorId, input);
});
});
describe('auth guards (HTTP)', () => {
let app: any;
const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
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: { 'sponsors.portal': 'enabled' },
loadedFrom: 'defaults',
loadedAtIso: new Date(0).toISOString(),
})),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [SponsorController],
providers: [
{
provide: SponsorService,
useValue: {
getEntitySponsorshipPricing: vi.fn(async () => ({ entityType: 'season', entityId: 's1', pricing: [] })),
getSponsors: vi.fn(async () => ({ sponsors: [] })),
},
},
],
}).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() endpoint without a session', async () => {
await request(app.getHttpServer()).get('/sponsors/pricing').expect(200);
});
it('denies protected endpoint when not authenticated (401)', async () => {
await request(app.getHttpServer()).get('/sponsors').expect(401);
});
it('returns 403 when authenticated but missing required role', async () => {
vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
token: 't',
user: { id: 'user-1' },
});
vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['user']);
await request(app.getHttpServer()).get('/sponsors').expect(403);
});
it('returns 404 when role is satisfied but capability is disabled', async () => {
vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
token: 't',
user: { id: 'user-1' },
});
vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']);
vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
policyVersion: 1,
operationalMode: 'normal',
maintenanceAllowlist: { view: [], mutate: [] },
capabilities: { 'sponsors.portal': 'disabled' },
loadedFrom: 'defaults',
loadedAtIso: new Date(0).toISOString(),
});
await request(app.getHttpServer()).get('/sponsors').expect(404);
});
it('allows access when role is satisfied and capability is enabled', async () => {
vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
token: 't',
user: { id: 'user-1' },
});
vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']);
vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
policyVersion: 1,
operationalMode: 'normal',
maintenanceAllowlist: { view: [], mutate: [] },
capabilities: { 'sponsors.portal': 'enabled' },
loadedFrom: 'defaults',
loadedAtIso: new Date(0).toISOString(),
});
await request(app.getHttpServer()).get('/sponsors').expect(200);
});
});
});