Skip to content

Commit 9618fc4

Browse files
authored
Merge pull request #288 from topcoder-platform/develop
[PROD RELEASE] 06/22
2 parents cd5a1f1 + b3ede6f commit 9618fc4

3 files changed

Lines changed: 200 additions & 13 deletions

File tree

src/api/challenge-review-context/challenge-review-context.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class ChallengeReviewContextController {
4242
@ApiOperation({
4343
summary: 'Create a challenge review context',
4444
description:
45-
'Roles: Admin, Copilot | Scopes: create:challenge-review-context. Only allowed for challenges in DRAFT status or REGISTRATION phase. At most one context per challenge.',
45+
'Roles: Admin, Copilot | Scopes: create:challenge-review-context. Allowed for any existing challenge. At most one context per challenge.',
4646
})
4747
@ApiBody({
4848
description: 'Challenge review context to create',
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
jest.mock('../../shared/modules/global/prisma.service', () => ({
2+
PrismaService: class PrismaServiceMock {},
3+
}));
4+
5+
import { ForbiddenException } from '@nestjs/common';
6+
import { ChallengeReviewContextService } from './challenge-review-context.service';
7+
import { ChallengeStatus } from 'src/shared/enums/challengeStatus.enum';
8+
import type { JwtUser } from 'src/shared/modules/global/jwt.service';
9+
import {
10+
CreateChallengeReviewContextDto,
11+
UpdateChallengeReviewContextDto,
12+
ChallengeReviewContextStatus,
13+
} from '../../dto/challengeReviewContext.dto';
14+
15+
describe('ChallengeReviewContextService', () => {
16+
const challengeApiMock = {
17+
getChallengeDetailForUser: jest.fn(),
18+
};
19+
20+
const prismaMock = {
21+
challengeReviewContext: {
22+
findUnique: jest.fn(),
23+
create: jest.fn(),
24+
update: jest.fn(),
25+
},
26+
};
27+
28+
let service: ChallengeReviewContextService;
29+
30+
beforeEach(() => {
31+
jest.resetAllMocks();
32+
service = new ChallengeReviewContextService(
33+
prismaMock as any,
34+
challengeApiMock as any,
35+
);
36+
});
37+
38+
const authUser: JwtUser = { userId: 'user-1', isMachine: false };
39+
40+
it('allows creating a review context for any existing challenge', async () => {
41+
const challenge = {
42+
id: 'challenge-1',
43+
status: ChallengeStatus.COMPLETED,
44+
phases: [],
45+
};
46+
challengeApiMock.getChallengeDetailForUser.mockResolvedValue(challenge);
47+
prismaMock.challengeReviewContext.findUnique.mockResolvedValue(null);
48+
prismaMock.challengeReviewContext.create.mockResolvedValue({
49+
id: 'context-1',
50+
challengeId: 'challenge-1',
51+
context: { summary: 'review context' },
52+
status: ChallengeReviewContextStatus.AI_GENERATED,
53+
createdAt: new Date('2025-01-01T00:00:00.000Z'),
54+
createdBy: 'user-1',
55+
updatedAt: new Date('2025-01-01T00:00:00.000Z'),
56+
updatedBy: 'user-1',
57+
});
58+
59+
const dto: CreateChallengeReviewContextDto = {
60+
challengeId: 'challenge-1',
61+
context: { summary: 'review context' },
62+
status: ChallengeReviewContextStatus.AI_GENERATED,
63+
};
64+
65+
const result = await service.create(dto, authUser);
66+
67+
expect(result.challengeId).toBe('challenge-1');
68+
expect(prismaMock.challengeReviewContext.create).toHaveBeenCalledWith({
69+
data: {
70+
challengeId: 'challenge-1',
71+
context: dto.context,
72+
status: dto.status,
73+
createdBy: authUser.userId?.toString(),
74+
updatedBy: authUser.userId?.toString(),
75+
},
76+
});
77+
});
78+
79+
it('forbids updating a review context when challenge is not DRAFT and has no open registration phase', async () => {
80+
const challenge = {
81+
id: 'challenge-2',
82+
status: ChallengeStatus.COMPLETED,
83+
phases: [{ id: 'phase-1', name: 'Submission', isOpen: false }],
84+
};
85+
challengeApiMock.getChallengeDetailForUser.mockResolvedValue(challenge);
86+
prismaMock.challengeReviewContext.findUnique.mockResolvedValue({
87+
id: 'context-2',
88+
challengeId: 'challenge-2',
89+
context: { summary: 'existing' },
90+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
91+
createdAt: new Date(),
92+
createdBy: 'user-1',
93+
updatedAt: new Date(),
94+
updatedBy: 'user-1',
95+
});
96+
97+
const dto: UpdateChallengeReviewContextDto = {
98+
context: { summary: 'updated context' },
99+
};
100+
101+
await expect(service.update('challenge-2', dto, authUser)).rejects.toThrow(
102+
ForbiddenException,
103+
);
104+
expect(prismaMock.challengeReviewContext.update).not.toHaveBeenCalled();
105+
});
106+
107+
it('allows updating a review context when challenge has an open registration phase', async () => {
108+
const challenge = {
109+
id: 'challenge-3',
110+
status: ChallengeStatus.ACTIVE,
111+
phases: [{ id: 'phase-2', name: 'Registration', isOpen: true }],
112+
};
113+
challengeApiMock.getChallengeDetailForUser.mockResolvedValue(challenge);
114+
prismaMock.challengeReviewContext.findUnique.mockResolvedValue({
115+
id: 'context-3',
116+
challengeId: 'challenge-3',
117+
context: { summary: 'existing' },
118+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
119+
createdAt: new Date(),
120+
createdBy: 'user-1',
121+
updatedAt: new Date(),
122+
updatedBy: 'user-1',
123+
});
124+
prismaMock.challengeReviewContext.update.mockResolvedValue({
125+
id: 'context-3',
126+
challengeId: 'challenge-3',
127+
context: { summary: 'updated context' },
128+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
129+
createdAt: new Date(),
130+
createdBy: 'user-1',
131+
updatedAt: new Date(),
132+
updatedBy: 'user-1',
133+
});
134+
135+
const dto: UpdateChallengeReviewContextDto = {
136+
context: { summary: 'updated context' },
137+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
138+
};
139+
140+
const result = await service.update('challenge-3', dto, authUser);
141+
142+
expect(result.context).toEqual({ summary: 'updated context' });
143+
expect(prismaMock.challengeReviewContext.update).toHaveBeenCalledWith({
144+
where: { challengeId: 'challenge-3' },
145+
data: {
146+
context: dto.context,
147+
status: dto.status,
148+
updatedBy: authUser.userId?.toString(),
149+
},
150+
});
151+
});
152+
153+
it('allows updating a review context when challenge is in DRAFT status', async () => {
154+
const challenge = {
155+
id: 'challenge-4',
156+
status: ChallengeStatus.DRAFT,
157+
phases: [],
158+
};
159+
challengeApiMock.getChallengeDetailForUser.mockResolvedValue(challenge);
160+
prismaMock.challengeReviewContext.findUnique.mockResolvedValue({
161+
id: 'context-4',
162+
challengeId: 'challenge-4',
163+
context: { summary: 'existing' },
164+
status: ChallengeReviewContextStatus.AI_GENERATED,
165+
createdAt: new Date(),
166+
createdBy: 'user-1',
167+
updatedAt: new Date(),
168+
updatedBy: 'user-1',
169+
});
170+
prismaMock.challengeReviewContext.update.mockResolvedValue({
171+
id: 'context-4',
172+
challengeId: 'challenge-4',
173+
context: { summary: 'updated context' },
174+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
175+
createdAt: new Date(),
176+
createdBy: 'user-1',
177+
updatedAt: new Date(),
178+
updatedBy: 'user-1',
179+
});
180+
const dto: UpdateChallengeReviewContextDto = {
181+
context: { summary: 'updated context' },
182+
status: ChallengeReviewContextStatus.HUMAN_APPROVED,
183+
};
184+
const result = await service.update('challenge-4', dto, authUser);
185+
expect(result.context).toEqual({ summary: 'updated context' });
186+
expect(prismaMock.challengeReviewContext.update).toHaveBeenCalledWith({
187+
where: { challengeId: 'challenge-4' },
188+
data: {
189+
context: dto.context,
190+
status: dto.status,
191+
updatedBy: authUser.userId?.toString(),
192+
},
193+
});
194+
});
195+
});

src/api/challenge-review-context/challenge-review-context.service.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class ChallengeReviewContextService {
9292
* @throws ForbiddenException when whitelist or challenge phase rules block writing.
9393
* @throws NotFoundException when the challenge cannot be loaded.
9494
*/
95-
private async validateChallengeAllowedForWrite(
95+
private async validateChallengeAllowedForUpdate(
9696
challengeId: string,
9797
authUser: JwtUser,
9898
loadedChallenge?: ChallengeData,
@@ -107,7 +107,7 @@ export class ChallengeReviewContextService {
107107
) ?? false;
108108
if (!isDraft && !hasRegistrationPhase) {
109109
throw new ForbiddenException(
110-
'Creating or updating challenge review context is only allowed for challenges in DRAFT status or REGISTRATION phase.',
110+
'Updating challenge review context is only allowed for challenges in DRAFT status or REGISTRATION phase.',
111111
);
112112
}
113113
}
@@ -116,15 +116,7 @@ export class ChallengeReviewContextService {
116116
dto: CreateChallengeReviewContextDto,
117117
authUser: JwtUser,
118118
): Promise<ChallengeReviewContextResponseDto> {
119-
const challenge = await this.validateChallengeExists(
120-
dto.challengeId,
121-
authUser,
122-
);
123-
await this.validateChallengeAllowedForWrite(
124-
dto.challengeId,
125-
authUser,
126-
challenge,
127-
);
119+
await this.validateChallengeExists(dto.challengeId, authUser);
128120

129121
const existing = await this.prisma.challengeReviewContext.findUnique({
130122
where: { challengeId: dto.challengeId },
@@ -173,7 +165,7 @@ export class ChallengeReviewContextService {
173165
authUser: JwtUser,
174166
): Promise<ChallengeReviewContextResponseDto> {
175167
const challenge = await this.validateChallengeExists(challengeId, authUser);
176-
await this.validateChallengeAllowedForWrite(
168+
await this.validateChallengeAllowedForUpdate(
177169
challengeId,
178170
authUser,
179171
challenge,

0 commit comments

Comments
 (0)