fix: address penalty catalog review findings

This commit is contained in:
Bastian Wagner
2026-08-01 14:46:58 +02:00
parent fa05e55d43
commit c7dfdd7498
7 changed files with 149 additions and 23 deletions

View File

@@ -26,6 +26,11 @@ type AuthenticatedRequest = { user: { id: number } };
export class PenaltyController {
constructor(private readonly service: PenaltyService) {}
@Get()
getUserPenalties(@Request() request: AuthenticatedRequest) {
return this.service.getUserPenalties(request.user.id);
}
@Get(':teamId')
getTeamPenalties(
@Request() request: AuthenticatedRequest,

View File

@@ -20,6 +20,7 @@ describe('penalty catalog HTTP boundary', () => {
createdAt: '2026-01-02T00:00:00.000Z',
};
const service = {
getUserPenalties: jest.fn(() => [penalty]),
getTeamPenalties: jest.fn(() => [penalty]),
createPenalty: jest.fn(() => penalty),
updatePenalty: jest.fn(() => penalty),
@@ -62,6 +63,15 @@ describe('penalty catalog HTTP boundary', () => {
expect(service.getTeamPenalties).toHaveBeenCalledWith(42, 5);
});
it('keeps the authenticated cross-team catalog route available', async () => {
await request(app.getHttpServer()).get('/api/v1/penalty').expect(401);
await request(app.getHttpServer())
.get('/api/v1/penalty')
.set('Authorization', 'Bearer user')
.expect(200, [penalty]);
expect(service.getUserPenalties).toHaveBeenCalledWith(42);
});
it('validates and strips create fields before invoking the service', async () => {
await request(app.getHttpServer())
.post('/api/v1/penalty')

View File

@@ -55,6 +55,33 @@ describe('PenaltyService catalog management', () => {
expect(access.assertMember).not.toHaveBeenCalled();
});
it('keeps the authenticated cross-team catalog route compatible and safely mapped', async () => {
readRepository.find.mockResolvedValue([
{
id: 8,
description: 'Zu spät',
amount: '5.50',
createdAt: new Date('2026-01-02T00:00:00.000Z'),
team: { id: 5, secret: 'hidden' },
},
]);
await expect(service.getUserPenalties(42)).resolves.toEqual([
{
id: 8,
description: 'Zu spät',
amount: 5.5,
createdAt: new Date('2026-01-02T00:00:00.000Z'),
},
]);
expect(readRepository.find).toHaveBeenCalledWith({
where: {
team: { players: { user: { id: 42 }, active: true } },
},
order: { description: 'ASC' },
});
});
it('authorizes team reads, sorts them, and maps only safe fields', async () => {
readRepository.find.mockResolvedValue([
{
@@ -136,11 +163,7 @@ describe('PenaltyService catalog management', () => {
writeRepository.save.mockImplementation(async (value) => value);
await expect(
service.updatePenalty(
8,
{ description: ' Neu ', amount: 2.5 },
42,
),
service.updatePenalty(8, { description: ' Neu ', amount: 2.5 }, 42),
).resolves.toMatchObject({ id: 8, description: 'Neu', amount: 2.5 });
expect(duplicateQuery.andWhere).toHaveBeenCalledWith(
'penalty.id != :penaltyId',
@@ -194,10 +217,7 @@ describe('PenaltyService catalog management', () => {
logger.info.mockRejectedValue(new Error('audit unavailable'));
await expect(
service.createPenalty(
{ teamId: 5, description: 'Neu', amount: 2 },
42,
),
service.createPenalty({ teamId: 5, description: 'Neu', amount: 2 }, 42),
).rejects.toThrow('audit unavailable');
});
});

View File

@@ -25,6 +25,16 @@ export class PenaltyService {
private readonly logger: LoggingService,
) {}
async getUserPenalties(userId: number): Promise<PenaltyResponseDTO[]> {
const penalties = await this.repository.find({
where: {
team: { players: { user: { id: userId }, active: true } },
},
order: { description: 'ASC' },
});
return penalties.map((penalty) => this.toResponse(penalty));
}
async getTeamPenalties(
userId: number,
teamId: number,
@@ -133,7 +143,10 @@ export class PenaltyService {
return penalty;
}
private async lockTeam(manager: EntityManager, teamId: number): Promise<Team> {
private async lockTeam(
manager: EntityManager,
teamId: number,
): Promise<Team> {
const team = await manager
.getRepository(Team)
.createQueryBuilder('team')