269 lines
8.0 KiB
TypeScript
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();
|
|
});
|
|
}); |