Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ Refer to the verification document `Verification.md`
- Challenge `metadata` may include `submission_type` to override the community-app submission flow:
`zip` shows the standard Topcoder zip upload page, and `url` shows the Topgear URL upload page.
When omitted, consumers should keep their existing default behavior.
- Challenge `metadata` uses the exact string values `true` and `false` for `is_test_challenge`.
Challenge creation adds `is_test_challenge: false` when it is omitted. `NEW` challenges retain
their existing deletion behavior. A `COMPLETED` or `CANCELLED*` challenge can be deleted when this
metadata value is exactly `true`; `DRAFT`, `APPROVED`, and `ACTIVE` challenges cannot use this
bypass. Any update that starts in or transitions to a completed or cancelled status cannot change
the effective `is_test_challenge` value; omitting metadata preserves it. Normal authorization
checks still apply.
- API base configuration points to v6 in dev/local and v5 in prod (for compatibility):
- Dev: `work-manager/config/constants/development.js`.
- Local: `work-manager/config/constants/local.js`.
Expand Down
1 change: 1 addition & 0 deletions app-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const prizeTypes = {
const ChallengeMetadataNames = {
ALLOW_ALL_REGISTRANTS_TO_DOWNLOAD_WINNING_SUBMISSIONS:
"allowAllRegistrantsToDownloadWinningSubmissions",
IS_TEST_CHALLENGE: "is_test_challenge",
};

const BOOLEAN_METADATA_VALUES = ["true", "false"];
Expand Down
35 changes: 27 additions & 8 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,10 @@ paths:
tags:
- Challenges
description: Delete the challenge with the provided id.
Only challenges with status of "NEW" can be deleted.
Challenges with status "NEW" retain their existing deletion behavior. Challenges with a
"COMPLETED" or "CANCELLED*" status can also be deleted when their is_test_challenge metadata
value is the exact string "true". "DRAFT", "APPROVED", and "ACTIVE" challenges cannot use
this bypass. Normal deletion authorization checks still apply.
security:
- bearer: []
produces:
Expand Down Expand Up @@ -2594,7 +2597,9 @@ definitions:
description: >-
Metadata value. For submission_type, supported values are zip and url. For
allowAllRegistrantsToDownloadWinningSubmissions, only the exact strings true and
false are accepted; a missing entry behaves as false.
false are accepted; a missing entry behaves as false. For is_test_challenge, only
the exact strings true and false are accepted, and create requests that omit it
persist false.
required:
- name
- value
Expand Down Expand Up @@ -2881,13 +2886,17 @@ definitions:
description: >-
Metadata name. Use submission_type to override the challenge submission flow. Use
allowAllRegistrantsToDownloadWinningSubmissions to let all registrants download
winning submissions after the challenge ends.
winning submissions after the challenge ends. For Design challenges,
submissionsViewable must also be true. Use is_test_challenge to mark production
test data that may be deleted after testing.
value:
type: string
description: >-
Metadata value. For submission_type, supported values are zip and url. For
allowAllRegistrantsToDownloadWinningSubmissions, only the exact strings true and
false are accepted; a missing entry behaves as false.
false are accepted; a missing entry behaves as false. For is_test_challenge, only
the exact strings true and false are accepted, and create requests that omit it
persist false.
required:
- name
- value
Expand Down Expand Up @@ -3058,13 +3067,18 @@ definitions:
description: >-
Metadata name. Use submission_type to override the challenge submission flow. Use
allowAllRegistrantsToDownloadWinningSubmissions to let all registrants download
winning submissions after the challenge ends.
winning submissions after the challenge ends. For Design challenges,
submissionsViewable must also be true. Use is_test_challenge to mark production
test data that may be deleted after testing.
value:
type: string
description: >-
Metadata value. For submission_type, supported values are zip and url. For
allowAllRegistrantsToDownloadWinningSubmissions, only the exact strings true and
false are accepted; a missing entry behaves as false.
false are accepted; a missing entry behaves as false. For is_test_challenge, only
the exact strings true and false are accepted; omitted values behave as false. Its
effective value cannot change in an update that starts in or transitions to
COMPLETED or CANCELLED status; omitting metadata preserves the existing value.
required:
- name
- value
Expand Down Expand Up @@ -3280,13 +3294,18 @@ definitions:
description: >-
Metadata name. Use submission_type to override the challenge submission flow. Use
allowAllRegistrantsToDownloadWinningSubmissions to let all registrants download
winning submissions after the challenge ends.
winning submissions after the challenge ends. For Design challenges,
submissionsViewable must also be true. Use is_test_challenge to mark production
test data that may be deleted after testing.
value:
type: string
description: >-
Metadata value. For submission_type, supported values are zip and url. For
allowAllRegistrantsToDownloadWinningSubmissions, only the exact strings true and
false are accepted; a missing entry behaves as false.
false are accepted; a missing entry behaves as false. For is_test_challenge, only
the exact strings true and false are accepted; omitted values behave as false. Its
effective value cannot change in an update that starts in or transitions to
COMPLETED or CANCELLED status; omitting metadata preserves the existing value.
required:
- name
- value
Expand Down
65 changes: 65 additions & 0 deletions src/common/challenge-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,69 @@ class ChallengeHelper {
}
}

/**
* Add the explicit false default for the metadata-backed test challenge flag.
* Challenge creation uses this before persistence so all newly created challenges have a
* deterministic `is_test_challenge` value. An existing entry is preserved unchanged so the
* subsequent validator can reject invalid values instead of silently replacing them.
*
* @param {Array<Object>|undefined|null} metadata challenge metadata entries
* @returns {Array<Object>} the original metadata entries plus the default flag when absent
* @throws {BadRequestError} if metadata is supplied with a non-array value
*/
applyTestChallengeMetadataDefault(metadata) {
if (!_.isNil(metadata) && !_.isArray(metadata)) {
throw new errors.BadRequestError("metadata must be an array");
}

const resolvedMetadata = metadata || [];
const testChallengeEntry = _.find(resolvedMetadata, {
name: ChallengeMetadataNames.IS_TEST_CHALLENGE,
});
if (!_.isNil(testChallengeEntry)) {
return resolvedMetadata;
}

return [
...resolvedMetadata,
{
name: ChallengeMetadataNames.IS_TEST_CHALLENGE,
value: "false",
},
];
}

/**
* Validate the metadata-backed test challenge flag.
* Create and update request validation call this before metadata is persisted. The exact string
* representation keeps Challenge API responses and downstream payment checks consistent.
*
* @param {Array<Object>|undefined|null} metadata challenge metadata entries
* @returns {void}
* @throws {BadRequestError} if `is_test_challenge` is not the string `true` or `false`
*/
validateTestChallengeMetadata(metadata) {
if (_.isNil(metadata)) {
return;
}

const testChallengeEntry = _.find(metadata, {
name: ChallengeMetadataNames.IS_TEST_CHALLENGE,
});
if (_.isNil(testChallengeEntry)) {
return;
}

if (
typeof testChallengeEntry.value !== "string" ||
!_.includes(BOOLEAN_METADATA_VALUES, testChallengeEntry.value)
) {
throw new errors.BadRequestError(
"metadata is_test_challenge must be either true or false as a string"
);
}
}

validatePrizeSetsAndGetPrizeType(prizeSets) {
if (_.isEmpty(prizeSets)) return null;

Expand Down Expand Up @@ -266,6 +329,7 @@ class ChallengeHelper {
// helper.ensureNoDuplicateOrNullElements(challenge.events, 'events')
this.validateSubmissionTypeMetadata(challenge.metadata);
this.validateRegisteredMemberWinningSubmissionDownloadMetadata(challenge.metadata);
this.validateTestChallengeMetadata(challenge.metadata);

// check groups authorization
if (challenge.groups && challenge.groups.length > 0) {
Expand Down Expand Up @@ -743,6 +807,7 @@ class ChallengeHelper {
helper.ensureNoDuplicateOrNullElements(data.groups, "groups");
this.validateSubmissionTypeMetadata(data.metadata);
this.validateRegisteredMemberWinningSubmissionDownloadMetadata(data.metadata);
this.validateTestChallengeMetadata(data.metadata);

if (data.projectId) {
await ChallengeHelper.ensureProjectExist(data.projectId, currentUser);
Expand Down
Loading
Loading