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 } = { getCurrentSession: vi.fn(async () => null), }; const authorizationService: Pick = { getRolesForUser: vi.fn(() => []), }; async function seedLeagueWithJoinRequest(params: { leagueId: string; adminId: string; requesterId: string; joinRequestId: string; maxDrivers?: number; extraActiveMemberId?: string; }): Promise { 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(); }); });