import 'reflect-metadata'; import { ValidationPipe } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { requestContextMiddleware } from '@adapters/http/RequestContext'; import { AuthenticationGuard } from '../auth/AuthenticationGuard'; import { AuthorizationGuard } from '../auth/AuthorizationGuard'; import { IDENTITY_SESSION_PORT_TOKEN } from '../auth/AuthProviders'; import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard'; describe('League roster admin read (HTTP, league-scoped)', () => { const originalEnv = { ...process.env }; let app: import("@nestjs/common").INestApplication; beforeAll(async () => { vi.resetModules(); process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory'; process.env.GRIDPILOT_API_BOOTSTRAP = 'true'; delete process.env.DATABASE_URL; const { AppModule } = await import('../../app.module'); const module = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = module.createNestApplication(); // Ensure AsyncLocalStorage request context is present for getActorFromRequestContext() app.use(requestContextMiddleware); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); const reflector = new Reflector(); const sessionPort = module.get(IDENTITY_SESSION_PORT_TOKEN); const authorizationService = { getRolesForUser: () => [], }; const policyService = { getSnapshot: async () => ({ policyVersion: 1, operationalMode: 'normal', maintenanceAllowlist: { view: [], mutate: [] }, capabilities: {}, loadedFrom: 'defaults', loadedAtIso: new Date(0).toISOString(), }), }; app.useGlobalGuards( new AuthenticationGuard(sessionPort as never), new AuthorizationGuard(reflector, authorizationService as never), new FeatureAvailabilityGuard(reflector, policyService as never), ); await app.init(); }, 20_000); afterAll(async () => { await app?.close(); process.env = originalEnv; vi.restoreAllMocks(); }); it('rejects unauthenticated actor (401)', async () => { await request(app.getHttpServer()).get('/leagues/league-5/admin/roster/members').expect(401); await request(app.getHttpServer()).get('/leagues/league-5/admin/roster/join-requests').expect(401); }); it('rejects authenticated non-admin actor (403)', async () => { const agent = request.agent(app.getHttpServer()); await agent .post('/auth/signup') .send({ email: 'roster-read-user@gridpilot.local', password: 'Password123!', displayName: 'Roster Read User' }) .expect(201); await agent.get('/leagues/league-5/admin/roster/members').expect(403); await agent.get('/leagues/league-5/admin/roster/join-requests').expect(403); }); it('returns roster members with stable fields (happy path)', async () => { const agent = request.agent(app.getHttpServer()); await agent.post('/auth/login').send({ email: 'admin@gridpilot.local', password: 'admin123' }).expect(201); const res = await agent.get('/leagues/league-5/admin/roster/members').expect(200); expect(res.body).toEqual(expect.any(Array)); expect(res.body.length).toBeGreaterThan(0); const first = res.body[0] as unknown as { driver: { id: string, name: string, country: string }, role: string }; expect(first).toMatchObject({ driverId: expect.any(String), role: expect.any(String), joinedAt: expect.any(String), driver: expect.any(Object), }); expect(first.driver).toMatchObject({ id: expect.any(String), name: expect.any(String), country: expect.any(String), }); expect(['owner', 'admin', 'steward', 'member']).toContain(first.role); }); it('returns join requests with stable fields (happy path)', async () => { const adminAgent = request.agent(app.getHttpServer()); await adminAgent.post('/auth/login').send({ email: 'admin@gridpilot.local', password: 'admin123' }).expect(201); const res = await adminAgent.get('/leagues/league-5/admin/roster/join-requests').expect(200); expect(res.body).toEqual(expect.any(Array)); // Seed data may or may not include join requests for a given league. // Validate shape on first item if present. if ((res.body as never[]).length > 0) { const first = (res.body as unknown as { message?: string }[])[0]; expect(first).toMatchObject({ id: expect.any(String), leagueId: expect.any(String), driverId: expect.any(String), requestedAt: expect.any(String), driver: { id: expect.any(String), name: expect.any(String), }, }); if (first) { if (first.message !== undefined) { expect(first.message).toEqual(expect.any(String)); } } } }); });