wip league admin tools
This commit is contained in:
154
apps/api/src/domain/league/LeagueRosterAdminRead.http.test.ts
Normal file
154
apps/api/src/domain/league/LeagueRosterAdminRead.http.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
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: any;
|
||||
|
||||
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 any),
|
||||
new AuthorizationGuard(reflector, authorizationService as any),
|
||||
new FeatureAvailabilityGuard(reflector, policyService as any),
|
||||
);
|
||||
|
||||
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: 'pw1', 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 any;
|
||||
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 any[]).length > 0) {
|
||||
const first = (res.body as any[])[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.message !== undefined) {
|
||||
expect(first.message).toEqual(expect.any(String));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user