Files
gridpilot.gg/apps/api/src/domain/league/LeagueRosterJoinRequests.mutations.http.test.ts
2025-12-28 12:04:12 +01:00

269 lines
8.0 KiB
TypeScript

import 'reflect-metadata';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import request from 'supertest';
import { ValidationPipe } from '@nestjs/common';
import { LeagueModule } from './LeagueModule';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { requestContextMiddleware } from '@adapters/http/RequestContext';
import {
DRIVER_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
} from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
import { League } from '@core/racing/domain/entities/League';
import { Driver } from '@core/racing/domain/entities/Driver';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
describe('League roster join request mutations (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(() => []),
};
async function seedLeagueWithJoinRequest(params: {
leagueId: string;
adminId: string;
requesterId: string;
joinRequestId: string;
maxDrivers?: number;
extraActiveMemberId?: string;
}): Promise<void> {
const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN);
const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN);
const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN);
await leagueRepo.create(
League.create({
id: params.leagueId,
name: 'Test League',
description: 'Test league',
ownerId: params.adminId,
settings: { visibility: 'unranked', ...(params.maxDrivers !== undefined ? { maxDrivers: params.maxDrivers } : {}) },
}),
);
await driverRepo.create(
Driver.create({
id: params.adminId,
iracingId: '1001',
name: 'Admin Driver',
country: 'DE',
}),
);
await driverRepo.create(
Driver.create({
id: params.requesterId,
iracingId: '1002',
name: 'Requester Driver',
country: 'DE',
}),
);
await membershipRepo.saveMembership(
LeagueMembership.create({
leagueId: params.leagueId,
driverId: params.adminId,
role: 'admin',
status: 'active',
}),
);
if (params.extraActiveMemberId) {
await driverRepo.create(
Driver.create({
id: params.extraActiveMemberId,
iracingId: '1003',
name: 'Extra Member',
country: 'DE',
}),
);
await membershipRepo.saveMembership(
LeagueMembership.create({
leagueId: params.leagueId,
driverId: params.extraActiveMemberId,
role: 'member',
status: 'active',
}),
);
}
await membershipRepo.saveJoinRequest(
JoinRequest.create({
id: params.joinRequestId,
leagueId: params.leagueId,
driverId: params.requesterId,
requestedAt: new Date('2025-01-01T12:00:00Z'),
message: 'please',
}),
);
}
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [LeagueModule],
}).compile();
app = module.createNestApplication();
// Required for getActorFromRequestContext() used by requireLeagueAdminOrOwner().
app.use(requestContextMiddleware as any);
// Test-only auth injection: emulate an authenticated session by setting request.user.
app.use((req: any, _res: any, next: any) => {
const userId = req.headers['x-test-user-id'];
if (typeof userId === 'string' && userId.length > 0) {
req.user = { userId };
}
next();
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
const reflector = new Reflector();
app.useGlobalGuards(
new AuthenticationGuard(sessionPort as any),
new AuthorizationGuard(reflector, authorizationService as any),
);
await app.init();
});
afterEach(async () => {
await app?.close();
vi.clearAllMocks();
});
it('returns 401 when unauthenticated', async () => {
await seedLeagueWithJoinRequest({
leagueId: 'league-1',
adminId: 'admin-1',
requesterId: 'driver-2',
joinRequestId: 'jr-1',
});
await request(app.getHttpServer())
.post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
.expect(401);
});
it('returns 403 when authenticated but not admin/owner', async () => {
await seedLeagueWithJoinRequest({
leagueId: 'league-1',
adminId: 'admin-1',
requesterId: 'driver-2',
joinRequestId: 'jr-1',
});
await request(app.getHttpServer())
.post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
.set('x-test-user-id', 'user-2')
.expect(403);
});
it('approve removes request and adds member; roster reads reflect changes', async () => {
await seedLeagueWithJoinRequest({
leagueId: 'league-1',
adminId: 'admin-1',
requesterId: 'driver-2',
joinRequestId: 'jr-1',
});
await request(app.getHttpServer())
.post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
.set('x-test-user-id', 'admin-1')
.expect(200);
const joinRequests = await request(app.getHttpServer())
.get('/leagues/league-1/admin/roster/join-requests')
.set('x-test-user-id', 'admin-1')
.expect(200);
expect(Array.isArray(joinRequests.body)).toBe(true);
expect(joinRequests.body.find((r: any) => r.id === 'jr-1')).toBeUndefined();
const members = await request(app.getHttpServer())
.get('/leagues/league-1/admin/roster/members')
.set('x-test-user-id', 'admin-1')
.expect(200);
expect(Array.isArray(members.body)).toBe(true);
expect(members.body.some((m: any) => m.driverId === 'driver-2')).toBe(true);
});
it('reject removes request only; roster reads reflect changes', async () => {
await seedLeagueWithJoinRequest({
leagueId: 'league-1',
adminId: 'admin-1',
requesterId: 'driver-2',
joinRequestId: 'jr-1',
});
await request(app.getHttpServer())
.post('/leagues/league-1/admin/roster/join-requests/jr-1/reject')
.set('x-test-user-id', 'admin-1')
.expect(200);
const joinRequests = await request(app.getHttpServer())
.get('/leagues/league-1/admin/roster/join-requests')
.set('x-test-user-id', 'admin-1')
.expect(200);
expect(Array.isArray(joinRequests.body)).toBe(true);
expect(joinRequests.body.find((r: any) => r.id === 'jr-1')).toBeUndefined();
const members = await request(app.getHttpServer())
.get('/leagues/league-1/admin/roster/members')
.set('x-test-user-id', 'admin-1')
.expect(200);
expect(Array.isArray(members.body)).toBe(true);
expect(members.body.some((m: any) => m.driverId === 'driver-2')).toBe(false);
});
it('approve returns error when league is full and keeps request pending', async () => {
await seedLeagueWithJoinRequest({
leagueId: 'league-1',
adminId: 'admin-1',
requesterId: 'driver-2',
joinRequestId: 'jr-1',
maxDrivers: 2,
extraActiveMemberId: 'driver-3',
});
await request(app.getHttpServer())
.post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
.set('x-test-user-id', 'admin-1')
.expect(409);
const joinRequests = await request(app.getHttpServer())
.get('/leagues/league-1/admin/roster/join-requests')
.set('x-test-user-id', 'admin-1')
.expect(200);
expect(Array.isArray(joinRequests.body)).toBe(true);
expect(joinRequests.body.find((r: any) => r.id === 'jr-1')).toBeDefined();
});
});