track-cli 3.1.0 → 4.0.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/_dnt.shims.d.ts +1 -1
- package/esm/_dnt.shims.js +1 -0
- package/esm/src/action/add.js +8 -8
- package/esm/src/action/clone.js +46 -76
- package/esm/src/action/lang.js +11 -16
- package/esm/src/action/pull.js +8 -8
- package/esm/src/action/remove.js +8 -6
- package/esm/src/action/reset.js +9 -10
- package/esm/src/action/run.js +13 -16
- package/esm/src/action/status.js +6 -3
- package/esm/src/meta.d.ts +1 -1
- package/esm/src/meta.js +1 -1
- package/esm/src/shared/config.d.ts +16 -9
- package/esm/src/shared/config.js +0 -3
- package/esm/src/shared/errors.d.ts +1 -1
- package/esm/src/shared/errors.js +4 -3
- package/esm/src/shared/mod.d.ts +10 -10
- package/esm/src/shared/mod.js +73 -93
- package/esm/src/shared/types.d.ts +141 -11
- package/esm/src/shared/types.js +100 -1
- package/esm/src/track/client.d.ts +35 -38
- package/esm/src/track/client.js +31 -122
- package/esm/src/track/test.d.ts +32 -0
- package/esm/src/track/test.js +184 -0
- package/esm/src/track/training.d.ts +29 -0
- package/esm/src/track/training.js +142 -0
- package/package.json +1 -1
- package/esm/src/track/types.d.ts +0 -211
- package/esm/src/track/types.js +0 -40
package/esm/src/track/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VERSION } from "../meta.js";
|
|
2
2
|
import { HttpStatus } from "../shared/mod.js";
|
|
3
|
-
import { APIError
|
|
3
|
+
import { APIError } from "../shared/errors.js";
|
|
4
4
|
// @ts-ignore: has no exported member
|
|
5
5
|
import { CookieJar, fetch } from "node-fetch-cookies";
|
|
6
6
|
import { ProxyAgent } from "proxy-agent";
|
|
@@ -8,8 +8,8 @@ export const FormType = {
|
|
|
8
8
|
URLEncoded: "urlencoded",
|
|
9
9
|
JSON: "json",
|
|
10
10
|
};
|
|
11
|
-
export class
|
|
12
|
-
constructor(
|
|
11
|
+
export class TrackClientBase {
|
|
12
|
+
constructor(baseUrl, basic, cookies, isUnauthorized) {
|
|
13
13
|
Object.defineProperty(this, "baseUrl", {
|
|
14
14
|
enumerable: true,
|
|
15
15
|
configurable: true,
|
|
@@ -28,71 +28,34 @@ export class TrackClient {
|
|
|
28
28
|
writable: true,
|
|
29
29
|
value: void 0
|
|
30
30
|
});
|
|
31
|
-
Object.defineProperty(this, "
|
|
32
|
-
enumerable: true,
|
|
33
|
-
configurable: true,
|
|
34
|
-
writable: true,
|
|
35
|
-
value: void 0
|
|
36
|
-
});
|
|
37
|
-
Object.defineProperty(this, "cookieExpired", {
|
|
31
|
+
Object.defineProperty(this, "agent", {
|
|
38
32
|
enumerable: true,
|
|
39
33
|
configurable: true,
|
|
40
34
|
writable: true,
|
|
41
|
-
value:
|
|
35
|
+
value: new ProxyAgent()
|
|
42
36
|
});
|
|
43
|
-
Object.defineProperty(this, "
|
|
37
|
+
Object.defineProperty(this, "isUnauthorized", {
|
|
44
38
|
enumerable: true,
|
|
45
39
|
configurable: true,
|
|
46
40
|
writable: true,
|
|
47
|
-
value:
|
|
41
|
+
value: void 0
|
|
48
42
|
});
|
|
49
|
-
this.baseUrl =
|
|
50
|
-
this.headers = {
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
this.baseUrl = baseUrl;
|
|
44
|
+
this.headers = {
|
|
45
|
+
"X-Requested-With": "codecheck",
|
|
46
|
+
"User-Agent": `track-cli-${VERSION}`,
|
|
47
|
+
};
|
|
53
48
|
if (basic) {
|
|
54
49
|
this.headers["Authorization"] = `Basic ${btoa(`${basic.username}:${basic.password}`)}`;
|
|
55
50
|
}
|
|
56
51
|
this.cookies = new CookieJar();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const cookies = {};
|
|
61
|
-
for (const cookie of this.cookies.cookiesAll()) {
|
|
62
|
-
if (cookie.name && cookie.value) {
|
|
63
|
-
cookies[cookie.name] = cookie.value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return cookies;
|
|
67
|
-
}
|
|
68
|
-
authCookieExpired() {
|
|
69
|
-
return this.cookieExpired;
|
|
70
|
-
}
|
|
71
|
-
static fromTrackConfig(config) {
|
|
72
|
-
const authData = {
|
|
73
|
-
examToken: config.examToken,
|
|
74
|
-
orgName: config.orgName,
|
|
75
|
-
};
|
|
76
|
-
const client = new TrackClient(config.scheme, config.host, config.basicAuth, authData);
|
|
77
|
-
if (config.cookies) {
|
|
78
|
-
for (const [name, value] of Object.entries(config.cookies)) {
|
|
79
|
-
client.cookies.addCookie(`${name}=${value}`, client.baseUrl);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return client;
|
|
83
|
-
}
|
|
84
|
-
async authenticate() {
|
|
85
|
-
this.cookies = new CookieJar();
|
|
86
|
-
if (this.authData) {
|
|
87
|
-
const authUrl = `${this.baseUrl}/${this.authData.orgName}/exams/${this.authData.examToken}`;
|
|
88
|
-
const res = await fetch(this.cookies, authUrl, {
|
|
89
|
-
headers: this.headers,
|
|
90
|
-
agent: this.agent,
|
|
91
|
-
});
|
|
92
|
-
if (!HttpStatus.isSuccess(res)) {
|
|
93
|
-
throw new OtherError(`Failed to authenticate: ${res.status}\n${await res.text()}`);
|
|
52
|
+
if (cookies) {
|
|
53
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
54
|
+
this.cookies.addCookie(`${name}=${value}`, baseUrl);
|
|
94
55
|
}
|
|
95
56
|
}
|
|
57
|
+
this.isUnauthorized = isUnauthorized ||
|
|
58
|
+
((res) => res.status === HttpStatus.Unauthorized);
|
|
96
59
|
}
|
|
97
60
|
async _get(path, asText = false) {
|
|
98
61
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -100,12 +63,12 @@ export class TrackClient {
|
|
|
100
63
|
method: "GET",
|
|
101
64
|
headers: this.headers,
|
|
102
65
|
agent: this.agent,
|
|
66
|
+
redirect: "manual",
|
|
103
67
|
});
|
|
104
|
-
if (response1
|
|
68
|
+
if (!this.isUnauthorized(response1)) {
|
|
105
69
|
return await this._result(response1, asText);
|
|
106
70
|
}
|
|
107
71
|
await this.authenticate();
|
|
108
|
-
this.cookieExpired = true;
|
|
109
72
|
const response2 = await fetch(this.cookies, url, {
|
|
110
73
|
method: "GET",
|
|
111
74
|
headers: this.headers,
|
|
@@ -134,12 +97,12 @@ export class TrackClient {
|
|
|
134
97
|
headers: headers,
|
|
135
98
|
body: bodyData,
|
|
136
99
|
agent: this.agent,
|
|
100
|
+
redirect: "manual",
|
|
137
101
|
});
|
|
138
|
-
if (response1
|
|
102
|
+
if (!this.isUnauthorized(response1)) {
|
|
139
103
|
return await this._result(response1);
|
|
140
104
|
}
|
|
141
105
|
await this.authenticate();
|
|
142
|
-
this.cookieExpired = true;
|
|
143
106
|
const response2 = await fetch(this.cookies, url, {
|
|
144
107
|
method: "POST",
|
|
145
108
|
headers: headers,
|
|
@@ -169,12 +132,12 @@ export class TrackClient {
|
|
|
169
132
|
headers: headers,
|
|
170
133
|
body: bodyData,
|
|
171
134
|
agent: this.agent,
|
|
135
|
+
redirect: "manual",
|
|
172
136
|
});
|
|
173
|
-
if (response1
|
|
137
|
+
if (!this.isUnauthorized(response1)) {
|
|
174
138
|
return await this._result(response1);
|
|
175
139
|
}
|
|
176
140
|
await this.authenticate();
|
|
177
|
-
this.cookieExpired = true;
|
|
178
141
|
const response2 = await fetch(this.cookies, url, {
|
|
179
142
|
method: "PUT",
|
|
180
143
|
headers: headers,
|
|
@@ -202,67 +165,13 @@ export class TrackClient {
|
|
|
202
165
|
throw await APIError.fromResponse(response);
|
|
203
166
|
}
|
|
204
167
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
async findProgrammingLanguage(programmingLanguage) {
|
|
215
|
-
return (await this.getProgrammingLanguages())
|
|
216
|
-
.find((pl, _) => pl.value === programmingLanguage);
|
|
217
|
-
}
|
|
218
|
-
async switchChallengeLanguage(applicantExamId, resultId, language) {
|
|
219
|
-
return await this._put(`/api/applicants/exams/${applicantExamId}/results/${resultId}/language`, FormType.URLEncoded, {
|
|
220
|
-
programmingLanguage: language,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
async startChallenge(applicantExamId, resultId) {
|
|
224
|
-
return await this._put(`/api/applicants/exams/${applicantExamId}/results/${resultId}/start`, FormType.URLEncoded, {
|
|
225
|
-
takenBy: 3, // 3 = LocalMachine
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
async prepareChallenge(applicantExamId, challengeId) {
|
|
229
|
-
return await this._post(`/api/applicants/exams/${applicantExamId}/results`, FormType.URLEncoded, {
|
|
230
|
-
challengeId: challengeId,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
async timeLeft(applicantExamId, resultId) {
|
|
234
|
-
return await this._get(`/api/applicants/exams/${applicantExamId}/results/${resultId}/timeleft`);
|
|
235
|
-
}
|
|
236
|
-
async getChallengeCodingContext(applicantExamId, resultId) {
|
|
237
|
-
return await this._get(`/api/applicants/exams/${applicantExamId}/results/${resultId}/context`);
|
|
238
|
-
}
|
|
239
|
-
async getPresignedUrls(applicantExamId, resultId, files) {
|
|
240
|
-
return await this._post(`/api/applicants/exams/${applicantExamId}/results/${resultId}/presigned`, FormType.JSON, {
|
|
241
|
-
files: files,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
async orcaToken(url) {
|
|
245
|
-
return await this._get(url);
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Temporary code to retrieve the Orca host.
|
|
249
|
-
* Will be able to get from CodingContext in the future.
|
|
250
|
-
*/
|
|
251
|
-
async orcaHost() {
|
|
252
|
-
const asText = true;
|
|
253
|
-
const editorHtml = await this._get("/editor/1.0/coding", asText);
|
|
254
|
-
return (/data-orca-host="([^"]+)"/.exec(editorHtml) ||
|
|
255
|
-
["", "track-prod-frontend.orca.run"])[1];
|
|
256
|
-
}
|
|
257
|
-
async saveFiles(applicantExamId, resultId, saveFilesRequest) {
|
|
258
|
-
return await this._put(`/api/applicants/exams/${applicantExamId}/results/${resultId}/save`, FormType.JSON, saveFilesRequest);
|
|
259
|
-
}
|
|
260
|
-
async updateEditorScore(applicantExamId, resultId, editorScore) {
|
|
261
|
-
return await this._put(`/api/applicants/exams/${applicantExamId}/results/${resultId}/editorScore`, FormType.URLEncoded, {
|
|
262
|
-
editorScore: editorScore,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
async reset(applicantExamId, resultId) {
|
|
266
|
-
return await this._put(`/api/applicants/exams/${applicantExamId}/results/${resultId}/reset`, FormType.URLEncoded, {});
|
|
168
|
+
getCookies() {
|
|
169
|
+
const cookies = {};
|
|
170
|
+
for (const cookie of this.cookies.cookiesAll()) {
|
|
171
|
+
if (cookie.name && cookie.value) {
|
|
172
|
+
cookies[cookie.name] = cookie.value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return cookies;
|
|
267
176
|
}
|
|
268
177
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TrackTestConfigPart } from "../shared/config.js";
|
|
2
|
+
import { ChallengeResult, ChallengeSession, CodingContext, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
|
|
3
|
+
import { BasicAuth, TrackClient, TrackClientBase } from "./client.js";
|
|
4
|
+
export declare class TestClient extends TrackClientBase implements TrackClient {
|
|
5
|
+
private orgName;
|
|
6
|
+
private token;
|
|
7
|
+
private challengeId;
|
|
8
|
+
private applicantExamId;
|
|
9
|
+
private resultId;
|
|
10
|
+
constructor(baseUrl: string, orgName: string, token: string, challengeId: number, basic?: BasicAuth, cookies?: Record<string, string>);
|
|
11
|
+
config(): TrackTestConfigPart;
|
|
12
|
+
authenticate(): Promise<void>;
|
|
13
|
+
startChallengeSession(): Promise<ChallengeSession>;
|
|
14
|
+
private getApplicantExamNoAuth;
|
|
15
|
+
private ensureExamInProgress;
|
|
16
|
+
private getExamSession;
|
|
17
|
+
private getChallengeFromExamSession;
|
|
18
|
+
languages(): Promise<ProgrammingLanguageInfo[]>;
|
|
19
|
+
start(): Promise<ChallengeResult>;
|
|
20
|
+
prepare(): Promise<ChallengeResult>;
|
|
21
|
+
updateLanguage(language: ProgrammingLanguage): Promise<void>;
|
|
22
|
+
timeLeft(): Promise<number>;
|
|
23
|
+
context(): Promise<CodingContext>;
|
|
24
|
+
presigned(files: string[]): Promise<{
|
|
25
|
+
[name: string]: string;
|
|
26
|
+
}>;
|
|
27
|
+
orcaHost(): Promise<string>;
|
|
28
|
+
orcaToken(context: CodingContext): Promise<string>;
|
|
29
|
+
saveFiles(files: SaveFilesRequest): Promise<void>;
|
|
30
|
+
updateEditorScore(score: number): Promise<void>;
|
|
31
|
+
reset(): Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ExamCanceled, ExamExpired, ExamSubmitted, ExamUnread, InvalidChallengeId, OtherError, } from "../shared/errors.js";
|
|
2
|
+
import { HttpStatus } from "../shared/mod.js";
|
|
3
|
+
import { ChallengeStyle, } from "../shared/types.js";
|
|
4
|
+
import { FormType, TrackClientBase, } from "./client.js";
|
|
5
|
+
// @ts-ignore: has no exported member
|
|
6
|
+
import { CookieJar, fetch } from "node-fetch-cookies";
|
|
7
|
+
export class TestClient extends TrackClientBase {
|
|
8
|
+
constructor(baseUrl, orgName, token, challengeId, basic, cookies) {
|
|
9
|
+
super(baseUrl, basic, cookies);
|
|
10
|
+
Object.defineProperty(this, "orgName", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "token", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "challengeId", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "applicantExamId", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "resultId", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: 0
|
|
39
|
+
});
|
|
40
|
+
this.orgName = orgName;
|
|
41
|
+
this.token = token;
|
|
42
|
+
this.challengeId = challengeId;
|
|
43
|
+
}
|
|
44
|
+
config() {
|
|
45
|
+
return {
|
|
46
|
+
challengeId: this.challengeId,
|
|
47
|
+
applicantExamId: this.applicantExamId,
|
|
48
|
+
challengeResultId: this.resultId,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async authenticate() {
|
|
52
|
+
this.cookies = new CookieJar();
|
|
53
|
+
const authUrl = `${this.baseUrl}/${this.orgName}/exams/${this.token}`;
|
|
54
|
+
const res = await fetch(this.cookies, authUrl, {
|
|
55
|
+
headers: this.headers,
|
|
56
|
+
agent: this.agent,
|
|
57
|
+
});
|
|
58
|
+
if (!HttpStatus.isSuccess(res)) {
|
|
59
|
+
throw new OtherError(`Failed to authenticate: ${res.status}\n${await res.text()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async startChallengeSession() {
|
|
63
|
+
const applicantExam = await this.getApplicantExamNoAuth();
|
|
64
|
+
const webUrl = `${this.baseUrl}/${this.orgName}/exams/${this.token}`;
|
|
65
|
+
this.ensureExamInProgress(applicantExam.status, webUrl);
|
|
66
|
+
const examSession = await this.getExamSession(applicantExam.id);
|
|
67
|
+
const result = examSession.results.find((r) => r.challengeId === this.challengeId);
|
|
68
|
+
const challenge = this.getChallengeFromExamSession(examSession, result);
|
|
69
|
+
if (!challenge) {
|
|
70
|
+
throw new InvalidChallengeId(this.challengeId);
|
|
71
|
+
}
|
|
72
|
+
this.applicantExamId = applicantExam.id;
|
|
73
|
+
this.resultId = result?.id || 0;
|
|
74
|
+
return challenge;
|
|
75
|
+
}
|
|
76
|
+
async getApplicantExamNoAuth() {
|
|
77
|
+
return await this._get(`/api/applicants/exams/${this.token}/min`);
|
|
78
|
+
}
|
|
79
|
+
ensureExamInProgress(status, webUrlRedirect) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case ApplicantExamStatus.Submitted:
|
|
82
|
+
case ApplicantExamStatus.Reviewed:
|
|
83
|
+
throw new ExamSubmitted();
|
|
84
|
+
case ApplicantExamStatus.Expired:
|
|
85
|
+
throw new ExamExpired();
|
|
86
|
+
case ApplicantExamStatus.Canceled:
|
|
87
|
+
throw new ExamCanceled();
|
|
88
|
+
case ApplicantExamStatus.Unread:
|
|
89
|
+
throw new ExamUnread(webUrlRedirect);
|
|
90
|
+
case ApplicantExamStatus.InProgress:
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async getExamSession(applicantExamId) {
|
|
95
|
+
return await this._get(`/api/applicants/exams/${applicantExamId}`);
|
|
96
|
+
}
|
|
97
|
+
getChallengeFromExamSession(examSession, result) {
|
|
98
|
+
return examSession.challengesSets
|
|
99
|
+
.flatMap((s) => s.challenges)
|
|
100
|
+
.map((esc) => toChallengeSession(esc, result))
|
|
101
|
+
.find((c) => c.challengeId === this.challengeId &&
|
|
102
|
+
(c.style === ChallengeStyle.Development ||
|
|
103
|
+
c.style === ChallengeStyle.Algorithm));
|
|
104
|
+
}
|
|
105
|
+
async languages() {
|
|
106
|
+
return (await this._get(`/api/enum/challenges/programminglanguages`))
|
|
107
|
+
.map(toProgrammingLanguageInfo);
|
|
108
|
+
}
|
|
109
|
+
async start() {
|
|
110
|
+
return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/start`, FormType.URLEncoded, {
|
|
111
|
+
takenBy: 3, // 3 = LocalMachine
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async prepare() {
|
|
115
|
+
return await this._post(`/api/applicants/exams/${this.applicantExamId}/results`, FormType.URLEncoded, {
|
|
116
|
+
challengeId: this.challengeId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async updateLanguage(language) {
|
|
120
|
+
return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/language`, FormType.URLEncoded, {
|
|
121
|
+
programmingLanguage: language,
|
|
122
|
+
}); // ChallengeResult
|
|
123
|
+
}
|
|
124
|
+
async timeLeft() {
|
|
125
|
+
return await this._get(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/timeleft`);
|
|
126
|
+
}
|
|
127
|
+
async context() {
|
|
128
|
+
return await this._get(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/context`);
|
|
129
|
+
}
|
|
130
|
+
async presigned(files) {
|
|
131
|
+
return await this._post(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/presigned`, FormType.JSON, {
|
|
132
|
+
files: files,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async orcaHost() {
|
|
136
|
+
const asText = true;
|
|
137
|
+
const editorHtml = await this._get("/editor/1.0/coding", asText);
|
|
138
|
+
return (/data-orca-host="([^"]+)"/.exec(editorHtml) ||
|
|
139
|
+
["", "track-prod-frontend.orca.run"])[1];
|
|
140
|
+
}
|
|
141
|
+
async orcaToken(context) {
|
|
142
|
+
return await this._get(context.urlForOrcaToken);
|
|
143
|
+
}
|
|
144
|
+
async saveFiles(files) {
|
|
145
|
+
return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/save`, FormType.JSON, files);
|
|
146
|
+
}
|
|
147
|
+
async updateEditorScore(score) {
|
|
148
|
+
return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/editorScore`, FormType.URLEncoded, {
|
|
149
|
+
editorScore: score,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async reset() {
|
|
153
|
+
return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/reset`, FormType.URLEncoded, {});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const ApplicantExamStatus = {
|
|
157
|
+
Unread: 1,
|
|
158
|
+
InProgress: 2,
|
|
159
|
+
Submitted: 3,
|
|
160
|
+
Expired: 4,
|
|
161
|
+
Canceled: 5,
|
|
162
|
+
Reviewed: 6,
|
|
163
|
+
};
|
|
164
|
+
function toChallengeSession(esc, result) {
|
|
165
|
+
return {
|
|
166
|
+
challengeId: esc.id,
|
|
167
|
+
challengeVersionId: esc.challengeVersionId,
|
|
168
|
+
style: esc.style,
|
|
169
|
+
title: esc.title,
|
|
170
|
+
description: esc.description,
|
|
171
|
+
timeLimitMinutes: esc.timeLimitMinutes,
|
|
172
|
+
displayOrder: esc.displayOrder,
|
|
173
|
+
programmingLanguages: esc.programmingLanguages,
|
|
174
|
+
localCodingAllowed: esc.localExamEnabled,
|
|
175
|
+
openTestcases: esc.openTestcases,
|
|
176
|
+
result,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function toProgrammingLanguageInfo(pl) {
|
|
180
|
+
return {
|
|
181
|
+
value: pl.value,
|
|
182
|
+
name: pl.displayString,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TrackTrainingConfigPart } from "../shared/config.js";
|
|
2
|
+
import { ChallengeResult, ChallengeSession, CodingContext, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
|
|
3
|
+
import { BasicAuth, TrackClient, TrackClientBase } from "./client.js";
|
|
4
|
+
export declare class TrainingClient extends TrackClientBase implements TrackClient {
|
|
5
|
+
private orgName;
|
|
6
|
+
private token;
|
|
7
|
+
private courseMaterialId;
|
|
8
|
+
private klassId;
|
|
9
|
+
private klassResultId;
|
|
10
|
+
constructor(baseUrl: string, orgName: string, token: string, courseMaterialId: number, klassId: number, klassResultId: number, basic?: BasicAuth, cookies?: Record<string, string>);
|
|
11
|
+
config(): TrackTrainingConfigPart;
|
|
12
|
+
authenticate(): Promise<void>;
|
|
13
|
+
startChallengeSession(): Promise<ChallengeSession>;
|
|
14
|
+
continueChallengeSession(): Promise<ChallengeSession>;
|
|
15
|
+
languages(): Promise<ProgrammingLanguageInfo[]>;
|
|
16
|
+
start(): Promise<ChallengeResult>;
|
|
17
|
+
prepare(): Promise<ChallengeResult>;
|
|
18
|
+
updateLanguage(language: ProgrammingLanguage): Promise<void>;
|
|
19
|
+
timeLeft(): Promise<number>;
|
|
20
|
+
context(): Promise<CodingContext>;
|
|
21
|
+
presigned(files: string[]): Promise<{
|
|
22
|
+
[name: string]: string;
|
|
23
|
+
}>;
|
|
24
|
+
orcaHost(): Promise<string>;
|
|
25
|
+
orcaToken(context: CodingContext): Promise<string>;
|
|
26
|
+
saveFiles(files: SaveFilesRequest): Promise<void>;
|
|
27
|
+
updateEditorScore(score: number): Promise<void>;
|
|
28
|
+
reset(): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { OtherError } from "../shared/errors.js";
|
|
2
|
+
import { HttpStatus } from "../shared/mod.js";
|
|
3
|
+
import { FormType, TrackClientBase, } from "./client.js";
|
|
4
|
+
// @ts-ignore: has no exported member
|
|
5
|
+
import { CookieJar, fetch } from "node-fetch-cookies";
|
|
6
|
+
export class TrainingClient extends TrackClientBase {
|
|
7
|
+
constructor(baseUrl, orgName, token, courseMaterialId, klassId, klassResultId, basic, cookies) {
|
|
8
|
+
super(baseUrl, basic, cookies, (res) => HttpStatus.isRedirect(res) || res.status === HttpStatus.Unauthorized);
|
|
9
|
+
Object.defineProperty(this, "orgName", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "token", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: void 0
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "courseMaterialId", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: void 0
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "klassId", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: void 0
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "klassResultId", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: void 0
|
|
38
|
+
});
|
|
39
|
+
this.orgName = orgName;
|
|
40
|
+
this.token = token;
|
|
41
|
+
this.courseMaterialId = courseMaterialId;
|
|
42
|
+
this.klassId = klassId;
|
|
43
|
+
this.klassResultId = klassResultId;
|
|
44
|
+
}
|
|
45
|
+
config() {
|
|
46
|
+
return {
|
|
47
|
+
courseMaterialId: this.courseMaterialId,
|
|
48
|
+
klassId: this.klassId,
|
|
49
|
+
klassResultId: this.klassResultId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async authenticate() {
|
|
53
|
+
this.cookies = new CookieJar();
|
|
54
|
+
const authUrl = `${this.baseUrl}/${this.orgName}/train/${this.token}/challenges/${this.courseMaterialId}`;
|
|
55
|
+
const res = await fetch(this.cookies, authUrl, {
|
|
56
|
+
headers: this.headers,
|
|
57
|
+
agent: this.agent,
|
|
58
|
+
});
|
|
59
|
+
if (!HttpStatus.isSuccess(res)) {
|
|
60
|
+
throw new OtherError(`Failed to authenticate: ${res.status}\n${await res.text()}`);
|
|
61
|
+
}
|
|
62
|
+
const { klassId } = (await res.json()).result;
|
|
63
|
+
this.klassId = klassId;
|
|
64
|
+
}
|
|
65
|
+
async startChallengeSession() {
|
|
66
|
+
const trainingChallenge = await this._post(`/api/train/${this.klassId}/challenges/${this.courseMaterialId}`, FormType.JSON, {});
|
|
67
|
+
this.klassResultId = trainingChallenge.result.klassResultId;
|
|
68
|
+
return toChallengeSession(trainingChallenge);
|
|
69
|
+
}
|
|
70
|
+
continueChallengeSession() {
|
|
71
|
+
return this.startChallengeSession();
|
|
72
|
+
}
|
|
73
|
+
async languages() {
|
|
74
|
+
return (await this._get(`/cli/languages`))
|
|
75
|
+
.map(toProgrammingLanguageInfo);
|
|
76
|
+
}
|
|
77
|
+
async start() {
|
|
78
|
+
return await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/start`, FormType.JSON, {
|
|
79
|
+
takenBy: 3, // 3 = LocalMachine
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
prepare() {
|
|
83
|
+
throw new OtherError("Unsupported operation");
|
|
84
|
+
}
|
|
85
|
+
async updateLanguage(language) {
|
|
86
|
+
await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/lang`, FormType.JSON, {
|
|
87
|
+
lang: language,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async timeLeft() {
|
|
91
|
+
return await this._get(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/timeleft`);
|
|
92
|
+
}
|
|
93
|
+
async context() {
|
|
94
|
+
return await this._get(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/context`);
|
|
95
|
+
}
|
|
96
|
+
async presigned(files) {
|
|
97
|
+
return await this._post(`/api/train/${this.klassId}/challenges/review/${this.klassResultId}/presigned`, FormType.JSON, {
|
|
98
|
+
files: files,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async orcaHost() {
|
|
102
|
+
const asText = true;
|
|
103
|
+
const editorHtml = await this._get("/editor/1.0/coding", asText);
|
|
104
|
+
return (/data-orca-host="([^"]+)"/.exec(editorHtml) ||
|
|
105
|
+
["", "track-prod-frontend.orca.run"])[1];
|
|
106
|
+
}
|
|
107
|
+
async orcaToken(context) {
|
|
108
|
+
return await this._get(context.urlForOrcaToken);
|
|
109
|
+
}
|
|
110
|
+
async saveFiles(files) {
|
|
111
|
+
return await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/save`, FormType.JSON, files);
|
|
112
|
+
}
|
|
113
|
+
async updateEditorScore(score) {
|
|
114
|
+
return await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/editorScore`, FormType.JSON, {
|
|
115
|
+
editorScore: score,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async reset() {
|
|
119
|
+
return await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/reset`, FormType.JSON, {});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function toChallengeSession(tcs) {
|
|
123
|
+
return {
|
|
124
|
+
challengeId: tcs.challenge.id,
|
|
125
|
+
challengeVersionId: tcs.challenge.challengeVersionId,
|
|
126
|
+
style: tcs.challenge.style,
|
|
127
|
+
title: tcs.challenge.title,
|
|
128
|
+
description: tcs.challenge.description,
|
|
129
|
+
timeLimitMinutes: tcs.challenge.timeLimitMinutes,
|
|
130
|
+
displayOrder: tcs.challenge.displayOrder,
|
|
131
|
+
openTestcases: tcs.challenge.openTestcases,
|
|
132
|
+
programmingLanguages: tcs.challenge.programmingLanguages,
|
|
133
|
+
localCodingAllowed: tcs.challenge.localCodingEnabled,
|
|
134
|
+
result: tcs.result,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function toProgrammingLanguageInfo(pl) {
|
|
138
|
+
return {
|
|
139
|
+
value: pl.id,
|
|
140
|
+
name: pl.name,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/package.json
CHANGED