Skip to content

Commit 81504b1

Browse files
committed
require valid hackerrank url when requesting review
1 parent 44b5cfb commit 81504b1

4 files changed

Lines changed: 164 additions & 5 deletions

File tree

src/bot/__tests__/requestReview.test.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe('requestReview', () => {
284284
[ActionId.HACKERRANK_URL]: {
285285
[ActionId.HACKERRANK_URL]: {
286286
type: 'plain_text_input',
287-
value: 'https://www.hackerrank.com/test/example123',
287+
value: 'https://www.hackerrank.com/test/example123?authkey=validkey123',
288288
},
289289
},
290290
};
@@ -384,7 +384,86 @@ _Candidate Identifier: some-identifier_
384384
messageTimestamp: '100',
385385
},
386386
],
387-
hackerRankUrl: 'https://www.hackerrank.com/test/example123',
387+
hackerRankUrl: 'https://www.hackerrank.com/test/example123?authkey=validkey123',
388+
});
389+
});
390+
391+
describe('HackerRank URL validation', () => {
392+
it('should reject submission when HackerRank URL is missing authkey parameter', async () => {
393+
const invalidUrlValues = {
394+
...defaultValues,
395+
[ActionId.HACKERRANK_URL]: {
396+
[ActionId.HACKERRANK_URL]: {
397+
type: 'plain_text_input',
398+
value:
399+
'https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary',
400+
},
401+
},
402+
};
403+
404+
const param = buildParam(invalidUrlValues);
405+
await requestReview.callback(param);
406+
407+
expect(param.ack).toHaveBeenCalledWith({
408+
response_action: 'errors',
409+
errors: {
410+
[ActionId.HACKERRANK_URL]:
411+
'Please provide a valid HackerRank URL with an authkey. Use the "Share Report" button to get the correct URL.',
412+
},
413+
});
414+
415+
// Should not proceed with creating the review
416+
expect(activeReviewRepo.create).not.toHaveBeenCalled();
417+
expect(param.client.chat.postMessage).not.toHaveBeenCalled();
418+
});
419+
420+
it('should accept submission when HackerRank URL contains authkey parameter', async () => {
421+
const validUrlValues = {
422+
...defaultValues,
423+
[ActionId.HACKERRANK_URL]: {
424+
[ActionId.HACKERRANK_URL]: {
425+
type: 'plain_text_input',
426+
value:
427+
'https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary?authkey=789',
428+
},
429+
},
430+
};
431+
432+
const { param } = await callCallback(buildParam(validUrlValues));
433+
434+
// Should acknowledge without errors
435+
expect(param.ack).toHaveBeenCalledWith();
436+
437+
// Should proceed with creating the review
438+
expect(activeReviewRepo.create).toHaveBeenCalled();
439+
expect(param.client.chat.postMessage).toHaveBeenCalled();
440+
});
441+
442+
it('should reject submission when HackerRank URL is invalid format', async () => {
443+
const invalidUrlValues = {
444+
...defaultValues,
445+
[ActionId.HACKERRANK_URL]: {
446+
[ActionId.HACKERRANK_URL]: {
447+
type: 'plain_text_input',
448+
value: 'not-a-valid-url',
449+
},
450+
},
451+
};
452+
453+
const param = buildParam(invalidUrlValues);
454+
await requestReview.callback(param);
455+
456+
expect(param.ack).toHaveBeenCalledWith({
457+
response_action: 'errors',
458+
errors: {
459+
[ActionId.HACKERRANK_URL]:
460+
'Please provide a valid HackerRank URL with an authkey. Use the "Share Report" button to get the correct URL.',
461+
},
462+
});
463+
464+
// Should not proceed with creating the review
465+
expect(activeReviewRepo.create).not.toHaveBeenCalled();
466+
expect(param.client.chat.postMessage).not.toHaveBeenCalled();
388467
});
389468
});
390469
});

src/bot/requestReview.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './enums';
2020
import { chatService } from '@/services/ChatService';
2121
import { determineExpirationTime } from '@utils/reviewExpirationUtils';
22+
import { validateHackerRankUrl } from '@utils/urlValidation';
2223

2324
export const requestReview = {
2425
app: undefined as unknown as App,
@@ -166,13 +167,29 @@ export const requestReview = {
166167

167168
async callback(params: CallbackParam): Promise<void> {
168169
const { ack, client, body } = params;
169-
await ack();
170170

171171
if (!isViewSubmitActionParam(params)) {
172172
// TODO: How should we handle this case(if we need to)?
173173
log.d('callback called for non-submit action');
174174
}
175175

176+
// Extract and validate HackerRank URL before acknowledging
177+
const hackerRankUrl = blockUtils.getBlockValue(body, ActionId.HACKERRANK_URL);
178+
const hackerRankUrlValue = hackerRankUrl.value;
179+
180+
if (!validateHackerRankUrl(hackerRankUrlValue)) {
181+
await ack({
182+
response_action: 'errors',
183+
errors: {
184+
[ActionId.HACKERRANK_URL]:
185+
'Please provide a valid HackerRank URL with an authkey. Use the "Share Report" button to get the correct URL.',
186+
},
187+
});
188+
return;
189+
}
190+
191+
await ack();
192+
176193
const user = body.user;
177194
const channel = process.env.INTERVIEWING_CHANNEL_ID;
178195
const numberOfInitialReviewers = Number(process.env.NUMBER_OF_INITIAL_REVIEWERS);
@@ -181,15 +198,13 @@ export const requestReview = {
181198
const numberOfRequestedReviewers = blockUtils.getBlockValue(body, ActionId.NUMBER_OF_REVIEWERS);
182199
const candidateIdentifier = blockUtils.getBlockValue(body, ActionId.CANDIDATE_IDENTIFIER);
183200
const candidateType = blockUtils.getBlockValue(body, ActionId.CANDIDATE_TYPE);
184-
const hackerRankUrl = blockUtils.getBlockValue(body, ActionId.HACKERRANK_URL);
185201

186202
const numberOfReviewersValue = numberOfRequestedReviewers.value;
187203
const deadlineValue = deadline.selected_option.value;
188204
const deadlineDisplay = deadline.selected_option.text.text;
189205
const candidateIdentifierValue = candidateIdentifier.value;
190206
const candidateTypeValue = candidateType.selected_option.value;
191207
const candidateTypeDisplay = candidateType.selected_option.text.text;
192-
const hackerRankUrlValue = hackerRankUrl.value;
193208
log.d(
194209
'requestReview.callback',
195210
'Parsed values:',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { validateHackerRankUrl } from '../urlValidation';
2+
3+
describe('validateHackerRankUrl', () => {
4+
it('should return true for a valid HackerRank URL with authkey', () => {
5+
const validUrl =
6+
'https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary?authkey=789';
7+
expect(validateHackerRankUrl(validUrl)).toBe(true);
8+
});
9+
10+
it('should return false for a HackerRank URL without authkey', () => {
11+
const invalidUrl =
12+
'https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary';
13+
expect(validateHackerRankUrl(invalidUrl)).toBe(false);
14+
});
15+
16+
it('should return true for a URL with authkey and other query parameters', () => {
17+
const validUrl =
18+
'https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary?foo=bar&authkey=abc123&baz=qux';
19+
expect(validateHackerRankUrl(validUrl)).toBe(true);
20+
});
21+
22+
it('should return false for an invalid URL format', () => {
23+
const invalidUrl = 'not-a-valid-url';
24+
expect(validateHackerRankUrl(invalidUrl)).toBe(false);
25+
});
26+
27+
it('should return false for an empty string', () => {
28+
expect(validateHackerRankUrl('')).toBe(false);
29+
});
30+
31+
it('should return false for a URL with authkey in the path but not as a query parameter', () => {
32+
const invalidUrl = 'https://www.hackerrank.com/work/tests/authkey/123/report';
33+
expect(validateHackerRankUrl(invalidUrl)).toBe(false);
34+
});
35+
36+
it('should return true for a URL with authkey as an empty value', () => {
37+
// Even with an empty value, the parameter exists
38+
const validUrl = 'https://www.hackerrank.com/work/tests/123/report?authkey=';
39+
expect(validateHackerRankUrl(validUrl)).toBe(true);
40+
});
41+
});

src/utils/urlValidation.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Validates that a HackerRank URL contains an authkey query parameter.
3+
*
4+
* @param url - The HackerRank URL to validate
5+
* @returns true if the URL contains an authkey parameter, false otherwise
6+
*
7+
* @example
8+
* ```typescript
9+
* validateHackerRankUrl('https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary?authkey=abc123')
10+
* // returns true
11+
*
12+
* validateHackerRankUrl('https://www.hackerrank.com/work/tests/123/candidates/completed/456/report/summary')
13+
* // returns false
14+
* ```
15+
*/
16+
export function validateHackerRankUrl(url: string): boolean {
17+
try {
18+
const urlObj = new URL(url);
19+
return urlObj.searchParams.has('authkey');
20+
} catch {
21+
// Invalid URL format
22+
return false;
23+
}
24+
}

0 commit comments

Comments
 (0)