@jrpool/kilotest 46.1.2 → 48.0.0
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/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +5 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/requestRetest.test.ts +144 -0
- package/api/requestRetest.ts +119 -0
- package/api/requestTest.test.ts +153 -0
- package/api/requestTest.ts +123 -0
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +35 -14
- package/api/{util.js → util.ts} +54 -51
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +360 -0
- package/docs/SERVICE.md +170 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +397 -152
- package/{index.js → index.ts} +386 -285
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/util.test.ts +1129 -0
- package/util.ts +1017 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +74 -58
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +71 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +57 -39
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +62 -0
- package/web/requestRetest/requestRetest.test.ts +130 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/index.ts +52 -0
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +18 -22
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -89
- package/api/requestFeature.test.js +0 -58
- package/api/requestRetest.js +0 -96
- package/api/requestRetest.test.js +0 -80
- package/api/requestTest.js +0 -106
- package/api/requestTest.test.js +0 -92
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -128
- package/util.js +0 -744
- package/util.test.js +0 -968
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTest/index.js +0 -24
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,47 +1,50 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.test.
|
|
2
|
+
index.test.ts
|
|
3
3
|
Integration tests for index.js requestHandler, covering GET routes, POST routes,
|
|
4
4
|
error paths, worker authentication, and helper function behavior.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// ENVIRONMENT (
|
|
8
|
-
//
|
|
7
|
+
// ENVIRONMENT (the test environment configuration; all modules now read
|
|
8
|
+
// process.env at call time, so load order no longer matters.)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {fixtureDBDir} from './test/dbFixture.ts';
|
|
12
12
|
|
|
13
|
+
const testCommentsDir = path.join(import.meta.dirname, 'test/fixtures/comments');
|
|
13
14
|
process.env.DB_DIR = fixtureDBDir;
|
|
14
15
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
16
|
+
process.env.TUTORIAL_WEB_COMMENTS_PATH = path.join(testCommentsDir, 'tutorialWeb.json');
|
|
17
|
+
process.env.TUTORIAL_AI_COMMENTS_PATH = path.join(testCommentsDir, 'tutorialAI.json');
|
|
15
18
|
process.env.TESTARO_WORKERS = JSON.stringify({
|
|
16
19
|
worker1: {secret: 'secret1', name: 'Worker One'}
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
// IMPORTS
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
import {test, before, beforeEach, after} from 'node:test';
|
|
25
|
+
import assert from 'node:assert/strict';
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import * as indexModule from './index.ts';
|
|
26
29
|
const {requestHandler, routes, serveError, startServer, runIfMain, isPathAllowed, getAbuseError} = indexModule;
|
|
27
30
|
|
|
28
31
|
// CONSTANTS
|
|
29
32
|
|
|
30
33
|
const port = 3997;
|
|
31
|
-
const
|
|
34
|
+
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
32
35
|
|
|
33
36
|
// SETUP AND TEARDOWN
|
|
34
37
|
|
|
35
|
-
let server;
|
|
38
|
+
let server: http.Server;
|
|
36
39
|
|
|
37
40
|
before(async () => {
|
|
38
41
|
server = http.createServer(requestHandler);
|
|
39
|
-
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
42
|
+
await new Promise<void>(resolve => server.listen(port, () => resolve()));
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
// Restore
|
|
45
|
+
// Restore testRequests.json and clean job directories before each test, so tests do not depend on execution order.
|
|
43
46
|
beforeEach(async () => {
|
|
44
|
-
await fs.writeFile(
|
|
47
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
45
48
|
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
46
49
|
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
47
50
|
await fs.mkdir(dir, {recursive: true});
|
|
@@ -50,12 +53,16 @@ beforeEach(async () => {
|
|
|
50
53
|
await fs.unlink(path.join(dir, file)).catch(() => {});
|
|
51
54
|
}
|
|
52
55
|
}
|
|
56
|
+
// Create test comments directory and reset test comment files so tests start clean.
|
|
57
|
+
await fs.mkdir(testCommentsDir, {recursive: true});
|
|
58
|
+
await fs.writeFile(path.join(testCommentsDir, 'tutorialWeb.json'), '[]\n');
|
|
59
|
+
await fs.writeFile(path.join(testCommentsDir, 'tutorialAI.json'), '[]\n');
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
after(async () => {
|
|
56
63
|
// Close all idle connections, then close the server with a timeout.
|
|
57
64
|
server.closeAllConnections?.();
|
|
58
|
-
await new Promise(resolve => {
|
|
65
|
+
await new Promise<void>(resolve => {
|
|
59
66
|
const timer = setTimeout(() => {
|
|
60
67
|
server.closeAllConnections?.();
|
|
61
68
|
resolve();
|
|
@@ -65,8 +72,8 @@ after(async () => {
|
|
|
65
72
|
resolve();
|
|
66
73
|
});
|
|
67
74
|
});
|
|
68
|
-
// Restore
|
|
69
|
-
await fs.writeFile(
|
|
75
|
+
// Restore testRequests.json and clean job directories after running.
|
|
76
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
70
77
|
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
71
78
|
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
72
79
|
await fs.mkdir(dir, {recursive: true});
|
|
@@ -83,7 +90,7 @@ const uniqueStamp = Date.now();
|
|
|
83
90
|
|
|
84
91
|
// HELPERS
|
|
85
92
|
|
|
86
|
-
const request = (method, requestPath, body = null, headers = {}) => new Promise((resolve, reject) => {
|
|
93
|
+
const request = (method: string, requestPath: string, body: any = null, headers: any = {}): Promise<any> => new Promise((resolve, reject) => {
|
|
87
94
|
const options = {method, host: 'localhost', port, path: requestPath, headers: {...headers}};
|
|
88
95
|
let bodyData = '';
|
|
89
96
|
if (body) {
|
|
@@ -97,7 +104,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
97
104
|
options.headers['content-length'] = Buffer.byteLength(bodyData);
|
|
98
105
|
}
|
|
99
106
|
const req = http.request(options, response => {
|
|
100
|
-
const chunks = [];
|
|
107
|
+
const chunks: Buffer[] = [];
|
|
101
108
|
response.on('data', chunk => chunks.push(chunk));
|
|
102
109
|
response.on('end', () => {
|
|
103
110
|
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
@@ -107,7 +114,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
107
114
|
req.end(bodyData || '');
|
|
108
115
|
});
|
|
109
116
|
|
|
110
|
-
const formRequest = (method, requestPath, formData, headers = {}) => new Promise((resolve, reject) => {
|
|
117
|
+
const formRequest = (method: string, requestPath: string, formData: any, headers: any = {}): Promise<any> => new Promise((resolve, reject) => {
|
|
111
118
|
const body = new URLSearchParams(formData).toString();
|
|
112
119
|
const options = {
|
|
113
120
|
method,
|
|
@@ -121,7 +128,7 @@ const formRequest = (method, requestPath, formData, headers = {}) => new Promise
|
|
|
121
128
|
}
|
|
122
129
|
};
|
|
123
130
|
const req = http.request(options, response => {
|
|
124
|
-
const chunks = [];
|
|
131
|
+
const chunks: Buffer[] = [];
|
|
125
132
|
response.on('data', chunk => chunks.push(chunk));
|
|
126
133
|
response.on('end', () => {
|
|
127
134
|
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
@@ -131,7 +138,7 @@ const formRequest = (method, requestPath, formData, headers = {}) => new Promise
|
|
|
131
138
|
req.end(body);
|
|
132
139
|
});
|
|
133
140
|
|
|
134
|
-
const jsonBody = res => {
|
|
141
|
+
const jsonBody = (res: any) => {
|
|
135
142
|
try {
|
|
136
143
|
return JSON.parse(res.body);
|
|
137
144
|
}
|
|
@@ -324,18 +331,18 @@ test('GET /fullReport.json/990101T0000/xxx returns an abuse error for a nonexist
|
|
|
324
331
|
assert.ok(res.body.includes('Invalid request'));
|
|
325
332
|
});
|
|
326
333
|
|
|
327
|
-
test('GET /
|
|
328
|
-
const res = await request('GET', '/
|
|
334
|
+
test('GET /tutorialWeb/images/newsletter-form.png serves the image', async () => {
|
|
335
|
+
const res = await request('GET', '/tutorialWeb/images/newsletter-form.png');
|
|
329
336
|
assert.equal(res.statusCode, 200);
|
|
330
337
|
assert.ok(res.headers['content-type'].includes('image/png'));
|
|
331
338
|
});
|
|
332
339
|
|
|
333
|
-
test('GET /
|
|
340
|
+
test('GET /tutorialWeb/images with an unknown extension serves octet-stream', async () => {
|
|
334
341
|
// Create a temporary image file with an unknown extension.
|
|
335
|
-
const imgPath = path.join(
|
|
342
|
+
const imgPath = path.join(import.meta.dirname, 'web', 'tutorialWeb', 'images', 'test.bmp');
|
|
336
343
|
await fs.writeFile(imgPath, 'fake bitmap data');
|
|
337
344
|
try {
|
|
338
|
-
const res = await request('GET', '/
|
|
345
|
+
const res = await request('GET', '/tutorialWeb/images/test.bmp');
|
|
339
346
|
assert.equal(res.statusCode, 200);
|
|
340
347
|
assert.ok(res.headers['content-type'].includes('application/octet-stream'));
|
|
341
348
|
}
|
|
@@ -344,13 +351,34 @@ test('GET /tutorial/images with an unknown extension serves octet-stream', async
|
|
|
344
351
|
}
|
|
345
352
|
});
|
|
346
353
|
|
|
347
|
-
test('GET /
|
|
348
|
-
const res = await request('GET', '/
|
|
354
|
+
test('GET /tutorialWeb/images/nonexistent.png returns an error page', async () => {
|
|
355
|
+
const res = await request('GET', '/tutorialWeb/images/nonexistent.png');
|
|
349
356
|
assert.equal(res.statusCode, 400);
|
|
350
357
|
assert.ok(res.body.includes('Image not found'));
|
|
351
358
|
});
|
|
352
359
|
|
|
353
|
-
test('GET /
|
|
360
|
+
test('GET /tutorialAI/images/test.png serves the image', async () => {
|
|
361
|
+
// Create a temporary image file in tutorialAI.
|
|
362
|
+
const imgPath = path.join(import.meta.dirname, 'web', 'tutorialAI', 'images', 'test.png');
|
|
363
|
+
await fs.mkdir(path.dirname(imgPath), {recursive: true});
|
|
364
|
+
await fs.writeFile(imgPath, 'fake png data');
|
|
365
|
+
try {
|
|
366
|
+
const res = await request('GET', '/tutorialAI/images/test.png');
|
|
367
|
+
assert.equal(res.statusCode, 200);
|
|
368
|
+
assert.ok(res.headers['content-type'].includes('image/png'));
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
await fs.unlink(imgPath).catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('GET /tutorialAI/images/nonexistent.png returns an error page', async () => {
|
|
376
|
+
const res = await request('GET', '/tutorialAI/images/nonexistent.png');
|
|
377
|
+
assert.equal(res.statusCode, 400);
|
|
378
|
+
assert.ok(res.body.includes('Image not found'));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('GET /nonexistent.html returns an abuse error', {timeout: 500}, async () => {
|
|
354
382
|
const res = await request('GET', '/nonexistent.html');
|
|
355
383
|
assert.equal(res.statusCode, 400);
|
|
356
384
|
assert.ok(res.body.includes('Invalid request'));
|
|
@@ -366,7 +394,7 @@ test('GET /forbidden-path returns an invalid GET request error', async () => {
|
|
|
366
394
|
|
|
367
395
|
test('POST /requestTest.html with valid data returns an HTML page', async () => {
|
|
368
396
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
369
|
-
|
|
397
|
+
description: `Unique Test Page ${uniqueStamp}`,
|
|
370
398
|
url: `https://example.com/unique-${uniqueStamp}`,
|
|
371
399
|
why: 'Because accessibility matters'
|
|
372
400
|
});
|
|
@@ -376,7 +404,7 @@ test('POST /requestTest.html with valid data returns an HTML page', async () =>
|
|
|
376
404
|
|
|
377
405
|
test('POST /requestTest.html with an already-tested URL returns an error', async () => {
|
|
378
406
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
379
|
-
|
|
407
|
+
description: 'Mixed Outcomes Page',
|
|
380
408
|
url: 'https://example.com/mixed',
|
|
381
409
|
why: 'Because accessibility matters'
|
|
382
410
|
});
|
|
@@ -386,17 +414,23 @@ test('POST /requestTest.html with an already-tested URL returns an error', async
|
|
|
386
414
|
|
|
387
415
|
test('POST /requestTest.html with invalid data returns an error', async () => {
|
|
388
416
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
389
|
-
|
|
417
|
+
description: '',
|
|
390
418
|
url: 'not-a-url',
|
|
391
419
|
why: ''
|
|
392
420
|
});
|
|
393
421
|
assert.equal(res.statusCode, 400);
|
|
394
|
-
assert.ok(res.body.includes('Invalid test
|
|
422
|
+
assert.ok(res.body.includes('Invalid test request'));
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('POST /requestTest.html with an unreadable body returns an error', async () => {
|
|
426
|
+
const res = await request('POST', '/requestTest.html', 'rawbody', {'content-type': 'text/plain'});
|
|
427
|
+
assert.equal(res.statusCode, 400);
|
|
428
|
+
assert.ok(res.body.includes('Unreadable request body'));
|
|
395
429
|
});
|
|
396
430
|
|
|
397
431
|
test('POST /requestRetest.html/260202T0000/new with valid data returns HTML', async () => {
|
|
398
|
-
// Reset
|
|
399
|
-
await fs.writeFile(
|
|
432
|
+
// Reset testRequests.json to avoid duplicate-request errors from prior tests.
|
|
433
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
400
434
|
const res = await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
401
435
|
why: 'Because the report is obsolete and needs refreshing'
|
|
402
436
|
});
|
|
@@ -409,7 +443,7 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
409
443
|
why: 'Because'
|
|
410
444
|
});
|
|
411
445
|
assert.equal(res.statusCode, 400);
|
|
412
|
-
assert.ok(res.body.includes('Invalid retest
|
|
446
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
413
447
|
});
|
|
414
448
|
|
|
415
449
|
test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
@@ -463,6 +497,36 @@ test('POST /worker/job without authentication returns 401', async () => {
|
|
|
463
497
|
assert.ok(body.error.message.includes('Unauthorized'));
|
|
464
498
|
});
|
|
465
499
|
|
|
500
|
+
test('POST /worker/job when TESTARO_WORKERS is unset returns 401', async () => {
|
|
501
|
+
const saved = process.env.TESTARO_WORKERS;
|
|
502
|
+
delete process.env.TESTARO_WORKERS;
|
|
503
|
+
try {
|
|
504
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
505
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
506
|
+
authorization: `Basic ${auth}`
|
|
507
|
+
});
|
|
508
|
+
assert.equal(res.statusCode, 401);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
process.env.TESTARO_WORKERS = saved;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('POST /worker/job when TESTARO_WORKERS is invalid JSON returns 401', async () => {
|
|
516
|
+
const saved = process.env.TESTARO_WORKERS;
|
|
517
|
+
process.env.TESTARO_WORKERS = 'not valid JSON {';
|
|
518
|
+
try {
|
|
519
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
520
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
521
|
+
authorization: `Basic ${auth}`
|
|
522
|
+
});
|
|
523
|
+
assert.equal(res.statusCode, 401);
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
process.env.TESTARO_WORKERS = saved;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
466
530
|
test('POST /worker/job with valid authentication returns a job or no-job response', async () => {
|
|
467
531
|
// Clean up any claimed jobs left by prior tests.
|
|
468
532
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
@@ -514,6 +578,26 @@ test('PUT / returns an invalid method error', async () => {
|
|
|
514
578
|
assert.ok(res.body.includes('Invalid request method'));
|
|
515
579
|
});
|
|
516
580
|
|
|
581
|
+
// TESTS: requestHandler catch boundary
|
|
582
|
+
|
|
583
|
+
test('GET /listReports.html returns a 500 error when an unexpected internal error occurs', {timeout: 500}, async () => {
|
|
584
|
+
// Corrupt testRequests.json so that getTestRequests throws, an error that should never occur in normal
|
|
585
|
+
// operation and so is not handled by handleRequest itself, only by requestHandler's
|
|
586
|
+
// outer catch boundary.
|
|
587
|
+
const backup = await fs.readFile(testRequestsPath, 'utf8');
|
|
588
|
+
await fs.writeFile(testRequestsPath, 'not valid json');
|
|
589
|
+
try {
|
|
590
|
+
const res = await request('GET', '/listReports.html');
|
|
591
|
+
assert.equal(res.statusCode, 500);
|
|
592
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
593
|
+
const body = jsonBody(res);
|
|
594
|
+
assert.ok(body.error);
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
await fs.writeFile(testRequestsPath, backup);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
517
601
|
// TESTS: serveError behavior
|
|
518
602
|
|
|
519
603
|
test('serveError sends JSON for agent requests (isHumanUser = false)', async () => {
|
|
@@ -530,10 +614,10 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
530
614
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
531
615
|
});
|
|
532
616
|
|
|
533
|
-
// TESTS:
|
|
617
|
+
// TESTS: requestAction.html
|
|
534
618
|
|
|
535
|
-
test('POST /
|
|
536
|
-
const res = await formRequest('POST', '/
|
|
619
|
+
test('POST /requestAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
|
|
620
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
537
621
|
target: 'https://example.com\tTest Page',
|
|
538
622
|
authCode: 'wrong-code'
|
|
539
623
|
});
|
|
@@ -541,13 +625,13 @@ test('POST /recAction.html with invalid auth code returns an error', {timeout: 5
|
|
|
541
625
|
assert.ok(res.body.includes('Invalid test order'));
|
|
542
626
|
});
|
|
543
627
|
|
|
544
|
-
test('POST /
|
|
628
|
+
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML', async () => {
|
|
545
629
|
await formRequest('POST', '/requestTest.html', {
|
|
546
|
-
|
|
630
|
+
description: `Reject Test Page ${uniqueStamp}`,
|
|
547
631
|
url: `https://example.com/reject-${uniqueStamp}`,
|
|
548
632
|
why: 'Because accessibility matters'
|
|
549
633
|
});
|
|
550
|
-
const res = await formRequest('POST', '/
|
|
634
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
551
635
|
target: `https://example.com/reject-${uniqueStamp}`,
|
|
552
636
|
authCode: 'test-auth-code'
|
|
553
637
|
});
|
|
@@ -555,31 +639,38 @@ test('POST /recAction.html with valid auth code and rejection (no what) returns
|
|
|
555
639
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
556
640
|
});
|
|
557
641
|
|
|
558
|
-
// TESTS:
|
|
642
|
+
// TESTS: tutorialWeb (web user tutorial)
|
|
559
643
|
|
|
560
|
-
test('
|
|
561
|
-
const res = await request('
|
|
562
|
-
|
|
644
|
+
test('GET /tutorialWeb.html returns HTML', async () => {
|
|
645
|
+
const res = await request('GET', '/tutorialWeb.html');
|
|
646
|
+
assert.equal(res.statusCode, 200);
|
|
647
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('POST /tutorialWebComment.html with content returns JSON', async () => {
|
|
651
|
+
const res = await request('POST', '/tutorialWebComment.html', {
|
|
652
|
+
content: `This is a test comment ${uniqueStamp}`
|
|
563
653
|
});
|
|
564
654
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
655
|
+
assert.deepEqual(JSON.parse(res.body), {status: 'ok'});
|
|
565
656
|
});
|
|
566
657
|
|
|
567
658
|
// TESTS: additional branch coverage
|
|
568
659
|
|
|
569
|
-
test('GET /fullReport.json/260101T0007/hid returns an
|
|
660
|
+
test('GET /fullReport.json/260101T0007/hid returns an error for an unavailable report', async () => {
|
|
570
661
|
const res = await request('GET', '/fullReport.json/260101T0007/hid');
|
|
571
662
|
assert.equal(res.statusCode, 400);
|
|
572
663
|
assert.ok(res.body.includes('Invalid request'));
|
|
573
664
|
});
|
|
574
665
|
|
|
575
|
-
test('POST /
|
|
576
|
-
await fs.writeFile(
|
|
666
|
+
test('POST /requestAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
|
|
667
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
577
668
|
await formRequest('POST', '/requestTest.html', {
|
|
578
|
-
|
|
669
|
+
description: `Approval Test Page ${uniqueStamp}`,
|
|
579
670
|
url: `https://example.com/approval-${uniqueStamp}`,
|
|
580
671
|
why: 'Because accessibility matters'
|
|
581
672
|
});
|
|
582
|
-
const res = await formRequest('POST', '/
|
|
673
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
583
674
|
target: `https://example.com/approval-${uniqueStamp}\tApproval Test Page ${uniqueStamp}`,
|
|
584
675
|
authCode: 'test-auth-code'
|
|
585
676
|
});
|
|
@@ -595,10 +686,10 @@ test('POST /reannotate.html with invalid auth code returns an error page', async
|
|
|
595
686
|
});
|
|
596
687
|
|
|
597
688
|
test('POST /reannotate.html with valid auth code serves the answer page', async () => {
|
|
598
|
-
// Back up all fixture reports, because
|
|
689
|
+
// Back up all fixture reports, because reannotation modifies them in place.
|
|
599
690
|
const reportsDir = path.join(fixtureDBDir, 'reports');
|
|
600
691
|
const reportFiles = await fs.readdir(reportsDir);
|
|
601
|
-
const backups = {};
|
|
692
|
+
const backups: Record<string, string> = {};
|
|
602
693
|
for (const file of reportFiles) {
|
|
603
694
|
backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
|
|
604
695
|
}
|
|
@@ -613,7 +704,7 @@ test('POST /reannotate.html with valid auth code serves the answer page', async
|
|
|
613
704
|
finally {
|
|
614
705
|
// Restore all fixture reports.
|
|
615
706
|
for (const file of reportFiles) {
|
|
616
|
-
await fs.writeFile(path.join(reportsDir, file), backups[file]);
|
|
707
|
+
await fs.writeFile(path.join(reportsDir, file), backups[file]!);
|
|
617
708
|
}
|
|
618
709
|
}
|
|
619
710
|
});
|
|
@@ -628,7 +719,7 @@ test('POST /renewWCAG.html with invalid auth code returns an error page', async
|
|
|
628
719
|
test('POST /renewWCAG.html with valid auth code serves the answer page', async () => {
|
|
629
720
|
// Mock fetch to avoid a network dependency.
|
|
630
721
|
const originalFetch = global.fetch;
|
|
631
|
-
const wcagMapPath = path.join(
|
|
722
|
+
const wcagMapPath = path.join(import.meta.dirname, 'wcagMap.json');
|
|
632
723
|
const wcagMapBackup = await fs.readFile(wcagMapPath, 'utf8');
|
|
633
724
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
634
725
|
global.fetch = async () => ({
|
|
@@ -649,21 +740,56 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
649
740
|
}
|
|
650
741
|
});
|
|
651
742
|
|
|
652
|
-
test('POST /
|
|
653
|
-
const res = await request('POST', '/
|
|
743
|
+
test('POST /tutorialWebComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
744
|
+
const res = await request('POST', '/tutorialWebComment.html', {
|
|
745
|
+
content: ''
|
|
746
|
+
});
|
|
747
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// TESTS: tutorialAI (AI agent configuration tutorial)
|
|
751
|
+
|
|
752
|
+
test('GET /tutorialAI.html returns HTML', async () => {
|
|
753
|
+
const res = await request('GET', '/tutorialAI.html');
|
|
754
|
+
assert.equal(res.statusCode, 200);
|
|
755
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test('POST /tutorialAIComment.html with content returns JSON', async () => {
|
|
759
|
+
const res = await request('POST', '/tutorialAIComment.html', {
|
|
760
|
+
content: `This is a test AI tutorial comment ${uniqueStamp}`
|
|
761
|
+
});
|
|
762
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
763
|
+
assert.deepEqual(JSON.parse(res.body), {status: 'ok'});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('POST /tutorialAIComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
767
|
+
const res = await request('POST', '/tutorialAIComment.html', {
|
|
654
768
|
content: ''
|
|
655
769
|
});
|
|
656
770
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
657
771
|
});
|
|
658
772
|
|
|
773
|
+
test('GET /qai redirects to /tutorialAI.html', async () => {
|
|
774
|
+
const res = await request('GET', '/qai', {}, {followRedirects: false});
|
|
775
|
+
assert.equal(res.statusCode, 301);
|
|
776
|
+
assert.equal(res.headers['location'], '/tutorialAI.html');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('GET /qai/comments redirects to /tutorialAI.html', async () => {
|
|
780
|
+
const res = await request('GET', '/qai/comments', {}, {followRedirects: false});
|
|
781
|
+
assert.equal(res.statusCode, 301);
|
|
782
|
+
assert.equal(res.headers['location'], '/tutorialAI.html');
|
|
783
|
+
});
|
|
784
|
+
|
|
659
785
|
test('POST /requestTest.html with non-https URL returns an error', async () => {
|
|
660
786
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
661
|
-
|
|
787
|
+
description: 'Test Page',
|
|
662
788
|
url: 'http://example.com',
|
|
663
789
|
why: 'Because accessibility matters'
|
|
664
790
|
});
|
|
665
791
|
assert.equal(res.statusCode, 400);
|
|
666
|
-
assert.ok(res.body.includes('Invalid test
|
|
792
|
+
assert.ok(res.body.includes('Invalid test request'));
|
|
667
793
|
});
|
|
668
794
|
|
|
669
795
|
test('POST /requestRetest.html/260202T0000/new with missing why returns an error', async () => {
|
|
@@ -671,7 +797,7 @@ test('POST /requestRetest.html/260202T0000/new with missing why returns an error
|
|
|
671
797
|
why: ''
|
|
672
798
|
});
|
|
673
799
|
assert.equal(res.statusCode, 400);
|
|
674
|
-
assert.ok(res.body.includes('Invalid retest
|
|
800
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
675
801
|
});
|
|
676
802
|
|
|
677
803
|
test('POST /worker/report with valid authentication and wrong worker returns an error', async () => {
|
|
@@ -689,6 +815,31 @@ test('POST /worker/report with valid authentication and wrong worker returns an
|
|
|
689
815
|
assert.ok(body.error);
|
|
690
816
|
});
|
|
691
817
|
|
|
818
|
+
test('POST /worker/report with a claimed job file that is not valid JSON returns a 500 error', {timeout: 500}, async () => {
|
|
819
|
+
// A corrupt claimed-job file is not the normal "no such claim" outcome (ENOENT), so it
|
|
820
|
+
// should propagate as an unexpected error rather than being treated as an unassigned job.
|
|
821
|
+
const jobID = '990101T0002-bad';
|
|
822
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
823
|
+
const jobPath = path.join(claimedDir, `${jobID}.json`);
|
|
824
|
+
await fs.writeFile(jobPath, 'not valid json');
|
|
825
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
826
|
+
const report = {
|
|
827
|
+
id: jobID,
|
|
828
|
+
target: {what: 'Test', url: 'https://example.com/test'}
|
|
829
|
+
};
|
|
830
|
+
try {
|
|
831
|
+
const res = await request('POST', '/worker/report', {report}, {
|
|
832
|
+
authorization: `Basic ${auth}`
|
|
833
|
+
});
|
|
834
|
+
assert.equal(res.statusCode, 500);
|
|
835
|
+
const body = jsonBody(res);
|
|
836
|
+
assert.ok(body.error);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
await fs.unlink(jobPath).catch(() => {});
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
692
843
|
test('POST /worker/job with wrong secret returns 401', async () => {
|
|
693
844
|
const auth = Buffer.from('worker1:wrongsecret').toString('base64');
|
|
694
845
|
const res = await request('POST', '/worker/job', {}, {
|
|
@@ -743,7 +894,7 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
743
894
|
const body = jsonBody(res);
|
|
744
895
|
assert.ok(body.error.message.includes('has not completed job'));
|
|
745
896
|
// Wait for the async rename to complete.
|
|
746
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
897
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
747
898
|
// The job should have been moved to failed.
|
|
748
899
|
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
749
900
|
assert.ok(failedExists, 'Job should be moved to failed directory');
|
|
@@ -778,7 +929,7 @@ test('POST /worker/job with a queued job assigns it to the worker', {timeout: 50
|
|
|
778
929
|
assert.equal(body.id, '260101T0000-mix');
|
|
779
930
|
assert.equal(body.sources.worker, 'Worker One');
|
|
780
931
|
// Wait for the async unlink to complete.
|
|
781
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
932
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
782
933
|
// The job should have been moved from queue to claimed.
|
|
783
934
|
const queueExists = await fs.access(path.join(queueDir, jobFile)).then(() => true).catch(() => false);
|
|
784
935
|
assert.equal(queueExists, false, 'Job should be removed from queue');
|
|
@@ -823,12 +974,57 @@ test('POST /worker/report with valid authentication and valid claimed job proces
|
|
|
823
974
|
assert.equal(body.status, 'ok');
|
|
824
975
|
});
|
|
825
976
|
|
|
977
|
+
test('POST /worker/report with a claimed job but an unusable report returns an error and reclassifies the job', {timeout: 500}, async () => {
|
|
978
|
+
// Use a unique job ID that does not conflict with existing fixtures.
|
|
979
|
+
const jobID = '990101T0001-unu';
|
|
980
|
+
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
981
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
982
|
+
const failedDir = path.join(fixtureDBDir, 'jobs', 'failed');
|
|
983
|
+
const jobFile = `${jobID}.json`;
|
|
984
|
+
const claimedJobPath = path.join(claimedDir, jobFile);
|
|
985
|
+
const failedJobPath = path.join(failedDir, jobFile);
|
|
986
|
+
// Clean up any leftover files.
|
|
987
|
+
await fs.unlink(reportPath).catch(() => {});
|
|
988
|
+
await fs.unlink(failedJobPath).catch(() => {});
|
|
989
|
+
// Create a claimed job assigned to Worker One.
|
|
990
|
+
await fs.writeFile(claimedJobPath, JSON.stringify({
|
|
991
|
+
id: jobID,
|
|
992
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
993
|
+
sources: {worker: 'Worker One'}
|
|
994
|
+
}));
|
|
995
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
996
|
+
// A report that is syntactically valid (has id, target.what, target.url) but is not
|
|
997
|
+
// usable by Kilotest, because it has no acts, jobData, or catalog.
|
|
998
|
+
const report = {
|
|
999
|
+
id: jobID,
|
|
1000
|
+
target: {what: 'Test', url: 'https://example.com/test'}
|
|
1001
|
+
};
|
|
1002
|
+
const res = await request('POST', '/worker/report', {report}, {
|
|
1003
|
+
authorization: `Basic ${auth}`
|
|
1004
|
+
});
|
|
1005
|
+
assert.equal(res.statusCode, 400);
|
|
1006
|
+
const body = jsonBody(res);
|
|
1007
|
+
assert.ok(body.error.message.includes('not usable'));
|
|
1008
|
+
// The report should not have been recorded.
|
|
1009
|
+
const reportExists = await fs.access(reportPath).then(() => true).catch(() => false);
|
|
1010
|
+
assert.equal(reportExists, false, 'Unusable report should not be saved');
|
|
1011
|
+
// Wait for the async rename to complete.
|
|
1012
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
1013
|
+
// The job should have been reclassified as failed rather than left claimed.
|
|
1014
|
+
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
1015
|
+
assert.ok(failedExists, 'Job should be moved to the failed directory');
|
|
1016
|
+
const claimedExists = await fs.access(claimedJobPath).then(() => true).catch(() => false);
|
|
1017
|
+
assert.equal(claimedExists, false, 'Job should be removed from the claimed directory');
|
|
1018
|
+
// Clean up.
|
|
1019
|
+
await fs.unlink(failedJobPath).catch(() => {});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
826
1022
|
// TESTS: remaining error branches and web pages
|
|
827
1023
|
|
|
828
1024
|
test('GET /style.css with a read error returns an error page', async () => {
|
|
829
1025
|
// Temporarily rename style.css to trigger a read error.
|
|
830
|
-
const stylePath = path.join(
|
|
831
|
-
const tempPath = path.join(
|
|
1026
|
+
const stylePath = path.join(import.meta.dirname, 'style.css');
|
|
1027
|
+
const tempPath = path.join(import.meta.dirname, 'style.css.bak');
|
|
832
1028
|
await fs.rename(stylePath, tempPath);
|
|
833
1029
|
try {
|
|
834
1030
|
const res = await request('GET', '/style.css');
|
|
@@ -840,22 +1036,26 @@ test('GET /style.css with a read error returns an error page', async () => {
|
|
|
840
1036
|
}
|
|
841
1037
|
});
|
|
842
1038
|
|
|
843
|
-
|
|
1039
|
+
// requestTest.html and requestRetest.html are POST-only routes (see the
|
|
1040
|
+
// `routes.POST` list above). They also match the generic '*.html*' GET pattern,
|
|
1041
|
+
// but the generic `pageName.endsWith('.html')` branch of the GET dispatcher
|
|
1042
|
+
// excludes any path that is POST-only, since such a path's answer handler
|
|
1043
|
+
// expects POST's argument list and performs no GET-appropriate rendering.
|
|
1044
|
+
|
|
1045
|
+
test('GET /requestTest.html without arguments is rejected as an invalid GET request', async () => {
|
|
844
1046
|
const res = await request('GET', '/requestTest.html');
|
|
845
1047
|
assert.equal(res.statusCode, 400);
|
|
846
|
-
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
847
1048
|
});
|
|
848
1049
|
|
|
849
|
-
test('GET /requestRetest.html/260202T0000/new
|
|
1050
|
+
test('GET /requestRetest.html/260202T0000/new is rejected as an invalid GET request', async () => {
|
|
850
1051
|
const res = await request('GET', '/requestRetest.html/260202T0000/new');
|
|
851
1052
|
assert.equal(res.statusCode, 400);
|
|
852
|
-
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
853
1053
|
});
|
|
854
1054
|
|
|
855
1055
|
// TESTS: GET HTML page routing
|
|
856
1056
|
// These verify that the HTTP routing layer connects each URL to the correct
|
|
857
1057
|
// handler and returns HTML with a 200 status. The handler logic itself is
|
|
858
|
-
// tested by the unit tests in web/*/*.test.
|
|
1058
|
+
// tested by the unit tests in web/*/*.test.cjs.
|
|
859
1059
|
|
|
860
1060
|
const htmlPagePaths = [
|
|
861
1061
|
'/listReports.html',
|
|
@@ -863,7 +1063,8 @@ const htmlPagePaths = [
|
|
|
863
1063
|
'/listDiagnoses.html/linkNoText/260101T0000/mix/0',
|
|
864
1064
|
'/enqueueForm.html',
|
|
865
1065
|
'/manage.html',
|
|
866
|
-
'/
|
|
1066
|
+
'/tutorialWeb.html',
|
|
1067
|
+
'/tutorialAI.html',
|
|
867
1068
|
'/listTopIssues.html',
|
|
868
1069
|
'/reannotateForm.html',
|
|
869
1070
|
'/renewWCAGForm.html',
|
|
@@ -876,17 +1077,17 @@ const htmlPagePaths = [
|
|
|
876
1077
|
];
|
|
877
1078
|
|
|
878
1079
|
for (const pagePath of htmlPagePaths) {
|
|
879
|
-
test(`GET ${pagePath} serves a generated HTML page`, async () => {
|
|
1080
|
+
test(`GET ${pagePath} serves a generated HTML page`, {timeout: 500}, async () => {
|
|
880
1081
|
const res = await request('GET', pagePath);
|
|
881
1082
|
assert.equal(res.statusCode, 200);
|
|
882
1083
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
883
1084
|
});
|
|
884
1085
|
}
|
|
885
1086
|
|
|
886
|
-
test('GET /enqueueForm.html shows
|
|
887
|
-
await fs.writeFile(
|
|
1087
|
+
test('GET /enqueueForm.html shows requests when testRequests.json has entries', async () => {
|
|
1088
|
+
await fs.writeFile(testRequestsPath, JSON.stringify({
|
|
888
1089
|
'https://example.com/enqueue-test': [
|
|
889
|
-
{
|
|
1090
|
+
{description: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
|
|
890
1091
|
]
|
|
891
1092
|
}));
|
|
892
1093
|
const res = await request('GET', '/enqueueForm.html');
|
|
@@ -909,81 +1110,137 @@ test('GET /test.html.bak matches isPathAllowed but falls through to catch-all',
|
|
|
909
1110
|
assert.ok(res.body.includes('Invalid GET request'));
|
|
910
1111
|
});
|
|
911
1112
|
|
|
912
|
-
test('GET /mcp returns a
|
|
913
|
-
const res = await request('GET', '/mcp');
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
assert.
|
|
1113
|
+
test('GET /mcp with an SSE-capable Accept header returns a JSON-RPC 405 error instead of opening an SSE stream', async () => {
|
|
1114
|
+
const res = await request('GET', '/mcp', null, {accept: 'application/json, text/event-stream'});
|
|
1115
|
+
assert.equal(res.statusCode, 405);
|
|
1116
|
+
const body = jsonBody(res);
|
|
1117
|
+
assert.equal(body.jsonrpc, '2.0');
|
|
1118
|
+
assert.equal(body.error.code, -32000);
|
|
917
1119
|
});
|
|
918
1120
|
|
|
919
|
-
test('
|
|
1121
|
+
test('GET /mcp without an SSE-capable Accept header serves an HTML explanation page instead of the MCP protocol response', async () => {
|
|
1122
|
+
const res = await request('GET', '/mcp', null, {accept: 'text/html'});
|
|
1123
|
+
assert.equal(res.statusCode, 400);
|
|
1124
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1125
|
+
assert.ok(res.body.includes('/tutorialAI.html'));
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test('POST /mcp without an SSE-capable Accept header returns a JSON-RPC 406 error', async () => {
|
|
1129
|
+
const res = await request('POST', '/mcp', {jsonrpc: '2.0', method: 'initialize', id: 1, params: {}});
|
|
1130
|
+
assert.equal(res.statusCode, 406);
|
|
1131
|
+
assert.equal(jsonBody(res).error.code, -32000);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test('POST /mcp with malformed JSON returns a JSON-RPC 400 parse error', async () => {
|
|
1135
|
+
const res = await request('POST', '/mcp', '{not json', {
|
|
1136
|
+
'content-type': 'application/json',
|
|
1137
|
+
accept: 'application/json, text/event-stream'
|
|
1138
|
+
});
|
|
1139
|
+
assert.equal(res.statusCode, 400);
|
|
1140
|
+
assert.equal(jsonBody(res).error.code, -32700);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
test('POST /mcp with a non-JSON-RPC body returns a JSON-RPC 400 error', async () => {
|
|
1144
|
+
const res = await request('POST', '/mcp', {foo: 'bar'}, {accept: 'application/json, text/event-stream'});
|
|
1145
|
+
assert.equal(res.statusCode, 400);
|
|
1146
|
+
assert.equal(jsonBody(res).error.code, -32700);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('POST /mcp with a non-JSON content type returns a JSON-RPC 415 error', async () => {
|
|
1150
|
+
const res = await request('POST', '/mcp', 'x=1', {
|
|
1151
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
1152
|
+
accept: 'application/json, text/event-stream'
|
|
1153
|
+
});
|
|
1154
|
+
assert.equal(res.statusCode, 415);
|
|
1155
|
+
assert.equal(jsonBody(res).error.code, -32000);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test('POST /mcp with an unknown JSON-RPC method returns a -32601 error', async () => {
|
|
1159
|
+
const res = await request('POST', '/mcp', {jsonrpc: '2.0', method: 'bogus/method', id: 9}, {
|
|
1160
|
+
accept: 'application/json, text/event-stream'
|
|
1161
|
+
});
|
|
1162
|
+
assert.equal(res.statusCode, 200);
|
|
1163
|
+
const dataLine = res.body.split('\n').find((line: string) => line.startsWith('data: '));
|
|
1164
|
+
assert.ok(dataLine);
|
|
1165
|
+
const message = JSON.parse(dataLine.slice(6));
|
|
1166
|
+
assert.equal(message.error.code, -32601);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
test('POST /mcp with a valid initialize request returns server info via SSE', async () => {
|
|
920
1170
|
const res = await request('POST', '/mcp', {
|
|
921
1171
|
jsonrpc: '2.0',
|
|
922
1172
|
method: 'initialize',
|
|
923
1173
|
id: 1,
|
|
924
1174
|
params: {
|
|
925
|
-
protocolVersion: '
|
|
1175
|
+
protocolVersion: '2025-06-18',
|
|
926
1176
|
capabilities: {},
|
|
927
1177
|
clientInfo: {name: 'test-client', version: '1.0'}
|
|
928
1178
|
}
|
|
929
1179
|
}, {
|
|
930
1180
|
accept: 'application/json, text/event-stream'
|
|
931
1181
|
});
|
|
932
|
-
|
|
933
|
-
|
|
1182
|
+
assert.equal(res.statusCode, 200);
|
|
1183
|
+
const dataLine = res.body.split('\n').find((line: string) => line.startsWith('data: '));
|
|
1184
|
+
assert.ok(dataLine);
|
|
1185
|
+
const message = JSON.parse(dataLine.slice(6));
|
|
1186
|
+
assert.equal(message.result.serverInfo.name, 'Kilotest');
|
|
934
1187
|
});
|
|
935
1188
|
|
|
936
1189
|
// TESTS: answer error branches
|
|
937
1190
|
|
|
938
1191
|
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
939
|
-
await fs.writeFile(
|
|
940
|
-
// First request to create the
|
|
1192
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1193
|
+
// First request to create the request. processTestRequest treats a request as a
|
|
1194
|
+
// duplicate only when description, URL, AND reason all match an existing pending
|
|
1195
|
+
// request, so the reason must be identical between the two requests here.
|
|
941
1196
|
await formRequest('POST', '/requestTest.html', {
|
|
942
|
-
|
|
1197
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
943
1198
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
944
1199
|
why: 'Because accessibility matters'
|
|
945
1200
|
});
|
|
946
|
-
// Second request
|
|
1201
|
+
// Second, identical request should get a duplicate error.
|
|
947
1202
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
948
|
-
|
|
1203
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
949
1204
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
950
|
-
why: 'Because accessibility matters
|
|
1205
|
+
why: 'Because accessibility matters'
|
|
951
1206
|
});
|
|
952
1207
|
assert.equal(res.statusCode, 400);
|
|
953
|
-
assert.ok(res.body.includes('
|
|
1208
|
+
assert.ok(res.body.includes('Test request duplicates an already submitted request'));
|
|
954
1209
|
});
|
|
955
1210
|
|
|
956
1211
|
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
957
|
-
await fs.writeFile(
|
|
958
|
-
// First retest to create the recommendation.
|
|
1212
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1213
|
+
// First retest to create the recommendation. As above, the reason must match
|
|
1214
|
+
// exactly between the two requests for processTestRequest to treat the second as
|
|
1215
|
+
// a duplicate rather than a second, independent (and thus also 'ok') request.
|
|
959
1216
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
960
1217
|
why: 'Because the report is obsolete and needs refreshing'
|
|
961
1218
|
});
|
|
962
|
-
// Second retest
|
|
1219
|
+
// Second, identical retest of the same report should get a duplicate error.
|
|
963
1220
|
const res = await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
964
|
-
why: 'Because the report is obsolete and needs refreshing
|
|
1221
|
+
why: 'Because the report is obsolete and needs refreshing'
|
|
965
1222
|
});
|
|
966
1223
|
assert.equal(res.statusCode, 400);
|
|
967
|
-
assert.ok(res.body.includes('
|
|
1224
|
+
assert.ok(res.body.includes('Test request duplicates an already submitted request'));
|
|
968
1225
|
});
|
|
969
1226
|
|
|
970
|
-
test('POST /
|
|
971
|
-
await fs.writeFile(
|
|
972
|
-
// Create a
|
|
1227
|
+
test('POST /requestAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
|
|
1228
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1229
|
+
// Create a request.
|
|
973
1230
|
await formRequest('POST', '/requestTest.html', {
|
|
974
|
-
|
|
1231
|
+
description: `Action Dup Page ${uniqueStamp}`,
|
|
975
1232
|
url: `https://example.com/action-dup-${uniqueStamp}`,
|
|
976
1233
|
why: 'Because accessibility matters'
|
|
977
1234
|
});
|
|
978
1235
|
// Approve it once.
|
|
979
|
-
await formRequest('POST', '/
|
|
1236
|
+
await formRequest('POST', '/requestAction.html', {
|
|
980
1237
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
981
1238
|
authCode: 'test-auth-code',
|
|
982
1239
|
what: 'yes'
|
|
983
1240
|
});
|
|
984
|
-
// Approve it again (the
|
|
985
|
-
// This test covers the
|
|
986
|
-
const res = await formRequest('POST', '/
|
|
1241
|
+
// Approve it again (the requests may have been cleared, so this may succeed or fail).
|
|
1242
|
+
// This test covers the requestAction answer error branch.
|
|
1243
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
987
1244
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
988
1245
|
authCode: 'test-auth-code',
|
|
989
1246
|
what: 'yes'
|
|
@@ -992,10 +1249,10 @@ test('POST /recAction.html with valid auth code and approval of a duplicate retu
|
|
|
992
1249
|
assert.ok(res.statusCode === 200 || res.statusCode === 400);
|
|
993
1250
|
});
|
|
994
1251
|
|
|
995
|
-
test('POST /
|
|
1252
|
+
test('POST /requestAction.html with valid auth code and approval of an invalid URL returns an error', async () => {
|
|
996
1253
|
// A URL that starts with https:// but is not a valid URL causes
|
|
997
1254
|
// enqueue.answer to return status error.
|
|
998
|
-
const res = await formRequest('POST', '/
|
|
1255
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
999
1256
|
target: 'https://\tTest Page',
|
|
1000
1257
|
authCode: 'test-auth-code',
|
|
1001
1258
|
what: 'yes'
|
|
@@ -1006,22 +1263,22 @@ test('POST /recAction.html with valid auth code and approval of an invalid URL r
|
|
|
1006
1263
|
|
|
1007
1264
|
// UNIT TESTS FOR serveError
|
|
1008
1265
|
|
|
1009
|
-
const mockRes = () => {
|
|
1010
|
-
const state = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
1266
|
+
const mockRes = (): any => {
|
|
1267
|
+
const state: any = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
1011
1268
|
return {
|
|
1012
1269
|
get statusCode() {return state.statusCode;},
|
|
1013
|
-
set statusCode(value) {state.statusCode = value;},
|
|
1014
|
-
setHeader(name, value) {state.headers[name] = value;},
|
|
1015
|
-
end(data) {state.body = data; state.writableEnded = true;},
|
|
1270
|
+
set statusCode(value: any) {state.statusCode = value;},
|
|
1271
|
+
setHeader(name: any, value: any) {state.headers[name] = value;},
|
|
1272
|
+
end(data: any) {state.body = data; state.writableEnded = true;},
|
|
1016
1273
|
_state: state
|
|
1017
1274
|
};
|
|
1018
1275
|
};
|
|
1019
1276
|
|
|
1020
1277
|
test('serveError does not write to a response that has already ended', async () => {
|
|
1021
1278
|
let wrote = false;
|
|
1022
|
-
const mockResponse = {
|
|
1279
|
+
const mockResponse: any = {
|
|
1023
1280
|
writableEnded: true,
|
|
1024
|
-
set statusCode(value) {
|
|
1281
|
+
set statusCode(value: any) {
|
|
1025
1282
|
wrote = true;
|
|
1026
1283
|
},
|
|
1027
1284
|
setHeader() {
|
|
@@ -1063,7 +1320,7 @@ test('getAbuseError uses unknown IP when no forwarding header or remote address'
|
|
|
1063
1320
|
headers: {},
|
|
1064
1321
|
socket: {remoteAddress: undefined}
|
|
1065
1322
|
};
|
|
1066
|
-
const result = getAbuseError(mockRequest, 'test reason');
|
|
1323
|
+
const result = getAbuseError(mockRequest as any, 'test reason');
|
|
1067
1324
|
assert.equal(result['IP address'], 'unknown');
|
|
1068
1325
|
assert.equal(result.reason, 'test reason');
|
|
1069
1326
|
});
|
|
@@ -1090,16 +1347,16 @@ test('startServer starts an HTTP server when protocol is http', {timeout: 500},
|
|
|
1090
1347
|
assert.equal(typeof server.listen, 'function');
|
|
1091
1348
|
// Verify the server is listening by making a request.
|
|
1092
1349
|
const address = server.address();
|
|
1093
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1350
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1094
1351
|
}
|
|
1095
1352
|
finally {
|
|
1096
|
-
server
|
|
1097
|
-
await new Promise(resolve => {
|
|
1353
|
+
server?.closeAllConnections?.();
|
|
1354
|
+
await new Promise<void>(resolve => {
|
|
1098
1355
|
const timer = setTimeout(() => {
|
|
1099
|
-
server
|
|
1356
|
+
server?.closeAllConnections?.();
|
|
1100
1357
|
resolve();
|
|
1101
1358
|
}, 1000);
|
|
1102
|
-
server
|
|
1359
|
+
server?.close(() => {
|
|
1103
1360
|
clearTimeout(timer);
|
|
1104
1361
|
resolve();
|
|
1105
1362
|
});
|
|
@@ -1116,12 +1373,12 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1116
1373
|
delete process.env.PORT;
|
|
1117
1374
|
try {
|
|
1118
1375
|
const server = await indexModule.serve(http, {});
|
|
1119
|
-
let bindError = null;
|
|
1376
|
+
let bindError: any = null;
|
|
1120
1377
|
server.on('error', error => {
|
|
1121
1378
|
bindError = error;
|
|
1122
1379
|
});
|
|
1123
1380
|
// Wait briefly for either successful binding or an EADDRINUSE error.
|
|
1124
|
-
await new Promise(resolve => {
|
|
1381
|
+
await new Promise<void>(resolve => {
|
|
1125
1382
|
const timer = setTimeout(resolve, 200);
|
|
1126
1383
|
server.on('listening', () => {
|
|
1127
1384
|
clearTimeout(timer);
|
|
@@ -1133,10 +1390,10 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1133
1390
|
}
|
|
1134
1391
|
else {
|
|
1135
1392
|
const address = server.address();
|
|
1136
|
-
assert.ok(typeof address === 'object' && address.port === 3000);
|
|
1393
|
+
assert.ok(typeof address === 'object' && address !== null && address.port === 3000);
|
|
1137
1394
|
}
|
|
1138
1395
|
server.closeAllConnections?.();
|
|
1139
|
-
await new Promise(resolve => {
|
|
1396
|
+
await new Promise<void>(resolve => {
|
|
1140
1397
|
const timer = setTimeout(() => {
|
|
1141
1398
|
server.closeAllConnections?.();
|
|
1142
1399
|
resolve();
|
|
@@ -1159,9 +1416,9 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1159
1416
|
|
|
1160
1417
|
test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
|
|
1161
1418
|
// Generate a self-signed certificate for the test using openssl.
|
|
1162
|
-
const {execSync} =
|
|
1163
|
-
const os =
|
|
1164
|
-
const fsSync =
|
|
1419
|
+
const {execSync} = await import('node:child_process');
|
|
1420
|
+
const os = await import('node:os');
|
|
1421
|
+
const fsSync = await import('node:fs');
|
|
1165
1422
|
const tmpDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'kilotest-https-'));
|
|
1166
1423
|
const keyPath = path.join(tmpDir, 'key.pem');
|
|
1167
1424
|
const certPath = path.join(tmpDir, 'cert.pem');
|
|
@@ -1184,9 +1441,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1184
1441
|
assert.ok(server);
|
|
1185
1442
|
assert.equal(typeof server.listen, 'function');
|
|
1186
1443
|
const address = server.address();
|
|
1187
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1444
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1188
1445
|
server.closeAllConnections?.();
|
|
1189
|
-
await new Promise(resolve => {
|
|
1446
|
+
await new Promise<void>(resolve => {
|
|
1190
1447
|
const timer = setTimeout(() => {
|
|
1191
1448
|
server.closeAllConnections?.();
|
|
1192
1449
|
resolve();
|
|
@@ -1228,37 +1485,25 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1228
1485
|
|
|
1229
1486
|
test('runIfMain calls startServer when mainModule matches currentModule', async () => {
|
|
1230
1487
|
let called = false;
|
|
1231
|
-
const
|
|
1232
|
-
indexModule.startServer = async () => {
|
|
1488
|
+
const starter = async () => {
|
|
1233
1489
|
called = true;
|
|
1234
1490
|
return null;
|
|
1235
1491
|
};
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
assert.equal(called, true);
|
|
1243
|
-
}
|
|
1244
|
-
finally {
|
|
1245
|
-
indexModule.startServer = originalStartServer;
|
|
1246
|
-
}
|
|
1492
|
+
// Pass the same object for both arguments so the guard is true.
|
|
1493
|
+
const fakeModule = {};
|
|
1494
|
+
runIfMain(fakeModule, fakeModule, starter);
|
|
1495
|
+
// Wait for the microtask queue to flush the promise.
|
|
1496
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1497
|
+
assert.equal(called, true);
|
|
1247
1498
|
});
|
|
1248
1499
|
|
|
1249
1500
|
test('runIfMain does not call startServer when mainModule differs from currentModule', async () => {
|
|
1250
1501
|
let called = false;
|
|
1251
|
-
const
|
|
1252
|
-
indexModule.startServer = async () => {
|
|
1502
|
+
const starter = async () => {
|
|
1253
1503
|
called = true;
|
|
1254
1504
|
return null;
|
|
1255
1505
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
assert.equal(called, false);
|
|
1260
|
-
}
|
|
1261
|
-
finally {
|
|
1262
|
-
indexModule.startServer = originalStartServer;
|
|
1263
|
-
}
|
|
1506
|
+
runIfMain({}, {}, starter);
|
|
1507
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1508
|
+
assert.equal(called, false);
|
|
1264
1509
|
});
|