@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/web/requestTest/index.ts
CHANGED
|
@@ -5,20 +5,48 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {populateTemplate, processTestRequest} from '../../util.ts';
|
|
9
9
|
|
|
10
10
|
// FUNCTIONS
|
|
11
11
|
|
|
12
|
-
export const answer = async (description: string, url: string,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export const answer = async (description: string, url: string, reason: string) => {
|
|
13
|
+
// Process the request.
|
|
14
|
+
const {result} = await processTestRequest(reason, {description, url});
|
|
15
|
+
// If the request was recorded:
|
|
16
|
+
if (result === 'ok') {
|
|
17
|
+
const query = {
|
|
18
|
+
description,
|
|
19
|
+
url,
|
|
20
|
+
reason
|
|
21
|
+
};
|
|
22
|
+
// Return the populated page.
|
|
23
|
+
return {
|
|
24
|
+
status: 'ok',
|
|
25
|
+
answerPage: await populateTemplate(import.meta.dirname, query)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Otherwise, if the request is a duplicate:
|
|
29
|
+
else if (result === 'duplicate') {
|
|
30
|
+
// Report this.
|
|
17
31
|
return {
|
|
18
32
|
status: 'error',
|
|
19
|
-
message:
|
|
33
|
+
message: 'Test request duplicates an already submitted request'
|
|
20
34
|
};
|
|
21
35
|
}
|
|
22
|
-
// Otherwise,
|
|
23
|
-
|
|
36
|
+
// Otherwise, if a report is available about a page with the same description and URL:
|
|
37
|
+
else if (result === 'retest') {
|
|
38
|
+
// Report this.
|
|
39
|
+
return {
|
|
40
|
+
status: 'error',
|
|
41
|
+
message: 'A report about the page is already available'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Otherwise, i.e. if a request to test a page with the same description or URL is approved:
|
|
45
|
+
else {
|
|
46
|
+
// Report this.
|
|
47
|
+
return {
|
|
48
|
+
status: 'error',
|
|
49
|
+
message: 'A request to test a page with the same description or URL is already approved'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
24
52
|
};
|
|
@@ -72,8 +72,9 @@ test('answer returns ok with a populated answer page for a valid test request',
|
|
|
72
72
|
// Verify the placeholders were replaced.
|
|
73
73
|
assert.ok(result.answerPage.includes('Test Page'));
|
|
74
74
|
assert.ok(result.answerPage.includes('Because accessibility'));
|
|
75
|
-
assert.ok(!result.answerPage.includes('
|
|
76
|
-
assert.ok(!result.answerPage.includes('
|
|
75
|
+
assert.ok(!result.answerPage.includes('__description__'));
|
|
76
|
+
assert.ok(!result.answerPage.includes('__url__'));
|
|
77
|
+
assert.ok(!result.answerPage.includes('__reason__'));
|
|
77
78
|
// Verify the HTML is well-formed.
|
|
78
79
|
const doc = parse(result.answerPage);
|
|
79
80
|
const h1 = doc.querySelector('h1');
|
|
@@ -95,7 +96,7 @@ test('answer returns an error when the URL is already queued', {timeout: 500}, a
|
|
|
95
96
|
'Queued Page', 'https://example.com/queued', 'Because accessibility'
|
|
96
97
|
);
|
|
97
98
|
assert.equal(result.status, 'error');
|
|
98
|
-
assert.
|
|
99
|
+
assert.equal(result.message, 'A request to test a page with the same description or URL is already approved');
|
|
99
100
|
}
|
|
100
101
|
finally {
|
|
101
102
|
try {
|
|
@@ -107,9 +108,23 @@ test('answer returns an error when the URL is already queued', {timeout: 500}, a
|
|
|
107
108
|
}
|
|
108
109
|
});
|
|
109
110
|
|
|
110
|
-
test('answer returns an error for
|
|
111
|
+
test('answer returns an error for a duplicate request', {timeout: 500}, async () => {
|
|
111
112
|
const {answer} = await import('./index.ts');
|
|
112
|
-
|
|
113
|
+
// Submit the same request twice; the second is a duplicate.
|
|
114
|
+
await answer('Duplicate Page', 'https://example.com/duplicate', 'Because accessibility');
|
|
115
|
+
const result: any = await answer(
|
|
116
|
+
'Duplicate Page', 'https://example.com/duplicate', 'Because accessibility'
|
|
117
|
+
);
|
|
118
|
+
assert.equal(result.status, 'error');
|
|
119
|
+
assert.equal(result.message, 'Test request duplicates an already submitted request');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('answer returns an error when a report already exists for the page', {timeout: 500}, async () => {
|
|
123
|
+
const {answer} = await import('./index.ts');
|
|
124
|
+
// The fixture database already has a report for this description and URL.
|
|
125
|
+
const result: any = await answer(
|
|
126
|
+
'Mixed Outcomes Page', 'https://example.com/mixed', 'Because accessibility'
|
|
127
|
+
);
|
|
113
128
|
assert.equal(result.status, 'error');
|
|
114
|
-
assert.equal(result.message, '
|
|
129
|
+
assert.equal(result.message, 'A report about the page is already available');
|
|
115
130
|
});
|
|
@@ -10,7 +10,7 @@ import {reportDeletionForm} from '../reportDeletion.ts';
|
|
|
10
10
|
// FUNCTIONS
|
|
11
11
|
|
|
12
12
|
// Returns a form for deleting latest superseding reports.
|
|
13
|
-
export const answer = async (_: any, search: string) => reportDeletionForm(import.meta.dirname, search, {
|
|
13
|
+
export const answer = async (_: any, search: string, method: string) => reportDeletionForm(import.meta.dirname, search, method, {
|
|
14
14
|
intro: 'Choose the latest superseding reports to delete.',
|
|
15
15
|
emptyIntro: 'Each target has only 1 report, so there are no reports to delete.',
|
|
16
16
|
failurePrefix: 'Deleting latest superseding reports failed',
|
|
@@ -35,32 +35,40 @@ after(async () => {
|
|
|
35
35
|
|
|
36
36
|
// TESTS
|
|
37
37
|
|
|
38
|
-
test('rewindReportsForm displays a list of reports
|
|
39
|
-
const result = await answer(null, '');
|
|
38
|
+
test('rewindReportsForm displays a list of reports on a GET request', async () => {
|
|
39
|
+
const result = await answer(null, '', 'GET');
|
|
40
40
|
assert.equal(result.status, 'ok');
|
|
41
41
|
assert.ok(result.answerPage);
|
|
42
42
|
const html = parse(result.answerPage);
|
|
43
43
|
assert.ok(html.querySelector('title'));
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
test('rewindReportsForm
|
|
47
|
-
const
|
|
46
|
+
test('rewindReportsForm ignores a report query string on a GET request', async () => {
|
|
47
|
+
const reportPath = path.join(reportsPath(), '260202T0000-new.json');
|
|
48
|
+
const result = await answer(null, 'authCode=test-auth-code&report=260202T0000-new', 'GET');
|
|
49
|
+
assert.equal(result.status, 'ok');
|
|
50
|
+
// The report should NOT have been deleted by a mere GET request.
|
|
51
|
+
await fs.access(reportPath);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('rewindReportsForm returns an error for an invalid auth code on a POST request', async () => {
|
|
55
|
+
const result = await answer(null, 'authCode=wrong&report=260202T0000-new', 'POST');
|
|
48
56
|
assert.equal(result.status, 'error');
|
|
49
|
-
assert.equal(result.message, 'Invalid
|
|
57
|
+
assert.equal(result.message, 'Invalid request');
|
|
50
58
|
});
|
|
51
59
|
|
|
52
|
-
test('rewindReportsForm returns an error when deleting a nonexistent report', async () => {
|
|
53
|
-
const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
60
|
+
test('rewindReportsForm returns an error when deleting a nonexistent report on a POST request', async () => {
|
|
61
|
+
const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope', 'POST');
|
|
54
62
|
assert.equal(result.status, 'error');
|
|
55
63
|
assert.ok(result.message?.includes('Deleting latest superseding reports'));
|
|
56
64
|
});
|
|
57
65
|
|
|
58
|
-
test('rewindReportsForm deletes a latest superseding report with valid auth code', async () => {
|
|
66
|
+
test('rewindReportsForm deletes a latest superseding report with valid auth code on a POST request', async () => {
|
|
59
67
|
// 260202T0000-new is the latest report for its URL, superseding 260101T0000-mix.
|
|
60
68
|
const reportPath = path.join(reportsPath(), '260202T0000-new.json');
|
|
61
69
|
const backup = await fs.readFile(reportPath, 'utf8');
|
|
62
70
|
try {
|
|
63
|
-
const result = await answer(null, 'authCode=test-auth-code&report=260202T0000-new');
|
|
71
|
+
const result = await answer(null, 'authCode=test-auth-code&report=260202T0000-new', 'POST');
|
|
64
72
|
assert.equal(result.status, 'ok');
|
|
65
73
|
await fs.access(reportPath).then(
|
|
66
74
|
() => {throw new Error('Report should have been deleted');},
|
|
@@ -77,7 +85,7 @@ test('rewindReportsForm returns an error when a report file is corrupt', {timeou
|
|
|
77
85
|
const backup = await fs.readFile(reportPath, 'utf8');
|
|
78
86
|
try {
|
|
79
87
|
await fs.writeFile(reportPath, 'not valid json');
|
|
80
|
-
const result = await answer(null, '');
|
|
88
|
+
const result = await answer(null, '', 'GET');
|
|
81
89
|
assert.equal(result.status, 'error');
|
|
82
90
|
}
|
|
83
91
|
finally {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
|
+
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
|
+
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
+
<title>View experimental reports | Kilotest</title>
|
|
10
|
+
<link rel="icon" href="/favicon.ico">
|
|
11
|
+
<link rel="stylesheet" href="/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<h1><a href="/">Kilotest</a>: View experimental reports</h1>
|
|
16
|
+
<form action="/showHiddenReportsForm.html" method="post">
|
|
17
|
+
<p><label>
|
|
18
|
+
Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required>
|
|
19
|
+
</label></p>
|
|
20
|
+
<p><button type="submit">Submit</button></p>
|
|
21
|
+
</form>
|
|
22
|
+
</main>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Serves a form requesting an authorization code, and, once a valid code is submitted,
|
|
4
|
+
serves unhideReportForm's own list of hidden reports and its self-submitting unhide
|
|
5
|
+
form, so that the names of hidden reports are never disclosed without a valid code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// IMPORTS
|
|
9
|
+
|
|
10
|
+
import {populateTemplate} from '../../util.ts';
|
|
11
|
+
import {answer as unhideReportForm} from '../unhideReportForm/index.ts';
|
|
12
|
+
|
|
13
|
+
// FUNCTIONS
|
|
14
|
+
|
|
15
|
+
// Returns a form requesting an authorization code on a GET request or an invalid
|
|
16
|
+
// submission, or, on a POST request with a valid code, unhideReportForm's own page
|
|
17
|
+
// (the list of hidden reports and its unhide form). A GET request, regardless of its
|
|
18
|
+
// query string, never processes a submission, so the code cannot be validated by GET.
|
|
19
|
+
export const answer = async (pathTail: any, search: string, method: string) => {
|
|
20
|
+
const searchParams = new URLSearchParams(search);
|
|
21
|
+
const authCode = searchParams?.get('authCode');
|
|
22
|
+
// If the form has been submitted:
|
|
23
|
+
if (method === 'POST') {
|
|
24
|
+
// If the authorization code is invalid:
|
|
25
|
+
if (authCode !== process.env.AUTH_CODE) {
|
|
26
|
+
// Report the error, deliberately vague so as not to confirm to an attacker that the
|
|
27
|
+
// authorization code specifically (as opposed to some other part of the request) is
|
|
28
|
+
// what was wrong.
|
|
29
|
+
return {
|
|
30
|
+
status: 'error',
|
|
31
|
+
message: 'Invalid request'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Otherwise, i.e. if the authorization code is valid, serve unhideReportForm's own
|
|
35
|
+
// page (the list of hidden reports and its self-submitting unhide form).
|
|
36
|
+
return unhideReportForm(pathTail, search, 'GET');
|
|
37
|
+
}
|
|
38
|
+
// Otherwise, i.e. if the form has not been submitted yet, get the populated template.
|
|
39
|
+
const answerPage = await populateTemplate(import.meta.dirname, {});
|
|
40
|
+
// Return the populated page.
|
|
41
|
+
return {
|
|
42
|
+
status: 'ok',
|
|
43
|
+
answerPage
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
showHiddenReportsForm.test.ts
|
|
3
|
+
Unit tests for web/showHiddenReportsForm/index.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {test, before, after, beforeEach} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import {parse} from 'node-html-parser';
|
|
13
|
+
import {answer} from './index.ts';
|
|
14
|
+
import {reportsPath, hiddenReportsPath} from '../../util.ts';
|
|
15
|
+
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
|
|
18
|
+
const hiddenFile = '260101T0007-hid.json';
|
|
19
|
+
|
|
20
|
+
// SETUP AND TEARDOWN
|
|
21
|
+
|
|
22
|
+
const savedAuthCode = process.env.AUTH_CODE;
|
|
23
|
+
const savedDBDir = process.env.DB_DIR;
|
|
24
|
+
import {fixtureDBDir} from '../../test/dbFixture.ts';
|
|
25
|
+
|
|
26
|
+
const getPaths = () => ({
|
|
27
|
+
hidden: path.join(hiddenReportsPath(), hiddenFile),
|
|
28
|
+
report: path.join(reportsPath(), hiddenFile)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
before(() => {
|
|
32
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
33
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
// Ensure the hidden report is in the hidden directory before each test.
|
|
38
|
+
const {hidden, report} = getPaths();
|
|
39
|
+
try {
|
|
40
|
+
await fs.rename(report, hidden);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Report may already be hidden.
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
after(async () => {
|
|
48
|
+
// Restore the hidden report if it was moved.
|
|
49
|
+
const {hidden, report} = getPaths();
|
|
50
|
+
try {
|
|
51
|
+
await fs.rename(report, hidden);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Report may already be hidden.
|
|
55
|
+
}
|
|
56
|
+
if (savedAuthCode !== undefined) {
|
|
57
|
+
process.env.AUTH_CODE = savedAuthCode;
|
|
58
|
+
}
|
|
59
|
+
if (savedDBDir !== undefined) {
|
|
60
|
+
process.env.DB_DIR = savedDBDir;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// TESTS
|
|
65
|
+
|
|
66
|
+
test('showHiddenReportsForm displays a bare authCode form on a GET request, disclosing nothing', async () => {
|
|
67
|
+
const result: any = await answer(null, '', 'GET');
|
|
68
|
+
assert.equal(result.status, 'ok');
|
|
69
|
+
const html = parse(result.answerPage);
|
|
70
|
+
assert.ok(html.querySelector('input[name="authCode"]'));
|
|
71
|
+
assert.equal(html.querySelectorAll('input[type="radio"]').length, 0);
|
|
72
|
+
assert.ok(!result.answerPage.includes('260101T0007-hid'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('showHiddenReportsForm submits its form as a POST', async () => {
|
|
76
|
+
const result: any = await answer(null, '', 'GET');
|
|
77
|
+
const html = parse(result.answerPage);
|
|
78
|
+
const form = html.querySelector('form');
|
|
79
|
+
assert.equal(form?.getAttribute('method'), 'post');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('showHiddenReportsForm ignores a valid authCode query string on a GET request', async () => {
|
|
83
|
+
const result: any = await answer(null, 'authCode=test-auth-code', 'GET');
|
|
84
|
+
assert.equal(result.status, 'ok');
|
|
85
|
+
const html = parse(result.answerPage);
|
|
86
|
+
assert.ok(html.querySelector('input[name="authCode"]'));
|
|
87
|
+
assert.equal(html.querySelectorAll('input[type="radio"]').length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('showHiddenReportsForm returns an error for an invalid auth code on a POST request', async () => {
|
|
91
|
+
const result: any = await answer(null, 'authCode=wrong', 'POST');
|
|
92
|
+
assert.equal(result.status, 'error');
|
|
93
|
+
assert.equal(result.message, 'Invalid request');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('showHiddenReportsForm serves the hidden reports list on a POST request with a valid auth code', async () => {
|
|
97
|
+
const result: any = await answer(null, 'authCode=test-auth-code', 'POST');
|
|
98
|
+
assert.equal(result.status, 'ok');
|
|
99
|
+
const html = parse(result.answerPage);
|
|
100
|
+
const radios = html.querySelectorAll('input[type="radio"]');
|
|
101
|
+
assert.ok(radios.length > 0);
|
|
102
|
+
assert.ok(result.answerPage.includes('260101T0007-hid'));
|
|
103
|
+
});
|
package/web/tutorialAI/index.ts
CHANGED
|
@@ -75,6 +75,6 @@ export const handleComment = async (content: unknown) => {
|
|
|
75
75
|
// Save the revised comments.
|
|
76
76
|
await fs.writeFile(commentsPath, getJSON(comments));
|
|
77
77
|
// Send an alert to the manager.
|
|
78
|
-
await sendAlert('New AI tutorial comment received', 'A new AI tutorial comment has been received.');
|
|
78
|
+
await sendAlert('Kilotest: New AI tutorial comment received', 'A new AI tutorial comment has been received.');
|
|
79
79
|
return {status: 'ok'};
|
|
80
80
|
};
|
package/web/tutorialWeb/index.ts
CHANGED
|
@@ -73,6 +73,6 @@ export const handleComment = async (content: unknown) => {
|
|
|
73
73
|
// Save the revised comments.
|
|
74
74
|
await fs.writeFile(commentsPath, getJSON(comments));
|
|
75
75
|
// Send an alert to the manager.
|
|
76
|
-
await sendAlert('New web tutorial comment received', 'A new web tutorial comment has been received.');
|
|
76
|
+
await sendAlert('Kilotest: New web tutorial comment received', 'A new web tutorial comment has been received.');
|
|
77
77
|
return {status: 'ok'};
|
|
78
78
|
};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<main>
|
|
15
15
|
<h1><a href="/">Kilotest</a>: Declassify report as experimental</h1>
|
|
16
16
|
<p>Choose a report to declassify as experimental.</p>
|
|
17
|
-
<form action="/unhideReportForm.html">
|
|
17
|
+
<form action="/unhideReportForm.html" method="post">
|
|
18
18
|
<fieldset>
|
|
19
19
|
<legend>Reports</legend>
|
|
20
20
|
__reports__
|
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
import {hiddenReportsPath, populateTemplate, reportsPath} from '../../util.ts';
|
|
8
|
+
import {hiddenReportsPath, populateTemplate, readdirOrCreate, reportsPath} from '../../util.ts';
|
|
9
9
|
import fs from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
12
|
// FUNCTIONS
|
|
13
13
|
|
|
14
|
-
// Returns a form for unhiding a report.
|
|
15
|
-
|
|
14
|
+
// Returns a form for unhiding a report. A GET request never processes a submission,
|
|
15
|
+
// regardless of its query string; only a POST request (the form's own submission) does.
|
|
16
|
+
export const answer = async (_: any, search: string, method: string) => {
|
|
16
17
|
const searchParams = new URLSearchParams(search);
|
|
17
18
|
const authCode = searchParams?.get('authCode');
|
|
18
19
|
const jobName = searchParams?.get('report');
|
|
19
|
-
// If the form has been
|
|
20
|
-
if (jobName) {
|
|
20
|
+
// If the form has been submitted and a report is to be unhidden:
|
|
21
|
+
if (method === 'POST' && jobName) {
|
|
21
22
|
// If the authorization code is valid:
|
|
22
23
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
24
|
const fileName = `${jobName}.json`;
|
|
@@ -35,17 +36,19 @@ export const answer = async (_: any, search: string) => {
|
|
|
35
36
|
}
|
|
36
37
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
37
38
|
else {
|
|
38
|
-
// Report the error
|
|
39
|
+
// Report the error, deliberately vague so as not to confirm to an attacker that the
|
|
40
|
+
// authorization code specifically (as opposed to some other part of the request) is
|
|
41
|
+
// what was wrong.
|
|
39
42
|
return {
|
|
40
43
|
status: 'error',
|
|
41
|
-
message: 'Invalid
|
|
44
|
+
message: 'Invalid request'
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
// Initialize an array of data on reports to be unhidden.
|
|
46
49
|
const reportsData: any[] = [];
|
|
47
50
|
// Get the names of the hidden report files.
|
|
48
|
-
const hiddenReportFileNames = await
|
|
51
|
+
const hiddenReportFileNames = await readdirOrCreate(hiddenReportsPath(), 'Hidden reports directory');
|
|
49
52
|
// For each hidden report:
|
|
50
53
|
for (const reportFileName of hiddenReportFileNames) {
|
|
51
54
|
// Get its file.
|
|
@@ -63,8 +63,8 @@ after(async () => {
|
|
|
63
63
|
|
|
64
64
|
// TESTS
|
|
65
65
|
|
|
66
|
-
test('unhideReportForm displays a list of hidden reports
|
|
67
|
-
const result: any = await answer(null, '');
|
|
66
|
+
test('unhideReportForm displays a list of hidden reports on a GET request', async () => {
|
|
67
|
+
const result: any = await answer(null, '', 'GET');
|
|
68
68
|
assert.equal(result.status, 'ok');
|
|
69
69
|
assert.ok(result.answerPage);
|
|
70
70
|
const html = parse(result.answerPage);
|
|
@@ -72,22 +72,30 @@ test('unhideReportForm displays a list of hidden reports when no submission', as
|
|
|
72
72
|
assert.ok(radios.length > 0);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('unhideReportForm
|
|
76
|
-
const
|
|
75
|
+
test('unhideReportForm ignores a report query string on a GET request', async () => {
|
|
76
|
+
const {report} = getPaths();
|
|
77
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0007-hid', 'GET');
|
|
78
|
+
assert.equal(result.status, 'ok');
|
|
79
|
+
// The report should NOT have been unhidden by a mere GET request.
|
|
80
|
+
await assert.rejects(fs.access(report));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('unhideReportForm returns an error for an invalid auth code on a POST request', async () => {
|
|
84
|
+
const result: any = await answer(null, 'authCode=wrong&report=260101T0007-hid', 'POST');
|
|
77
85
|
assert.equal(result.status, 'error');
|
|
78
|
-
assert.equal(result.message, 'Invalid
|
|
86
|
+
assert.equal(result.message, 'Invalid request');
|
|
79
87
|
});
|
|
80
88
|
|
|
81
|
-
test('unhideReportForm unhides a report with valid auth code', async () => {
|
|
89
|
+
test('unhideReportForm unhides a report with valid auth code on a POST request', async () => {
|
|
82
90
|
const {report} = getPaths();
|
|
83
|
-
const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0007-hid');
|
|
91
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0007-hid', 'POST');
|
|
84
92
|
assert.equal(result.status, 'ok');
|
|
85
93
|
// The report should now be in the reports directory.
|
|
86
94
|
await fs.access(report);
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
test('unhideReportForm returns an error when unhiding a nonexistent report', async () => {
|
|
90
|
-
const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
97
|
+
test('unhideReportForm returns an error when unhiding a nonexistent report on a POST request', async () => {
|
|
98
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope', 'POST');
|
|
91
99
|
assert.equal(result.status, 'error');
|
|
92
100
|
assert.ok(result.message.includes('Unhiding report'));
|
|
93
101
|
});
|
|
@@ -102,7 +110,7 @@ test('unhideReportForm sorts hidden reports with the same page name by timeStamp
|
|
|
102
110
|
const copyPath = path.join(hiddenReportsPath(), '260101T0010-hid2.json');
|
|
103
111
|
await fs.writeFile(copyPath, JSON.stringify(copy));
|
|
104
112
|
try {
|
|
105
|
-
const result: any = await answer(null, '');
|
|
113
|
+
const result: any = await answer(null, '', 'GET');
|
|
106
114
|
assert.equal(result.status, 'ok');
|
|
107
115
|
// Both reports should appear in the form.
|
|
108
116
|
assert.ok(result.answerPage.includes('260101T0007-hid'));
|
|
@@ -123,7 +131,7 @@ test('unhideReportForm sorts hidden reports with different page names alphabetic
|
|
|
123
131
|
const copyPath = path.join(hiddenReportsPath(), '260101T0011-hid3.json');
|
|
124
132
|
await fs.writeFile(copyPath, JSON.stringify(copy));
|
|
125
133
|
try {
|
|
126
|
-
const result: any = await answer(null, '');
|
|
134
|
+
const result: any = await answer(null, '', 'GET');
|
|
127
135
|
assert.equal(result.status, 'ok');
|
|
128
136
|
// Both reports should appear in the form.
|
|
129
137
|
assert.ok(result.answerPage.includes('260101T0007-hid'));
|