wip league admin tools
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user