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
4 changes: 1 addition & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ jobs:
MYSQL_DB_USER: codecast
MYSQL_DB_PASSWORD: codecast
MYSQL_DB_DATABASE: task_platform
TEST_MODE: 1
TEST_MODE_PLATFORM_NAME: codecast-test
TEST_MODE_USER_ID: 1
TEST_MODE: 0
GRADER_QUEUE_DEBUG_PASSWORD: test
CODECAST_DEBUGGERS_URL: ws://127.0.0.1:9003
steps:
Expand Down
159 changes: 144 additions & 15 deletions features/get_submission.feature

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions features/get_task.feature
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Feature: Get task
| 5001 | 1000 | 4000 | Evaluation | 1 | 1 | s1-t2 | 10 | 15 | 2147483647 |
| 5002 | 1000 | 4001 | Evaluation | 2 | 1 | s2-t1 | 15 | 10 | 2147483647 |
And the database has the following table "tm_platforms":
| ID | name | public_key |
| 1 | codecast-test | |
| ID | name | public_key | api_url |
| 1 | codecast-test | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt8dBg+ojFTrgFeDxoGqqBSQkW/BDSl/H+qzpIpZTCj4mw7zyrIeV7zaaPuA/8g8WVPDjliuVxLwOnX6p8bT0ZEgsyo4/nql2VEI1cLBqSowQ3VoICqeRYHqgv+8g/B4mFxvRRpNNWiM9aE80KtjXBesi7GjULjg6Jnpqfn1UAGrx4AlnbuabH50/xQoQMWLHSpSVhnpEV5XrUPvzHGbkW51/HRRMEF9Fj5SSPs8vQPbA5ZO8H7NgHwN+8fyNuyVtm9DwY9QZVp2mYlbLlV/+y8xrd5TKf/aGyMjVr3du5YwfosrlrnTAJ+DgoxuZRw77DKaiATxSpEiQRH/C208mOwIDAQAB -----END PUBLIC KEY----- | https://mockapi.com |
Scenario: Get task by id
When I send a GET request to "/tasks/1000"
Then the response status code should be 200
Expand All @@ -35,10 +35,11 @@ Feature: Get task
"""
{
"bAccessSolutions": true,
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1000"
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1000",
"idUser": "1"
}
"""
When I send a GET request to "/tasks/1000?token={{taskToken}}"
When I send a GET request to "/tasks/1000?token={{taskToken}}&platform=codecast-test"
Then the response status code should be 200
And the response body content at property path "strings.0" should be the following JSON:
"""
Expand Down
58 changes: 42 additions & 16 deletions features/post_submission.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,40 @@ Feature: Post submission
| 1 | codecast-test | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt8dBg+ojFTrgFeDxoGqqBSQkW/BDSl/H+qzpIpZTCj4mw7zyrIeV7zaaPuA/8g8WVPDjliuVxLwOnX6p8bT0ZEgsyo4/nql2VEI1cLBqSowQ3VoICqeRYHqgv+8g/B4mFxvRRpNNWiM9aE80KtjXBesi7GjULjg6Jnpqfn1UAGrx4AlnbuabH50/xQoQMWLHSpSVhnpEV5XrUPvzHGbkW51/HRRMEF9Fj5SSPs8vQPbA5ZO8H7NgHwN+8fyNuyVtm9DwY9QZVp2mYlbLlV/+y8xrd5TKf/aGyMjVr3du5YwfosrlrnTAJ+DgoxuZRw77DKaiATxSpEiQRH/C208mOwIDAQAB -----END PUBLIC KEY----- | https://mockapi.com |
And I seed the ID generator to 100
And I mock the graderqueue
And "taskToken" is a token signed by the platform with the following payload:
"""
{
"bSubmissionPossible": true,
"date": "10-04-2024",
"idUser": "1",
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1000"
}
"""
And "answerToken" is a token signed by the platform with the following payload:
"""
{
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1000",
"idUser": "1",
"randomSeed": "6",
"sHintsRequested": "[]",
"sAnswer": "print('ici')"
}
"""

Scenario: Post submission
When I send a POST request to "/submissions" with the following payload:
"""
{
"token": null,
"answerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpdGVtVXJsIjoiaHR0cDovL2x2aC5tZTo4MDAxL25leHQvdGFzaz90YXNrSUQ9bnVsbCZ2ZXJzaW9uPXVuZGVmaW5lZCIsInJhbmRvbVNlZWQiOiI2Iiwic0hpbnRzUmVxdWVzdGVkIjoiW10iLCJzQW5zd2VyIjoiXCJcXFwiYWFhXFxcIlwiIiwiaWF0IjoxNjgyMzQxMTQxfQ.vNA9EgZkGboNS7aGzFJRo60JdrQX-APIOHnf313ESzA",
"token": "{{taskToken}}",
"answerToken": "{{answerToken}}",
"answer": {
"language": "python",
"fileName": "Code 5",
"sourceCode": "print('ici')"
},
"userTests": [],
"sLocale": "fr",
"platform": null,
"platform": "codecast-test",
"taskId": "1000",
"taskParams": {
"minScore": 0,
Expand Down Expand Up @@ -73,7 +92,7 @@ Feature: Post submission
"tags": "",
"jobname": "101",
"jobdata": "{\"taskPath\":\"$ROOT_PATH/FranceIOI/Contests/2018/Algorea_finale/plateau\",\"extraParams\":{\"solutionFilename\":\"101.py\",\"solutionContent\":\"print('ici')\",\"solutionLanguage\":\"python3\",\"solutionDependencies\":\"@defaultDependencies-python3\",\"solutionFilterTests\":\"@defaultFilterTests-python3\",\"solutionId\":\"sol0-101.py\",\"solutionExecId\":\"exec0-101.py\",\"defaultSolutionCompParams\":{\"memoryLimitKb\":131072,\"timeLimitMs\":10000,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]},\"defaultSolutionExecParams\":{\"memoryLimitKb\":64000,\"timeLimitMs\":200,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]}},\"options\":{\"locale\":\"fr\"}}",
"jobusertaskid": "1000-103-1",
"jobusertaskid": "1000-1-1",
"debugPassword": "test"
}
"""
Expand All @@ -82,8 +101,8 @@ Feature: Post submission
When I send a POST request to "/submissions" with the following payload:
"""
{
"token": null,
"answerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpdGVtVXJsIjoiaHR0cDovL2x2aC5tZTo4MDAxL25leHQvdGFzaz90YXNrSUQ9bnVsbCZ2ZXJzaW9uPXVuZGVmaW5lZCIsInJhbmRvbVNlZWQiOiI2Iiwic0hpbnRzUmVxdWVzdGVkIjoiW10iLCJzQW5zd2VyIjoiXCJcXFwiYWFhXFxcIlwiIiwiaWF0IjoxNjgyMzQxMTQxfQ.vNA9EgZkGboNS7aGzFJRo60JdrQX-APIOHnf313ESzA",
"token": "{{taskToken}}",
"answerToken": "{{answerToken}}",
"answer": {
"language": "python",
"fileName": "Code 5",
Expand All @@ -98,7 +117,7 @@ Feature: Post submission
}
],
"sLocale": "fr",
"platform": null,
"platform": "codecast-test",
"taskId": "1000",
"taskParams": {
"minScore": 0,
Expand Down Expand Up @@ -140,26 +159,34 @@ Feature: Post submission
"tags": "",
"jobname": "101",
"jobdata": "{\"taskPath\":\"$ROOT_PATH/FranceIOI/Contests/2018/Algorea_finale/plateau\",\"extraParams\":{\"solutionFilename\":\"101.py\",\"solutionContent\":\"print('ici')\",\"solutionLanguage\":\"python3\",\"solutionDependencies\":\"@defaultDependencies-python3\",\"solutionFilterTests\":[\"id-*.in\"],\"solutionId\":\"sol0-101.py\",\"solutionExecId\":\"exec0-101.py\",\"defaultSolutionCompParams\":{\"memoryLimitKb\":131072,\"timeLimitMs\":10000,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]},\"defaultSolutionExecParams\":{\"memoryLimitKb\":64000,\"timeLimitMs\":200,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]}},\"extraTests\":[{\"name\":\"id-10.in\",\"content\":\"test\"},{\"name\":\"id-10.out\",\"content\":\"ici\"}],\"executions\":[{\"id\":\"testExecution\",\"idSolution\":\"@solutionId\",\"filterTests\":[\"id-*.in\"],\"runExecution\":\"@defaultSolutionExecParams\"}],\"options\":{\"locale\":\"fr\"}}",
"jobusertaskid": "1000-103-1",
"jobusertaskid": "1000-1-1",
"debugPassword": "test"
}
"""

Scenario: Post submission on unknown task
Given "unknownTaskToken" is a token signed by the platform with the following payload:
"""
{
"bSubmissionPossible": true,
"date": "10-04-2024",
"idUser": "1",
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1001"
}
"""
When I send a POST request to "/submissions" with the following payload:
"""
{
"token": null,
"answerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpdGVtVXJsIjoiaHR0cDovL2x2aC5tZTo4MDAxL25leHQvdGFzaz90YXNrSUQ9bnVsbCZ2ZXJzaW9uPXVuZGVmaW5lZCIsInJhbmRvbVNlZWQiOiI2Iiwic0hpbnRzUmVxdWVzdGVkIjoiW10iLCJzQW5zd2VyIjoiXCJcXFwiYWFhXFxcIlwiIiwiaWF0IjoxNjgyMzQxMTQxfQ.vNA9EgZkGboNS7aGzFJRo60JdrQX-APIOHnf313ESzA",
"token": "{{unknownTaskToken}}",
"answerToken": "{{answerToken}}",
"answer": {
"language": "python",
"fileName": "Code 5",
"sourceCode": "print('ici')"
},
"userTests": [],
"sLocale": "fr",
"platform": null,
"taskId": "1001",
"platform": "codecast-test",
"taskParams": {
"minScore": 0,
"maxScore": 100,
Expand Down Expand Up @@ -187,16 +214,15 @@ Feature: Post submission
"date": "10-04-2024",
"idUser": "1",
"itemUrl": "https://codecast.france-ioi.org/next/task?taskId=1000",
"nbHintsGiven": "0",
"platformName": "codecast-test"
"nbHintsGiven": "0"
}
"""
And I setup a mock API answering any POST request to "/answers" with the following payload:
"""
{
"success": true,
"data": {
"answer_token": "fake_answer_token"
"answer_token": "{{answerToken}}"
}
}
"""
Expand Down Expand Up @@ -236,7 +262,7 @@ Feature: Post submission
"tags": "",
"jobname": "101",
"jobdata": "{\"taskPath\":\"$ROOT_PATH/FranceIOI/Contests/2018/Algorea_finale/plateau\",\"extraParams\":{\"solutionFilename\":\"101.py\",\"solutionContent\":\"print('test')\",\"solutionLanguage\":\"python3\",\"solutionDependencies\":\"@defaultDependencies-python3\",\"solutionFilterTests\":\"@defaultFilterTests-python3\",\"solutionId\":\"sol0-101.py\",\"solutionExecId\":\"exec0-101.py\",\"defaultSolutionCompParams\":{\"memoryLimitKb\":131072,\"timeLimitMs\":10000,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]},\"defaultSolutionExecParams\":{\"memoryLimitKb\":64000,\"timeLimitMs\":200,\"stdoutTruncateKb\":-1,\"stderrTruncateKb\":-1,\"useCache\":true,\"getFiles\":[]}},\"options\":{\"locale\":\"fr\"}}",
"jobusertaskid": "1000-103-1",
"jobusertaskid": "1000-1-1",
"debugPassword": "test"
}
"""
Expand Down
6 changes: 3 additions & 3 deletions features/steps/server_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ When(/^I send a GET request to "([^"]*)"$/, async function (this: ServerStepsCon
When(/^I asynchronously send a GET request to "([^"]*)"$/, function (this: ServerStepsContext, url: string) {
this.responsePromise = testServer.inject({
method: 'GET',
url,
url: injectVariables(this, url),
})
.then((response: ServerInjectResponse): ServerInjectResponse => {
this.response = response;
Expand All @@ -41,7 +41,7 @@ When(/^I asynchronously send a GET request to "([^"]*)"$/, function (this: Serve
When(/^I send a POST request to "([^"]*)" with the following payload:$/, async function (this: ServerStepsContext, url: string, payload: string) {
this.response = await testServer.inject({
method: 'POST',
url,
url: injectVariables(this, url),
payload: injectVariables(this, payload),
});
});
Expand Down Expand Up @@ -193,7 +193,7 @@ Then(/^the "([^"]*)" WS server should have received the following JSON:$/, funct
Given(/^I setup a mock API answering any POST request to "([^"]*)" with the following payload:$/, function (this: ServerStepsContext, endpoint: string, mockPayload: string) {
nock('https://mockapi.com')
.post(endpoint)
.reply(200, JSON.parse(mockPayload) as Record<string, unknown>);
.reply(200, JSON.parse(injectVariables(this, mockPayload)) as Record<string, unknown>);
});

Then(/^the mock API should have received the expected request$/, function () {
Expand Down
3 changes: 2 additions & 1 deletion features/support/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function randomIdGenerator(): string {
}

BeforeAll(async function () {
process.env.TZ = 'UTC';
Db.init();
setRandomIdGenerator(randomIdGenerator);
testServer = await init();
Expand All @@ -55,7 +56,7 @@ AfterAll(async function () {
});

async function cleanDatabase(): Promise<void> {
if (!appConfig.testMode.enabled) {
if ('test' !== appConfig.nodeEnv) {
throw new Error('Database cannot be cleaned while not in test environment.');
}

Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dotenv.config({path: path.resolve(__dirname, '../.env')});

interface Config {
nodeEnv: string,
development: boolean,
port: number|null,
mysqlDatabase: {
host: string,
Expand Down Expand Up @@ -48,6 +49,7 @@ function stringifyIfExists(string: string|undefined): string|undefined {

const appConfig: Config = {
nodeEnv,
development: 'test' === nodeEnv || 'development' === nodeEnv,
port: process.env['PORT'] ? Number(process.env['PORT']) : null,
mysqlDatabase: {
host: String(process.env.MYSQL_DB_HOST),
Expand Down
15 changes: 11 additions & 4 deletions src/crypto/jwes_decoder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type JWTPayload, KeyLike} from 'jose/dist/types/types';
import * as jose from 'jose';
import moment from 'moment/moment';
import {InvalidInputError} from '../error_handler';

/**
* JWE key is our private key used for decryption
Expand Down Expand Up @@ -43,26 +44,32 @@ export class JwesDecoder {
throw new Error('JWS key must be fulfilled to do decryption');
}

const {payload: validatedContent} = await jose.compactVerify(payload, this.jwsKey);
let validatedContent;
try {
const verification = await jose.compactVerify(payload, this.jwsKey);
validatedContent = verification.payload;
} catch (e) {
throw new InvalidInputError('Token cannot be decrypted, please check your SSL keys');
}

const result = new TextDecoder().decode(validatedContent);
let params: {date?: string, type?: string} = {};
try {
params = JSON.parse(result) as {date?: string, type?: string};
} catch (e) {
throw new Error('Token cannot be decrypted, please check your SSL keys');
throw new InvalidInputError('Token cannot be decrypted, please check your SSL keys');
}

if (!params['date']) {
throw new Error(`Invalid Task token, unable to decrypt: ${result}`);
throw new InvalidInputError(`Invalid Task token, unable to decrypt: ${result}`);
}

const yesterdayDate = moment().subtract(1, 'day').format('DD-MM-YYYY');
const todayDate = moment().format('DD-MM-YYYY');
const tomorrowDate = moment().add(1, 'day').format('DD-MM-YYYY');

if ((!params['type'] || params['type'] !== 'long') && params['date'] !== yesterdayDate && params['date'] !== todayDate && params['date'] !== tomorrowDate) {
throw new Error(`API token expired: ${params['date']}`);
throw new InvalidInputError(`API token expired: ${params['date']}`);
}

return params;
Expand Down
3 changes: 1 addition & 2 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ export function init(): void {
namedPlaceholders: true,
supportBigNumbers: true,
bigNumberStrings: true,
timezone: 'test' === appConfig.nodeEnv ? '+00:00' : undefined,
});

// TODO: set timezone?
} catch (error) {
throw new DatabaseError('Failed to initialized pool', undefined, error);
}
Expand Down
17 changes: 13 additions & 4 deletions src/error_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export class NotFoundError extends Error {
export class InvalidInputError extends Error {
}

export class AccessDeniedError extends Error {
}

export class PlatformInteractionError extends Error {
}

Expand All @@ -25,6 +28,12 @@ export class ErrorHandler {
.code(400);
}

if (e instanceof AccessDeniedError) {
return h
.response({error: 'Access denied.', message: String(e)})
.code(401);
}

if (e instanceof DatabaseError) {
if (e.query) {
// eslint-disable-next-line
Expand All @@ -35,13 +44,13 @@ export class ErrorHandler {
}

return h
.response({error: 'A database error has occurred.', ...(appConfig.testMode.enabled ? {details: String(e), query: e.query, databaseError: e.error} : {})})
.response({error: 'A database error has occurred.', ...(appConfig.development ? {details: String(e), query: e.query, databaseError: e.error} : {})})
.code(500);
}

if (e instanceof NotFoundError) {
return h
.response({error: 'Not found', ...(appConfig.testMode.enabled ? {details: String(e)} : {})})
.response({error: 'Not found', ...(appConfig.development ? {details: String(e)} : {})})
.code(404);
}

Expand All @@ -60,15 +69,15 @@ export class ErrorHandler {
console.error({url, error});

return h
.response({error: 'Error during external API call', ...(appConfig.testMode.enabled ? {url, details: parsedError} : {})})
.response({error: 'Error during external API call', ...(appConfig.development ? {url, details: parsedError} : {})})
.code(500);
}

// eslint-disable-next-line
console.error(e);

return h
.response({error: 'An internal server error occurred', ...(appConfig.testMode.enabled ? {details: String(e)} : {})})
.response({error: 'An internal server error occurred', ...(appConfig.development ? {details: String(e)} : {})})
.code(500);
}
}
11 changes: 5 additions & 6 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getSubmission, offlineSubmissionDataDecoder, OfflineSubmissionParameters,
submissionDataDecoder,
SubmissionParameters,
submissionQueryDecoder,
SubmissionQueryParameters,
} from './submissions';
import ReturnValue = Lifecycle.ReturnValue;
import {ErrorHandler, isResponseBoom, NotFoundError} from './error_handler';
Expand Down Expand Up @@ -106,12 +108,9 @@ export async function init(): Promise<Server> {
path: '/submissions/{submissionId}',
options: {
handler: async (request, h) => {
const submissionQueryParameters = {
longPolling: 'longPolling' in request.query,
withTests: 'withTests' in request.query,
};
const submissionQueryParameters: SubmissionQueryParameters = decode(submissionQueryDecoder)(request.query);

let submissionData = await getSubmission(String(request.params.submissionId), submissionQueryParameters.withTests);
let submissionData = await getSubmission(String(request.params.submissionId), submissionQueryParameters);
if (null === submissionData) {
throw new NotFoundError(`Submission not found with this id: ${String(request.params.submissionId)}`);
}
Expand All @@ -122,7 +121,7 @@ export async function init(): Promise<Server> {
const longPollingResult = await longPollingHandler.waitForEvent('evaluation-' + submissionData.id, 10 * 1000);
if ('event' === longPollingResult) {
// Re-fetch submission
submissionData = await getSubmission(String(request.params.submissionId), submissionQueryParameters.withTests);
submissionData = await getSubmission(String(request.params.submissionId), submissionQueryParameters);
if (null === submissionData) {
throw new NotFoundError(`Submission not found with this id: ${String(request.params.submissionId)}`);
}
Expand Down
Loading
Loading