Skip to content

Commit

Permalink
Add endpoint for offline submission with test
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienTainon committed Apr 12, 2024
1 parent 4da3736 commit 561477f
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 47 deletions.
66 changes: 64 additions & 2 deletions features/post_submission.feature
Expand Up @@ -20,8 +20,8 @@ Feature: Post submission
| 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----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fdfSR+W+pwMz/hx11pyJndF1L+LDHyIIW3tj5vYQ57aUjtnU9LUxdscMfF1F9ZNzmHutU+bRKlutNodoEKSHVkRyotQ0qK/VO2nv+DYuiJ0EB2V1uf77xkZzrMT+htukD5XUMyAt38amb6y7daFC5dcD9B7Q2Hx1RT5hzjCILWzZsRD83xEKQ1QAg6JwYYWVVEx759O2SUDqxffyuw/wqANfgWxihlIPimVFbbDoTpfpTf7fnDZu9UU8lFIK4I3EyFRRmKGUC99sMIfw545/p2byB3veIi6507Rb2k0nlwhq2zfGwHlUbwy4QLqL9zk2ipEN5tLvJn4ltU6YOQOawIDAQAB-----END PUBLIC KEY----- | https://mockapi.com |
And I seed the ID generator to 100
And I mock the graderqueue

Expand Down Expand Up @@ -178,3 +178,65 @@ Feature: Post submission
"message": "Error: Invalid task id: 1001"
}
"""

Scenario: Post offline submission
Given "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",
"nbHintsGiven": "0",
"platformName": "codecast-test"
}
"""
And I setup a mock API answering any POST request to "/answers" with the following payload:
"""
{
"success": true,
"data": {
"answer_token": "fake_answer_token"
}
}
"""
When I send a POST request to "/submissions-offline" with the following payload:
"""
{
"token": "{{taskToken}}",
"answer": {
"language": "python",
"fileName": "Code 5",
"sourceCode": "print('test')"
},
"sLocale": "fr",
"platform": "codecast-test"
}
"""
Then the response status code should be 200
And the response body should be the following JSON:
"""
{
"submissionId": "101",
"success": true
}
"""
And the table "tm_submissions" should be:
| ID | idUser | idPlatform | idTask | idSourceCode | bManualCorrection | bSuccess | nbTestsTotal | nbTestsPassed | iScore | bCompilError | bEvaluated | bConfirmed | sMode | iChecksum | iVersion |
| 101 | 1 | 1 | 1000 | 100 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Submitted | 0 | 2147483647 |
And the table "tm_source_codes" should be:
| ID | idUser | idPlatform | idTask | sParams | sName | sSource | bEditable | bSubmission | sType | bActive | iRank | iVersion |
| 100 | 1 | 1 | 1000 | {"sLangProg":"python"} | Code 5 | print('test') | 0 | 1 | User | 0 | 0 | 2147483647 |
And the grader queue should have received the following request:
"""
{
"request": "sendjob",
"priority": 1,
"taskrevision": "7156",
"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",
"debugPassword": "test"
}
"""
14 changes: 13 additions & 1 deletion features/steps/server_steps.ts
Expand Up @@ -6,10 +6,16 @@ import {ServerInjectResponse} from '@hapi/hapi';
import {testServer} from '../support/hooks';
import * as ws from 'ws';
import {remoteExecutionProxyHandler} from '../../src/remote_execution_proxy';
import nock from 'nock';

interface ServerStepsContext {
response: ServerInjectResponse,
responsePromise: Promise<ServerInjectResponse>,
[key: string]: any,
}

function injectVariables(context: {[key: string]: string}, payload: string): string {
return payload.replace(/\{\{(\w+)}}/g, (replacement, contents: string) => context[contents]);
}

When(/^I send a GET request to "([^"]*)"$/, async function (this: ServerStepsContext, url: string) {
Expand All @@ -35,7 +41,7 @@ When(/^I send a POST request to "([^"]*)" with the following payload:$/, async f
this.response = await testServer.inject({
method: 'POST',
url,
payload,
payload: injectVariables(this, payload),
});
});

Expand Down Expand Up @@ -155,3 +161,9 @@ Then(/^the "([^"]*)" WS server should have received the following JSON:$/, funct
const lastMessage: unknown = openServers[serverName].lastMessages.shift();
expect(lastMessage).to.deep.equal(expectedResponse);
});

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, any>);
});
9 changes: 9 additions & 0 deletions features/steps/util_steps.ts
@@ -1,6 +1,8 @@
import {Given, When} from '@cucumber/cucumber';
import {longPollingHandler} from '../../src/long_polling';
import {seedRandomIdGenerator} from '../support/hooks';
import {TokenGenerator} from '../../src/tokenization';
import appConfig from '../../src/config';

When(/^I wait (\d+)ms$/, async function (delay: number) {
await new Promise(resolve => setTimeout(resolve, delay));
Expand All @@ -13,3 +15,10 @@ When(/^I fire the event "([^"]*)" to the longPolling handler$/, function (event:
Given(/^I seed the ID generator to (\d+)$/, function (seed: number) {
seedRandomIdGenerator(seed);
});
Given(/^"([^"]*)" is a token signed by the platform with the following payload:$/, async function (this, tokenName: string, payload: string) {
const tokenGenerator = new TokenGenerator();
await tokenGenerator.setKeys(appConfig.graderQueue.ownPrivateKey);
const parsedPayload = JSON.parse(payload) as Record<string, any>;
const token = await tokenGenerator.jwsSignPayload(parsedPayload);
this[tokenName] = token;
});
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -33,6 +33,7 @@
"chai-subset": "^1.6.0",
"dotenv": "^16.0.1",
"eslint": "^8.24.0",
"nock": "^13.5.4",
"ts-node-dev": "^2.0.0",
"typescript": "^4.7.4"
}
Expand Down
12 changes: 6 additions & 6 deletions src/grader_interface.ts
Expand Up @@ -132,7 +132,7 @@ export async function generateQueueRequest(submissionId: string, submissionData:
if (returnUrl || idUserAnswer) {
await Db.execute('update tm_submissions set sReturnUrl = :returnUrl, idUserAnswer = :idUserAnswer WHERE tm_submissions.`ID` = :idSubmission and tm_submissions.idUser = :idUser and tm_submissions.idPlatform = :idPlatform and tm_submissions.idTask = :idTask;', {
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idTask: taskTokenData.taskId,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
returnUrl,
Expand All @@ -149,17 +149,17 @@ WHERE tm_submissions.ID = :idSubmission
and tm_submissions.idPlatform = :idPlatform
and tm_submissions.idTask = :idTask;`, {
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idTask: taskTokenData.taskId,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
});
if (null === submission) {
throw new InvalidInputError('Cannot find submission ' + submissionId);
}

const task = await findTaskById(taskTokenData.idTaskLocal);
const task = await findTaskById(taskTokenData.taskId);
if (null === task) {
throw new InvalidInputError(`Cannot find task with id ${taskTokenData.idTaskLocal}`);
throw new InvalidInputError(`Cannot find task with id ${taskTokenData.taskId}`);
}
const sourceCode = await findSourceCodeById(submission.idSourceCode);
if (null === sourceCode) {
Expand All @@ -174,7 +174,7 @@ WHERE tm_submissions.ID = :idSubmission
if ('UserTest' === submission.sMode) {
tests = await Db.execute<TaskTest[]>('SELECT tm_tasks_tests.* FROM tm_tasks_tests WHERE idUser = :idUser and idPlatform = :idPlatform and idTask = :idTask and idSubmission = :idSubmission ORDER BY iRank ASC', {
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idTask: taskTokenData.taskId,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
});
Expand Down Expand Up @@ -258,7 +258,7 @@ WHERE tm_submissions.ID = :idSubmission

jobData['taskPath'] = task.sTaskPath;
jobData['options'] = {
locale: submissionData.sLocale,
locale: submissionData.sLocale ?? 'fr',
};

// When this is a test user, avoid blocking the grader queue because several people use
Expand Down
10 changes: 5 additions & 5 deletions src/platform_interface.ts
Expand Up @@ -11,13 +11,13 @@ import {InvalidInputError, PlatformInteractionError} from './error_handler';

export interface PlatformTaskTokenData {
payload: PlatformTaskTokenPayload,
idTaskLocal: string,
taskId: string,
platform: Platform,
}

export interface PlatformAnswerTokenData {
payload: PlatformAnswerTokenPayload,
idTaskLocal: string,
taskId: string,
platform: Platform,
}

Expand All @@ -40,7 +40,7 @@ export async function extractPlatformTaskTokenData(token: string|null|undefined,

return {
payload: payload,
idTaskLocal: await getLocalIdTask(payload),
taskId: await getTaskIdFromTaskTokenPayload(payload),
platform: platformEntity,
};
}
Expand All @@ -64,7 +64,7 @@ export async function extractPlatformAnswerTaskTokenData(token: string|null|unde

return {
payload: payload,
idTaskLocal: await getLocalIdTask(payload),
taskId: await getTaskIdFromTaskTokenPayload(payload),
platform: platformEntity,
};
}
Expand All @@ -76,7 +76,7 @@ function getIdFromUrl(itemUrl: string): string|null {
return params['taskId'] ? params['taskId'] : null;
}

async function getLocalIdTask(params: PlatformGenericTokenPayload): Promise<string> {
async function getTaskIdFromTaskTokenPayload(params: PlatformGenericTokenPayload): Promise<string> {
const idItem = params.idItem || null;
const itemUrl = params.itemUrl || null;
if (itemUrl) {
Expand Down
17 changes: 14 additions & 3 deletions src/server.ts
@@ -1,7 +1,13 @@
import Hapi, {Lifecycle} from '@hapi/hapi';
import {Server} from '@hapi/hapi';
import {getTask} from './tasks';
import {createOfflineSubmission, createSubmission, getSubmission} from './submissions';
import {
createOfflineSubmission,
createSubmission,
getSubmission, offlineSubmissionDataDecoder, OfflineSubmissionParameters,
submissionDataDecoder,
SubmissionParameters
} from './submissions';
import ReturnValue = Lifecycle.ReturnValue;
import {ErrorHandler, isResponseBoom, NotFoundError} from './error_handler';
import {receiveSubmissionResultsFromTaskGrader} from './grader_webhook';
Expand All @@ -10,6 +16,7 @@ import log from 'loglevel';
import HAPIWebSocket from 'hapi-plugin-websocket';
import {remoteExecutionProxyHandler} from './remote_execution_proxy';
import appConfig from './config';
import {decode} from './util';

export async function init(): Promise<Server> {
const server = Hapi.server({
Expand Down Expand Up @@ -42,7 +49,9 @@ export async function init(): Promise<Server> {
path: '/submissions',
options: {
handler: async (request, h) => {
const submissionId = await createSubmission(request.payload);
const submissionData: SubmissionParameters = decode(submissionDataDecoder)(request.payload);

const submissionId = await createSubmission(submissionData);

return h.response({
success: true,
Expand All @@ -57,7 +66,9 @@ export async function init(): Promise<Server> {
path: '/submissions-offline',
options: {
handler: async (request, h) => {
const submissionId = await createOfflineSubmission(request.payload);
const submissionData: OfflineSubmissionParameters = decode(offlineSubmissionDataDecoder)(request.payload);

const submissionId = await createOfflineSubmission(submissionData);

return h.response({
success: true,
Expand Down

0 comments on commit 561477f

Please sign in to comment.