@jrpool/kilotest 47.0.1 → 49.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/scheduled_tasks.lock +1 -0
- package/api/requestFeature.test.ts +4 -2
- package/api/requestFeature.ts +1 -1
- package/api/requestRetest.test.ts +66 -7
- package/api/requestRetest.ts +55 -39
- package/api/requestTest.test.ts +63 -7
- package/api/requestTest.ts +36 -26
- package/api/util.test.ts +6 -5
- package/api/util.ts +2 -24
- package/docs/BACKLOG.md +228 -16
- package/docs/SERVICE.md +84 -7
- package/index.test.ts +354 -25
- package/index.ts +174 -8
- package/linkValidation.test.ts +1 -0
- package/mcp.test.ts +30 -0
- package/mcp.ts +9 -0
- package/package.json +1 -1
- package/smokeTest.ts +16 -0
- package/test/dbFixture.ts +8 -0
- package/util.test.ts +426 -127
- package/util.ts +389 -196
- package/web/ai0BalanceForm/ai0BalanceForm.test.ts +16 -10
- package/web/ai0BalanceForm/index.html +1 -1
- package/web/ai0BalanceForm/index.ts +10 -6
- package/web/enqueue/index.ts +5 -2
- package/web/expungeReportsForm/expungeReportsForm.test.ts +19 -11
- package/web/expungeReportsForm/index.html +1 -1
- package/web/expungeReportsForm/index.ts +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +24 -9
- package/web/hideReportForm/index.html +1 -1
- package/web/hideReportForm/index.ts +9 -6
- package/web/listReports/index.ts +33 -12
- package/web/listReports/listReports.test.ts +1 -1
- package/web/listTopIssues/index.ts +1 -1
- package/web/manage/index.html +2 -1
- package/web/metrics/index.html +19 -0
- package/web/metrics/index.ts +126 -0
- package/web/metrics/metrics.test.ts +211 -0
- package/web/pruneReportsForm/index.html +1 -1
- package/web/pruneReportsForm/index.ts +1 -1
- package/web/pruneReportsForm/pruneReportsForm.test.ts +18 -10
- package/web/reannotate/index.ts +7 -3
- package/web/reannotate/reannotate.test.ts +1 -1
- package/web/renewWCAG/index.ts +4 -1
- package/web/renewWCAG/renewWCAG.test.ts +1 -1
- package/web/reportDeletion.ts +12 -7
- package/web/requestRetest/index.html +3 -3
- package/web/requestRetest/index.ts +47 -16
- package/web/requestRetest/requestRetest.test.ts +38 -24
- package/web/requestTest/index.html +4 -4
- package/web/requestTest/index.ts +37 -9
- package/web/requestTest/requestTest.test.ts +21 -6
- package/web/rewindReportsForm/index.html +1 -1
- package/web/rewindReportsForm/index.ts +1 -1
- package/web/rewindReportsForm/rewindReportsForm.test.ts +18 -10
- package/web/showHiddenReportsForm/index.html +24 -0
- package/web/showHiddenReportsForm/index.ts +45 -0
- package/web/showHiddenReportsForm/showHiddenReportsForm.test.ts +103 -0
- package/web/tutorialAI/index.ts +1 -1
- package/web/tutorialWeb/index.ts +1 -1
- package/web/unhideReportForm/index.html +1 -1
- package/web/unhideReportForm/index.ts +11 -8
- package/web/unhideReportForm/unhideReportForm.test.ts +19 -11
package/index.test.ts
CHANGED
|
@@ -32,6 +32,7 @@ const {requestHandler, routes, serveError, startServer, runIfMain, isPathAllowed
|
|
|
32
32
|
|
|
33
33
|
const port = 3997;
|
|
34
34
|
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
35
|
+
const metricsPath = path.join(fixtureDBDir, 'metrics.json');
|
|
35
36
|
|
|
36
37
|
// SETUP AND TEARDOWN
|
|
37
38
|
|
|
@@ -146,6 +147,31 @@ const jsonBody = (res: any) => {
|
|
|
146
147
|
return null;
|
|
147
148
|
}
|
|
148
149
|
};
|
|
150
|
+
// Returns the current count for a metrics category and name, or 0 if the file or the
|
|
151
|
+
// entry is absent (the file does not exist until the first metric is recorded).
|
|
152
|
+
const getMetricCount = async (category: string, name: string): Promise<number> => {
|
|
153
|
+
let metricsJSON: string;
|
|
154
|
+
try {
|
|
155
|
+
metricsJSON = await fs.readFile(metricsPath, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
const metrics = JSON.parse(metricsJSON);
|
|
161
|
+
return metrics[category][name] ?? 0;
|
|
162
|
+
};
|
|
163
|
+
// Returns the current managerActivity ok/error count for a page name, or 0 if absent.
|
|
164
|
+
const getManagerActivityCount = async (name: string, outcome: 'ok' | 'error'): Promise<number> => {
|
|
165
|
+
let metricsJSON: string;
|
|
166
|
+
try {
|
|
167
|
+
metricsJSON = await fs.readFile(metricsPath, 'utf8');
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
const metrics = JSON.parse(metricsJSON);
|
|
173
|
+
return metrics.managerActivity[name]?.[outcome] ?? 0;
|
|
174
|
+
};
|
|
149
175
|
|
|
150
176
|
// TESTS: routes table
|
|
151
177
|
|
|
@@ -267,11 +293,13 @@ test('GET /favicon.ico serves the favicon as an icon', async () => {
|
|
|
267
293
|
assert.ok(res.headers['content-type'].includes('image/x-icon'));
|
|
268
294
|
});
|
|
269
295
|
|
|
270
|
-
test('GET /api/listReports returns JSON with report data', async () => {
|
|
296
|
+
test('GET /api/listReports returns JSON with report data and records an API-operation metric', async () => {
|
|
297
|
+
const countBefore = await getMetricCount('apiOperations', 'listReports');
|
|
271
298
|
const res = await request('GET', '/api/listReports');
|
|
272
299
|
assert.equal(res.statusCode, 200);
|
|
273
300
|
const body = jsonBody(res);
|
|
274
301
|
assert.equal(body['tool name'], 'listReports');
|
|
302
|
+
assert.equal(await getMetricCount('apiOperations', 'listReports'), countBefore + 1);
|
|
275
303
|
});
|
|
276
304
|
|
|
277
305
|
test('GET /api/listIssues/260101T0000/mix returns JSON with issue data', async () => {
|
|
@@ -466,13 +494,15 @@ test('POST /api/requestRetest/260202T0000/new with valid JSON returns a JSON res
|
|
|
466
494
|
assert.ok(body['tool name'] || body['response content']);
|
|
467
495
|
});
|
|
468
496
|
|
|
469
|
-
test('POST /api/requestFeature with valid JSON returns a JSON response', async () => {
|
|
497
|
+
test('POST /api/requestFeature with valid JSON returns a JSON response and records an API-operation metric', async () => {
|
|
498
|
+
const countBefore = await getMetricCount('apiOperations', 'requestFeature');
|
|
470
499
|
const res = await request('POST', '/api/requestFeature', {
|
|
471
500
|
feature: 'Add a dark mode toggle'
|
|
472
501
|
});
|
|
473
502
|
assert.equal(res.statusCode, 200);
|
|
474
503
|
const body = jsonBody(res);
|
|
475
504
|
assert.ok(body['tool name'] || body['response content']);
|
|
505
|
+
assert.equal(await getMetricCount('apiOperations', 'requestFeature'), countBefore + 1);
|
|
476
506
|
});
|
|
477
507
|
|
|
478
508
|
test('POST /api/invalidService returns an error', async () => {
|
|
@@ -616,35 +646,41 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
616
646
|
|
|
617
647
|
// TESTS: requestAction.html
|
|
618
648
|
|
|
619
|
-
test('POST /requestAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
|
|
649
|
+
test('POST /requestAction.html with invalid auth code returns an error and records a managerActivity failure', {timeout: 500}, async () => {
|
|
650
|
+
const countBefore = await getManagerActivityCount('requestAction.html', 'error');
|
|
620
651
|
const res = await formRequest('POST', '/requestAction.html', {
|
|
621
652
|
target: 'https://example.com\tTest Page',
|
|
622
653
|
authCode: 'wrong-code'
|
|
623
654
|
});
|
|
624
655
|
assert.equal(res.statusCode, 400);
|
|
625
|
-
assert.ok(res.body.includes('Invalid
|
|
656
|
+
assert.ok(res.body.includes('Invalid request'));
|
|
657
|
+
assert.equal(await getManagerActivityCount('requestAction.html', 'error'), countBefore + 1);
|
|
626
658
|
});
|
|
627
659
|
|
|
628
|
-
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML', async () => {
|
|
660
|
+
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML and records a managerActivity success', async () => {
|
|
629
661
|
await formRequest('POST', '/requestTest.html', {
|
|
630
662
|
description: `Reject Test Page ${uniqueStamp}`,
|
|
631
663
|
url: `https://example.com/reject-${uniqueStamp}`,
|
|
632
664
|
why: 'Because accessibility matters'
|
|
633
665
|
});
|
|
666
|
+
const countBefore = await getManagerActivityCount('requestAction.html', 'ok');
|
|
634
667
|
const res = await formRequest('POST', '/requestAction.html', {
|
|
635
668
|
target: `https://example.com/reject-${uniqueStamp}`,
|
|
636
669
|
authCode: 'test-auth-code'
|
|
637
670
|
});
|
|
638
671
|
assert.equal(res.statusCode, 200);
|
|
639
672
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
673
|
+
assert.equal(await getManagerActivityCount('requestAction.html', 'ok'), countBefore + 1);
|
|
640
674
|
});
|
|
641
675
|
|
|
642
676
|
// TESTS: tutorialWeb (web user tutorial)
|
|
643
677
|
|
|
644
|
-
test('GET /tutorialWeb.html returns HTML', async () => {
|
|
678
|
+
test('GET /tutorialWeb.html returns HTML and records a page-view metric', async () => {
|
|
679
|
+
const countBefore = await getMetricCount('pageViews', 'tutorialWeb');
|
|
645
680
|
const res = await request('GET', '/tutorialWeb.html');
|
|
646
681
|
assert.equal(res.statusCode, 200);
|
|
647
682
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
683
|
+
assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), countBefore + 1);
|
|
648
684
|
});
|
|
649
685
|
|
|
650
686
|
test('POST /tutorialWebComment.html with content returns JSON', async () => {
|
|
@@ -678,14 +714,16 @@ test('POST /requestAction.html with valid auth code and approval returns HTML',
|
|
|
678
714
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
679
715
|
});
|
|
680
716
|
|
|
681
|
-
test('POST /reannotate.html with invalid auth code returns an error page', async () => {
|
|
717
|
+
test('POST /reannotate.html with invalid auth code returns an error page and records a managerActivity failure', async () => {
|
|
718
|
+
const countBefore = await getManagerActivityCount('reannotate.html', 'error');
|
|
682
719
|
const res = await formRequest('POST', '/reannotate.html', {
|
|
683
720
|
authCode: 'wrong-code'
|
|
684
721
|
});
|
|
685
722
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
723
|
+
assert.equal(await getManagerActivityCount('reannotate.html', 'error'), countBefore + 1);
|
|
686
724
|
});
|
|
687
725
|
|
|
688
|
-
test('POST /reannotate.html with valid auth code serves the answer page', async () => {
|
|
726
|
+
test('POST /reannotate.html with valid auth code serves the answer page and records a managerActivity success', async () => {
|
|
689
727
|
// Back up all fixture reports, because reannotation modifies them in place.
|
|
690
728
|
const reportsDir = path.join(fixtureDBDir, 'reports');
|
|
691
729
|
const reportFiles = await fs.readdir(reportsDir);
|
|
@@ -694,12 +732,14 @@ test('POST /reannotate.html with valid auth code serves the answer page', async
|
|
|
694
732
|
backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
|
|
695
733
|
}
|
|
696
734
|
try {
|
|
735
|
+
const countBefore = await getManagerActivityCount('reannotate.html', 'ok');
|
|
697
736
|
const res = await formRequest('POST', '/reannotate.html', {
|
|
698
737
|
authCode: 'test-auth-code'
|
|
699
738
|
});
|
|
700
739
|
assert.equal(res.statusCode, 200);
|
|
701
740
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
702
741
|
assert.ok(res.body.includes('Reannotation order'));
|
|
742
|
+
assert.equal(await getManagerActivityCount('reannotate.html', 'ok'), countBefore + 1);
|
|
703
743
|
}
|
|
704
744
|
finally {
|
|
705
745
|
// Restore all fixture reports.
|
|
@@ -709,14 +749,16 @@ test('POST /reannotate.html with valid auth code serves the answer page', async
|
|
|
709
749
|
}
|
|
710
750
|
});
|
|
711
751
|
|
|
712
|
-
test('POST /renewWCAG.html with invalid auth code returns an error page', async () => {
|
|
752
|
+
test('POST /renewWCAG.html with invalid auth code returns an error page and records a managerActivity failure', async () => {
|
|
753
|
+
const countBefore = await getManagerActivityCount('renewWCAG.html', 'error');
|
|
713
754
|
const res = await formRequest('POST', '/renewWCAG.html', {
|
|
714
755
|
authCode: 'wrong-code'
|
|
715
756
|
});
|
|
716
757
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
758
|
+
assert.equal(await getManagerActivityCount('renewWCAG.html', 'error'), countBefore + 1);
|
|
717
759
|
});
|
|
718
760
|
|
|
719
|
-
test('POST /renewWCAG.html with valid auth code serves the answer page', async () => {
|
|
761
|
+
test('POST /renewWCAG.html with valid auth code serves the answer page and records a managerActivity success', async () => {
|
|
720
762
|
// Mock fetch to avoid a network dependency.
|
|
721
763
|
const originalFetch = global.fetch;
|
|
722
764
|
const wcagMapPath = path.join(import.meta.dirname, 'wcagMap.json');
|
|
@@ -727,12 +769,14 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
727
769
|
text: async () => '<a href="understanding/contrast-minimum"><span class="secno">1.4.3 </span>'
|
|
728
770
|
});
|
|
729
771
|
try {
|
|
772
|
+
const countBefore = await getManagerActivityCount('renewWCAG.html', 'ok');
|
|
730
773
|
const res = await formRequest('POST', '/renewWCAG.html', {
|
|
731
774
|
authCode: 'test-auth-code'
|
|
732
775
|
});
|
|
733
776
|
assert.equal(res.statusCode, 200);
|
|
734
777
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
735
778
|
assert.ok(res.body.includes('WCAG map renewed'));
|
|
779
|
+
assert.equal(await getManagerActivityCount('renewWCAG.html', 'ok'), countBefore + 1);
|
|
736
780
|
}
|
|
737
781
|
finally {
|
|
738
782
|
global.fetch = originalFetch;
|
|
@@ -902,6 +946,101 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
902
946
|
await fs.unlink(failedJobPath).catch(() => {});
|
|
903
947
|
});
|
|
904
948
|
|
|
949
|
+
test('POST /worker/job skips a claimed job file that disappears between the directory listing and the read', {timeout: 500}, async (t) => {
|
|
950
|
+
// Create a claimed job assigned to a different worker, so it does not match this
|
|
951
|
+
// worker and would otherwise just be skipped normally, then make the read of that
|
|
952
|
+
// specific file (not the directory listing) throw ENOENT, simulating the file
|
|
953
|
+
// vanishing between getJobNames listing the claimed directory and this loop's read
|
|
954
|
+
// of the file it found there (e.g. because a concurrent report submission completed
|
|
955
|
+
// the job just after the listing).
|
|
956
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
957
|
+
const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
|
|
958
|
+
for (const dir of [claimedDir, queueDir]) {
|
|
959
|
+
await fs.mkdir(dir, {recursive: true});
|
|
960
|
+
const files = await fs.readdir(dir).catch(() => []);
|
|
961
|
+
for (const file of files) {
|
|
962
|
+
await fs.unlink(path.join(dir, file)).catch(() => {});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const jobFile = '260101T0000-mix.json';
|
|
966
|
+
const claimedJobPath = path.join(claimedDir, jobFile);
|
|
967
|
+
await fs.writeFile(claimedJobPath, JSON.stringify({
|
|
968
|
+
id: '260101T0000-mix',
|
|
969
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
970
|
+
sources: {worker: 'Worker Two'}
|
|
971
|
+
}));
|
|
972
|
+
const originalReadFile = fs.readFile;
|
|
973
|
+
t.mock.method(fs, 'readFile', async (filePath: any, ...rest: any[]) => {
|
|
974
|
+
if (String(filePath) === claimedJobPath) {
|
|
975
|
+
await fs.unlink(claimedJobPath);
|
|
976
|
+
const error: any = new Error('ENOENT: no such file or directory');
|
|
977
|
+
error.code = 'ENOENT';
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
return (originalReadFile as any)(filePath, ...rest);
|
|
981
|
+
});
|
|
982
|
+
try {
|
|
983
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
984
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
985
|
+
authorization: `Basic ${auth}`
|
|
986
|
+
});
|
|
987
|
+
// With no queued jobs and the claimed job having vanished mid-read, the worker
|
|
988
|
+
// should just get told there is no job, rather than a 500 error from an
|
|
989
|
+
// unhandled ENOENT.
|
|
990
|
+
assert.equal(res.statusCode, 200);
|
|
991
|
+
const body = jsonBody(res);
|
|
992
|
+
assert.ok(!body.error);
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
t.mock.reset();
|
|
996
|
+
await fs.unlink(claimedJobPath).catch(() => {});
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test('POST /worker/job propagates a non-ENOENT failure to read a claimed job file', {timeout: 500}, async (t) => {
|
|
1001
|
+
// The catch in processJobRequest's claimed-job loop rethrows any failure other than
|
|
1002
|
+
// the file having vanished (ENOENT). This confirms that a different failure, such as
|
|
1003
|
+
// a permissions error, is not swallowed alongside the expected race condition.
|
|
1004
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
1005
|
+
const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
|
|
1006
|
+
for (const dir of [claimedDir, queueDir]) {
|
|
1007
|
+
await fs.mkdir(dir, {recursive: true});
|
|
1008
|
+
const files = await fs.readdir(dir).catch(() => []);
|
|
1009
|
+
for (const file of files) {
|
|
1010
|
+
await fs.unlink(path.join(dir, file)).catch(() => {});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const jobFile = '260101T0000-mix.json';
|
|
1014
|
+
const claimedJobPath = path.join(claimedDir, jobFile);
|
|
1015
|
+
await fs.writeFile(claimedJobPath, JSON.stringify({
|
|
1016
|
+
id: '260101T0000-mix',
|
|
1017
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
1018
|
+
sources: {worker: 'Worker Two'}
|
|
1019
|
+
}));
|
|
1020
|
+
const originalReadFile = fs.readFile;
|
|
1021
|
+
t.mock.method(fs, 'readFile', async (filePath: any, ...rest: any[]) => {
|
|
1022
|
+
if (String(filePath) === claimedJobPath) {
|
|
1023
|
+
const error: any = new Error('EACCES: permission denied');
|
|
1024
|
+
error.code = 'EACCES';
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
return (originalReadFile as any)(filePath, ...rest);
|
|
1028
|
+
});
|
|
1029
|
+
try {
|
|
1030
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
1031
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
1032
|
+
authorization: `Basic ${auth}`
|
|
1033
|
+
});
|
|
1034
|
+
// The request should fail loudly rather than silently proceeding as if there
|
|
1035
|
+
// were no claimed job.
|
|
1036
|
+
assert.equal(res.statusCode, 500);
|
|
1037
|
+
}
|
|
1038
|
+
finally {
|
|
1039
|
+
t.mock.reset();
|
|
1040
|
+
await fs.unlink(claimedJobPath).catch(() => {});
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
905
1044
|
test('POST /worker/job with a queued job assigns it to the worker', {timeout: 500}, async () => {
|
|
906
1045
|
// Clean up claimed and queue directories.
|
|
907
1046
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
@@ -1036,16 +1175,20 @@ test('GET /style.css with a read error returns an error page', async () => {
|
|
|
1036
1175
|
}
|
|
1037
1176
|
});
|
|
1038
1177
|
|
|
1039
|
-
|
|
1178
|
+
// requestTest.html and requestRetest.html are POST-only routes (see the
|
|
1179
|
+
// `routes.POST` list above). They also match the generic '*.html*' GET pattern,
|
|
1180
|
+
// but the generic `pageName.endsWith('.html')` branch of the GET dispatcher
|
|
1181
|
+
// excludes any path that is POST-only, since such a path's answer handler
|
|
1182
|
+
// expects POST's argument list and performs no GET-appropriate rendering.
|
|
1183
|
+
|
|
1184
|
+
test('GET /requestTest.html without arguments is rejected as an invalid GET request', async () => {
|
|
1040
1185
|
const res = await request('GET', '/requestTest.html');
|
|
1041
1186
|
assert.equal(res.statusCode, 400);
|
|
1042
|
-
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1043
1187
|
});
|
|
1044
1188
|
|
|
1045
|
-
test('GET /requestRetest.html/260202T0000/new
|
|
1189
|
+
test('GET /requestRetest.html/260202T0000/new is rejected as an invalid GET request', async () => {
|
|
1046
1190
|
const res = await request('GET', '/requestRetest.html/260202T0000/new');
|
|
1047
1191
|
assert.equal(res.statusCode, 400);
|
|
1048
|
-
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1049
1192
|
});
|
|
1050
1193
|
|
|
1051
1194
|
// TESTS: GET HTML page routing
|
|
@@ -1065,11 +1208,12 @@ const htmlPagePaths = [
|
|
|
1065
1208
|
'/reannotateForm.html',
|
|
1066
1209
|
'/renewWCAGForm.html',
|
|
1067
1210
|
'/hideReportForm.html',
|
|
1068
|
-
'/
|
|
1211
|
+
'/showHiddenReportsForm.html',
|
|
1069
1212
|
'/expungeReportsForm.html',
|
|
1070
1213
|
'/pruneReportsForm.html',
|
|
1071
1214
|
'/rewindReportsForm.html',
|
|
1072
|
-
'/ai0BalanceForm.html'
|
|
1215
|
+
'/ai0BalanceForm.html',
|
|
1216
|
+
'/metrics.html'
|
|
1073
1217
|
];
|
|
1074
1218
|
|
|
1075
1219
|
for (const pagePath of htmlPagePaths) {
|
|
@@ -1080,6 +1224,187 @@ for (const pagePath of htmlPagePaths) {
|
|
|
1080
1224
|
});
|
|
1081
1225
|
}
|
|
1082
1226
|
|
|
1227
|
+
test('POST /metrics.html with a valid authCode serves the usage-metrics table and records a managerActivity success', async () => {
|
|
1228
|
+
const countBefore = await getManagerActivityCount('metrics', 'ok');
|
|
1229
|
+
const res = await formRequest('POST', '/metrics.html', {authCode: 'test-auth-code'});
|
|
1230
|
+
assert.equal(res.statusCode, 200);
|
|
1231
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1232
|
+
assert.equal(await getManagerActivityCount('metrics', 'ok'), countBefore + 1);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
test('POST /metrics.html with a valid authCode sets a metrics-exclusion cookie', async () => {
|
|
1236
|
+
const res = await formRequest('POST', '/metrics.html', {authCode: 'test-auth-code'});
|
|
1237
|
+
const setCookie = res.headers['set-cookie']?.[0] ?? res.headers['set-cookie'];
|
|
1238
|
+
assert.ok(setCookie);
|
|
1239
|
+
assert.ok(setCookie.startsWith('kilotestExclude='));
|
|
1240
|
+
assert.ok(setCookie.includes('Max-Age=2592000'));
|
|
1241
|
+
assert.ok(setCookie.includes('HttpOnly'));
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
test('GET /metrics.html with a valid authCode in the query string does not set a cookie or show the counts', async () => {
|
|
1245
|
+
const res = await request('GET', '/metrics.html?authCode=test-auth-code');
|
|
1246
|
+
assert.equal(res.statusCode, 200);
|
|
1247
|
+
assert.equal(res.headers['set-cookie'], undefined);
|
|
1248
|
+
assert.ok(res.body.includes('Authorization code'));
|
|
1249
|
+
assert.ok(!res.body.includes('Web page views'));
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test('a request carrying the metrics-exclusion cookie does not record a pageViews or apiOperations metric', async () => {
|
|
1253
|
+
const {getExclusionCookieValue} = await import('./util.ts');
|
|
1254
|
+
const cookieHeader = `kilotestExclude=${getExclusionCookieValue()}`;
|
|
1255
|
+
const pageViewCountBefore = await getMetricCount('pageViews', 'tutorialWeb');
|
|
1256
|
+
const apiOperationCountBefore = await getMetricCount('apiOperations', 'listReports');
|
|
1257
|
+
await request('GET', '/tutorialWeb.html', null, {cookie: cookieHeader});
|
|
1258
|
+
await request('GET', '/api/listReports', null, {cookie: cookieHeader});
|
|
1259
|
+
assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), pageViewCountBefore);
|
|
1260
|
+
assert.equal(await getMetricCount('apiOperations', 'listReports'), apiOperationCountBefore);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
test('a request carrying an invalid exclusion cookie still records metrics normally', async () => {
|
|
1264
|
+
const pageViewCountBefore = await getMetricCount('pageViews', 'tutorialWeb');
|
|
1265
|
+
await request('GET', '/tutorialWeb.html', null, {cookie: 'kilotestExclude=wrong-value'});
|
|
1266
|
+
assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), pageViewCountBefore + 1);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
test('a request with an unrelated cookie still records metrics normally', async () => {
|
|
1270
|
+
const pageViewCountBefore = await getMetricCount('pageViews', 'tutorialWeb');
|
|
1271
|
+
await request('GET', '/tutorialWeb.html', null, {cookie: 'someOtherCookie=1'});
|
|
1272
|
+
assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), pageViewCountBefore + 1);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
test('GET / and GET /index.html record a pageViews metric under "index"', async () => {
|
|
1276
|
+
const countBefore = await getMetricCount('pageViews', 'index');
|
|
1277
|
+
await request('GET', '/');
|
|
1278
|
+
await request('GET', '/index.html');
|
|
1279
|
+
assert.equal(await getMetricCount('pageViews', 'index'), countBefore + 2);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test('POST /metrics.html with an invalid authCode is rejected and records a managerActivity failure', async () => {
|
|
1283
|
+
const countBefore = await getManagerActivityCount('metrics', 'error');
|
|
1284
|
+
const res = await formRequest('POST', '/metrics.html', {authCode: 'wrong'});
|
|
1285
|
+
assert.equal(res.statusCode, 400);
|
|
1286
|
+
assert.equal(await getManagerActivityCount('metrics', 'error'), countBefore + 1);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
test('GET /hideReportForm.html records managerActivity, not a pageViews entry', async () => {
|
|
1290
|
+
const managerCountBefore = await getManagerActivityCount('hideReportForm', 'ok');
|
|
1291
|
+
const pageViewCountBefore = await getMetricCount('pageViews', 'hideReportForm');
|
|
1292
|
+
const res = await request('GET', '/hideReportForm.html');
|
|
1293
|
+
assert.equal(res.statusCode, 200);
|
|
1294
|
+
assert.equal(await getManagerActivityCount('hideReportForm', 'ok'), managerCountBefore + 1);
|
|
1295
|
+
assert.equal(await getMetricCount('pageViews', 'hideReportForm'), pageViewCountBefore);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// TESTS: hidden report list is never served on a direct GET, only via showHiddenReports
|
|
1299
|
+
|
|
1300
|
+
test('GET /unhideReportForm.html is rejected, since the hidden-report list must not be disclosed without an authorization code', async () => {
|
|
1301
|
+
const res = await request('GET', '/unhideReportForm.html');
|
|
1302
|
+
assert.equal(res.statusCode, 400);
|
|
1303
|
+
assert.ok(!res.body.includes('260101T0007-hid'));
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test('GET /showHiddenReportsForm.html displays a bare authCode form, disclosing no report names', async () => {
|
|
1307
|
+
const res = await request('GET', '/showHiddenReportsForm.html');
|
|
1308
|
+
assert.equal(res.statusCode, 200);
|
|
1309
|
+
assert.ok(res.body.includes('Authorization code'));
|
|
1310
|
+
assert.ok(!res.body.includes('260101T0007-hid'));
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
test('POST /showHiddenReportsForm.html with a valid authCode serves the hidden-report list and records a managerActivity success', async () => {
|
|
1314
|
+
const countBefore = await getManagerActivityCount('showHiddenReportsForm', 'ok');
|
|
1315
|
+
const res = await formRequest('POST', '/showHiddenReportsForm.html', {authCode: 'test-auth-code'});
|
|
1316
|
+
assert.equal(res.statusCode, 200);
|
|
1317
|
+
assert.ok(res.body.includes('type="radio"'));
|
|
1318
|
+
assert.equal(await getManagerActivityCount('showHiddenReportsForm', 'ok'), countBefore + 1);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test('POST /showHiddenReportsForm.html with an invalid authCode is rejected and records a managerActivity failure', async () => {
|
|
1322
|
+
const countBefore = await getManagerActivityCount('showHiddenReportsForm', 'error');
|
|
1323
|
+
const res = await formRequest('POST', '/showHiddenReportsForm.html', {authCode: 'wrong'});
|
|
1324
|
+
assert.equal(res.statusCode, 400);
|
|
1325
|
+
assert.equal(await getManagerActivityCount('showHiddenReportsForm', 'error'), countBefore + 1);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// TESTS: self-submitting manager pages now POST their own submissions (formerly GET)
|
|
1329
|
+
|
|
1330
|
+
test('POST /ai0BalanceForm.html with a valid authCode records the balance and records a managerActivity success', async () => {
|
|
1331
|
+
const countBefore = await getManagerActivityCount('ai0BalanceForm', 'ok');
|
|
1332
|
+
const res = await formRequest('POST', '/ai0BalanceForm.html', {
|
|
1333
|
+
newBalance: '42.50',
|
|
1334
|
+
authCode: 'test-auth-code'
|
|
1335
|
+
});
|
|
1336
|
+
assert.equal(res.statusCode, 200);
|
|
1337
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1338
|
+
assert.ok(res.body.includes('$42.5'));
|
|
1339
|
+
assert.equal(await getManagerActivityCount('ai0BalanceForm', 'ok'), countBefore + 1);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
test('POST /ai0BalanceForm.html with an invalid authCode is rejected and records a managerActivity failure', async () => {
|
|
1343
|
+
const countBefore = await getManagerActivityCount('ai0BalanceForm', 'error');
|
|
1344
|
+
const res = await formRequest('POST', '/ai0BalanceForm.html', {
|
|
1345
|
+
newBalance: '10',
|
|
1346
|
+
authCode: 'wrong-code'
|
|
1347
|
+
});
|
|
1348
|
+
assert.equal(res.statusCode, 400);
|
|
1349
|
+
assert.equal(await getManagerActivityCount('ai0BalanceForm', 'error'), countBefore + 1);
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
test('POST /hideReportForm.html with an invalid authCode is rejected and records a managerActivity failure', async () => {
|
|
1353
|
+
const countBefore = await getManagerActivityCount('hideReportForm', 'error');
|
|
1354
|
+
const res = await formRequest('POST', '/hideReportForm.html', {
|
|
1355
|
+
report: '260101T0009-brd',
|
|
1356
|
+
authCode: 'wrong-code'
|
|
1357
|
+
});
|
|
1358
|
+
assert.equal(res.statusCode, 400);
|
|
1359
|
+
assert.equal(await getManagerActivityCount('hideReportForm', 'error'), countBefore + 1);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test('POST /unhideReportForm.html with an invalid authCode is rejected and records a managerActivity failure', async () => {
|
|
1363
|
+
const countBefore = await getManagerActivityCount('unhideReportForm', 'error');
|
|
1364
|
+
const res = await formRequest('POST', '/unhideReportForm.html', {
|
|
1365
|
+
report: '260101T0009-brd',
|
|
1366
|
+
authCode: 'wrong-code'
|
|
1367
|
+
});
|
|
1368
|
+
assert.equal(res.statusCode, 400);
|
|
1369
|
+
assert.equal(await getManagerActivityCount('unhideReportForm', 'error'), countBefore + 1);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
test('POST /expungeReportsForm.html with no selections shows the form and records a managerActivity success', async () => {
|
|
1373
|
+
const countBefore = await getManagerActivityCount('expungeReportsForm', 'ok');
|
|
1374
|
+
const res = await formRequest('POST', '/expungeReportsForm.html', {});
|
|
1375
|
+
assert.equal(res.statusCode, 200);
|
|
1376
|
+
assert.equal(await getManagerActivityCount('expungeReportsForm', 'ok'), countBefore + 1);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
test('POST /pruneReportsForm.html with no selections shows the form and records a managerActivity success', async () => {
|
|
1380
|
+
const countBefore = await getManagerActivityCount('pruneReportsForm', 'ok');
|
|
1381
|
+
const res = await formRequest('POST', '/pruneReportsForm.html', {});
|
|
1382
|
+
assert.equal(res.statusCode, 200);
|
|
1383
|
+
assert.equal(await getManagerActivityCount('pruneReportsForm', 'ok'), countBefore + 1);
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
test('POST /rewindReportsForm.html with no selections shows the form and records a managerActivity success', async () => {
|
|
1387
|
+
const countBefore = await getManagerActivityCount('rewindReportsForm', 'ok');
|
|
1388
|
+
const res = await formRequest('POST', '/rewindReportsForm.html', {});
|
|
1389
|
+
assert.equal(res.statusCode, 200);
|
|
1390
|
+
assert.equal(await getManagerActivityCount('rewindReportsForm', 'ok'), countBefore + 1);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
test('GET /pruneReportsForm.html records a managerActivity failure when a report file is corrupt', {timeout: 500}, async () => {
|
|
1394
|
+
const reportPath = path.join(fixtureDBDir, 'reports', '260101T0001-ct.json');
|
|
1395
|
+
const backup = await fs.readFile(reportPath, 'utf8');
|
|
1396
|
+
const countBefore = await getManagerActivityCount('pruneReportsForm', 'error');
|
|
1397
|
+
try {
|
|
1398
|
+
await fs.writeFile(reportPath, 'not valid json');
|
|
1399
|
+
const res = await request('GET', '/pruneReportsForm.html');
|
|
1400
|
+
assert.equal(res.statusCode, 400);
|
|
1401
|
+
assert.equal(await getManagerActivityCount('pruneReportsForm', 'error'), countBefore + 1);
|
|
1402
|
+
}
|
|
1403
|
+
finally {
|
|
1404
|
+
await fs.writeFile(reportPath, backup);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1083
1408
|
test('GET /enqueueForm.html shows requests when testRequests.json has entries', async () => {
|
|
1084
1409
|
await fs.writeFile(testRequestsPath, JSON.stringify({
|
|
1085
1410
|
'https://example.com/enqueue-test': [
|
|
@@ -1186,34 +1511,38 @@ test('POST /mcp with a valid initialize request returns server info via SSE', as
|
|
|
1186
1511
|
|
|
1187
1512
|
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
1188
1513
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1189
|
-
// First request to create the request.
|
|
1514
|
+
// First request to create the request. processTestRequest treats a request as a
|
|
1515
|
+
// duplicate only when description, URL, AND reason all match an existing pending
|
|
1516
|
+
// request, so the reason must be identical between the two requests here.
|
|
1190
1517
|
await formRequest('POST', '/requestTest.html', {
|
|
1191
1518
|
description: `Dup Test Page ${uniqueStamp}`,
|
|
1192
1519
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
1193
1520
|
why: 'Because accessibility matters'
|
|
1194
1521
|
});
|
|
1195
|
-
// Second request
|
|
1522
|
+
// Second, identical request should get a duplicate error.
|
|
1196
1523
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
1197
1524
|
description: `Dup Test Page ${uniqueStamp}`,
|
|
1198
1525
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
1199
|
-
why: 'Because accessibility matters
|
|
1526
|
+
why: 'Because accessibility matters'
|
|
1200
1527
|
});
|
|
1201
1528
|
assert.equal(res.statusCode, 400);
|
|
1202
|
-
assert.ok(res.body.includes('
|
|
1529
|
+
assert.ok(res.body.includes('Test request duplicates an already submitted request'));
|
|
1203
1530
|
});
|
|
1204
1531
|
|
|
1205
1532
|
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
1206
1533
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1207
|
-
// First retest to create the recommendation.
|
|
1534
|
+
// First retest to create the recommendation. As above, the reason must match
|
|
1535
|
+
// exactly between the two requests for processTestRequest to treat the second as
|
|
1536
|
+
// a duplicate rather than a second, independent (and thus also 'ok') request.
|
|
1208
1537
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
1209
1538
|
why: 'Because the report is obsolete and needs refreshing'
|
|
1210
1539
|
});
|
|
1211
|
-
// Second retest
|
|
1540
|
+
// Second, identical retest of the same report should get a duplicate error.
|
|
1212
1541
|
const res = await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
1213
|
-
why: 'Because the report is obsolete and needs refreshing
|
|
1542
|
+
why: 'Because the report is obsolete and needs refreshing'
|
|
1214
1543
|
});
|
|
1215
1544
|
assert.equal(res.statusCode, 400);
|
|
1216
|
-
assert.ok(res.body.includes('
|
|
1545
|
+
assert.ok(res.body.includes('Test request duplicates an already submitted request'));
|
|
1217
1546
|
});
|
|
1218
1547
|
|
|
1219
1548
|
test('POST /requestAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
|
|
@@ -1250,7 +1579,7 @@ test('POST /requestAction.html with valid auth code and approval of an invalid U
|
|
|
1250
1579
|
what: 'yes'
|
|
1251
1580
|
});
|
|
1252
1581
|
assert.equal(res.statusCode, 400);
|
|
1253
|
-
assert.ok(res.body.includes('Invalid
|
|
1582
|
+
assert.ok(res.body.includes('Invalid request'));
|
|
1254
1583
|
});
|
|
1255
1584
|
|
|
1256
1585
|
// UNIT TESTS FOR serveError
|