Files
gridpilot.gg/apps/api/src/domain/league/LeagueRosterAdminRead.http.test.ts
2026-01-16 12:55:48 +01:00

156 lines
5.0 KiB
TypeScript

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));
}
}
}
});
});