Skip to content

Commit 3bf346a

Browse files
authored
Merge pull request #309 from topcoder-platform/PM-5747
PM-5747: require passing Design F2F submissions for winner downloads
2 parents 52bcd7e + 768dbfa commit 3bf346a

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/api/submission/submission.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ export class SubmissionController {
405405
@ApiOperation({
406406
summary: 'Download the submission',
407407
description:
408-
'Roles: Copilot, Admin, User, Reviewer. After challenge completion, the exact metadata value allowAllRegistrantsToDownloadWinningSubmissions=true lets every registered Submitter download only an exact final winning submission and denies non-winners without legacy fallback. Other values retain legacy passing or First2Finish eligibility. | Scopes: read:submission',
408+
'Roles: Copilot, Admin, User, Reviewer. After challenge completion, the exact metadata value allowAllRegistrantsToDownloadWinningSubmissions=true lets every registered Submitter download only an exact final winning submission and denies non-winners without legacy fallback. Other values require passing-submission eligibility, except non-Design First2Finish challenges retain legacy submitter eligibility. | Scopes: read:submission',
409409
})
410410
@ApiParam({
411411
name: 'submissionId',

src/api/submission/submission.service.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,52 @@ describe('SubmissionService', () => {
13641364
expect(s3Send).not.toHaveBeenCalled();
13651365
});
13661366

1367+
it('denies a failed Design First2Finish submitter when the new flag is disabled', async () => {
1368+
resourceApiService.getMemberResourcesRoles.mockResolvedValue([
1369+
{ roleName: 'Submitter' },
1370+
]);
1371+
challengeApiServiceMock.getChallengeDetail.mockResolvedValue({
1372+
status: ChallengeStatus.COMPLETED,
1373+
type: 'First2Finish',
1374+
track: 'Design',
1375+
metadata: {
1376+
submissionsViewable: 'true',
1377+
},
1378+
winners: [{ userId: 'owner-user', placement: 1 }],
1379+
});
1380+
prismaMock.submission.findFirst.mockImplementation(({ where }) =>
1381+
Promise.resolve(
1382+
where.reviewSummation ? null : { id: 'failed-own-submission' },
1383+
),
1384+
);
1385+
1386+
await expect(
1387+
service.getSubmissionFileStream(
1388+
{
1389+
userId: 'failed-first2finish-submitter',
1390+
isMachine: false,
1391+
roles: [],
1392+
} as any,
1393+
'sub-123',
1394+
),
1395+
).rejects.toBeInstanceOf(ForbiddenException);
1396+
1397+
expect(prismaMock.submission.findFirst).toHaveBeenCalledTimes(1);
1398+
expect(prismaMock.submission.findFirst).toHaveBeenCalledWith({
1399+
where: {
1400+
challengeId: 'challenge-xyz',
1401+
memberId: 'failed-first2finish-submitter',
1402+
reviewSummation: {
1403+
some: {
1404+
isPassing: true,
1405+
},
1406+
},
1407+
},
1408+
select: { id: true },
1409+
});
1410+
expect(s3Send).not.toHaveBeenCalled();
1411+
});
1412+
13671413
it('preserves manager access when Design submissions are not viewable', async () => {
13681414
resourceApiService.getMemberResourcesRoles.mockResolvedValue([
13691415
{ roleName: 'Manager' },

src/api/submission/submission.service.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,8 +1285,9 @@ export class SubmissionService {
12851285
* eligible reviewers, copilots, or managers.
12861286
* - When `allowAllRegistrantsToDownloadWinningSubmissions` is exactly
12871287
* `"true"`, every registered Submitter may download only an exact final
1288-
* winning submission after completion. Other metadata values retain legacy
1289-
* passing or First2Finish eligibility.
1288+
* winning submission after completion. Other metadata values require
1289+
* passing-submission eligibility, except non-Design First2Finish challenges
1290+
* retain legacy submitter eligibility.
12901291
*
12911292
* The file is always fetched from the configured clean bucket, never from DMZ.
12921293
* The S3 key is derived from the submission.url.
@@ -1506,8 +1507,8 @@ export class SubmissionService {
15061507
* `"true"` makes the requested target decisive: it must be the exact canonical
15071508
* result for a recorded placement winner (or carry matching legacy placement
15081509
* data), and a non-winner fails closed without legacy fallback. Other metadata
1509-
* values retain the legacy First2Finish or passing-submission eligibility
1510-
* behavior.
1510+
* values require passing-submission eligibility, except non-Design
1511+
* First2Finish challenges retain legacy submitter eligibility.
15111512
*
15121513
* @param challengeId - Challenge containing the requested submission.
15131514
* @param requesterMemberId - Registered Submitter requesting the download.
@@ -1534,7 +1535,10 @@ export class SubmissionService {
15341535
return this.isWinningSubmission(challengeId, challenge, submission);
15351536
}
15361537

1537-
if (this.isFirst2FinishChallenge(challenge)) {
1538+
if (
1539+
this.isFirst2FinishChallenge(challenge) &&
1540+
!this.isDesignChallenge(challenge)
1541+
) {
15381542
const memberSubmission = await this.prisma.submission.findFirst({
15391543
where: {
15401544
challengeId,
@@ -1560,6 +1564,26 @@ export class SubmissionService {
15601564
return Boolean(passingSubmission);
15611565
}
15621566

1567+
/**
1568+
* Determines whether a challenge belongs to the Design track.
1569+
*
1570+
* The current track name is preferred, while the legacy track is also checked
1571+
* so migrated Design First2Finish challenges use passing-submission
1572+
* eligibility.
1573+
*
1574+
* @param challenge - Challenge metadata returned by the challenge service.
1575+
* @returns True when either current or legacy track is named Design.
1576+
* @throws Never.
1577+
*/
1578+
private isDesignChallenge(challenge: ChallengeData): boolean {
1579+
return [challenge.track, challenge.legacy?.track].some(
1580+
(track) =>
1581+
String(track ?? '')
1582+
.trim()
1583+
.toLowerCase() === 'design',
1584+
);
1585+
}
1586+
15631587
/**
15641588
* Reads the all-registrants winner-download feature flag.
15651589
*

0 commit comments

Comments
 (0)