@jrpool/kilotest 46.1.0 → 47.0.1
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 +8 -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.js → requestRetest.test.ts} +33 -28
- package/api/{requestRetest.js → requestRetest.ts} +32 -25
- package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
- package/api/{requestTest.js → requestTest.ts} +35 -28
- 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} +31 -11
- package/api/{util.js → util.ts} +75 -46
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +356 -0
- package/docs/SERVICE.md +144 -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} +462 -137
- package/{index.js → index.ts} +388 -283
- 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.js → util.test.ts} +350 -257
- package/util.ts +921 -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} +48 -53
- 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 +70 -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} +56 -38
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +31 -0
- package/web/requestRetest/requestRetest.test.ts +116 -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.js → index.ts} +5 -5
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
- 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} +17 -21
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -87
- package/api/requestFeature.test.js +0 -58
- 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 -126
- package/util.js +0 -733
- 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/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,25 +1,25 @@
|
|
|
1
1
|
/*
|
|
2
|
-
ai0BalanceForm.test.
|
|
3
|
-
Unit tests for web/ai0BalanceForm/index.
|
|
2
|
+
ai0BalanceForm.test.ts
|
|
3
|
+
Unit tests for web/ai0BalanceForm/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import {test, before, after} 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
14
|
|
|
15
15
|
// CONSTANTS
|
|
16
16
|
|
|
17
|
-
const balancePath = path.join(
|
|
17
|
+
const balancePath = path.join(import.meta.dirname, '..', '..', 'ai0Balance.json');
|
|
18
18
|
|
|
19
19
|
// SETUP AND TEARDOWN
|
|
20
20
|
|
|
21
21
|
const savedAuthCode = process.env.AUTH_CODE;
|
|
22
|
-
let savedBalance;
|
|
22
|
+
let savedBalance: any;
|
|
23
23
|
|
|
24
24
|
before(async () => {
|
|
25
25
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
@@ -45,7 +45,7 @@ after(async () => {
|
|
|
45
45
|
// TESTS
|
|
46
46
|
|
|
47
47
|
test('ai0BalanceForm displays the current balance when no newBalance is submitted', async () => {
|
|
48
|
-
const result = await answer(null, '');
|
|
48
|
+
const result: any = await answer(null, '');
|
|
49
49
|
assert.equal(result.status, 'ok');
|
|
50
50
|
assert.ok(result.answerPage);
|
|
51
51
|
const html = parse(result.answerPage);
|
|
@@ -53,13 +53,13 @@ test('ai0BalanceForm displays the current balance when no newBalance is submitte
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test('ai0BalanceForm returns an error for an invalid auth code', async () => {
|
|
56
|
-
const result = await answer(null, 'authCode=wrong&newBalance=5.00');
|
|
56
|
+
const result: any = await answer(null, 'authCode=wrong&newBalance=5.00');
|
|
57
57
|
assert.equal(result.status, 'error');
|
|
58
58
|
assert.equal(result.message, 'Invalid authorization code');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
test('ai0BalanceForm records a valid new balance with valid auth code', async () => {
|
|
62
|
-
const result = await answer(null, 'authCode=test-auth-code&newBalance=2.50');
|
|
62
|
+
const result: any = await answer(null, 'authCode=test-auth-code&newBalance=2.50');
|
|
63
63
|
assert.equal(result.status, 'ok');
|
|
64
64
|
assert.ok(result.answerPage.includes('$2.5 is the'));
|
|
65
65
|
// Verify the file was written.
|
|
@@ -69,7 +69,7 @@ test('ai0BalanceForm records a valid new balance with valid auth code', async ()
|
|
|
69
69
|
|
|
70
70
|
test('ai0BalanceForm does not record an invalid balance', async () => {
|
|
71
71
|
// 150 is out of range (>= 100).
|
|
72
|
-
const result = await answer(null, 'authCode=test-auth-code&newBalance=150');
|
|
72
|
+
const result: any = await answer(null, 'authCode=test-auth-code&newBalance=150');
|
|
73
73
|
assert.equal(result.status, 'ok');
|
|
74
74
|
// The oldBalance should not show the new value.
|
|
75
75
|
assert.ok(!result.answerPage.includes('$150 is the'));
|
|
@@ -78,7 +78,7 @@ test('ai0BalanceForm does not record an invalid balance', async () => {
|
|
|
78
78
|
test('ai0BalanceForm shows no-balance message when balance file is missing', async () => {
|
|
79
79
|
await fs.unlink(balancePath).catch(() => {});
|
|
80
80
|
try {
|
|
81
|
-
const result = await answer(null, '');
|
|
81
|
+
const result: any = await answer(null, '');
|
|
82
82
|
assert.equal(result.status, 'ok');
|
|
83
83
|
assert.ok(result.answerPage.includes('There is no'));
|
|
84
84
|
}
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
3
|
-
Serves a form for
|
|
2
|
+
index.ts
|
|
3
|
+
Serves a form for recording the AI service 0 balance.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// CONSTANTS
|
|
13
|
-
|
|
14
|
-
const balancePath = path.join(__dirname, '../../ai0Balance.json');
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import {balancePath} from '../../balances.ts';
|
|
10
|
+
import {getJSON, populateTemplate} from '../../util.ts';
|
|
15
11
|
|
|
16
12
|
// FUNCTIONS
|
|
17
13
|
|
|
18
14
|
// Returns a form for recording the AI service 0 balance.
|
|
19
|
-
|
|
15
|
+
export const answer = async (_: any, search: string) => {
|
|
20
16
|
const searchParams = new URLSearchParams(search);
|
|
21
17
|
const authCode = searchParams?.get('authCode');
|
|
22
18
|
const newBalanceString = searchParams?.get('newBalance');
|
|
23
|
-
let oldBalance;
|
|
19
|
+
let oldBalance: any;
|
|
24
20
|
// If the form displayed itself:
|
|
25
21
|
if (newBalanceString) {
|
|
26
22
|
// If the authorization code is valid:
|
|
@@ -64,13 +60,9 @@ exports.answer = async (_, search) => {
|
|
|
64
60
|
oldBalance = 'There is no';
|
|
65
61
|
}
|
|
66
62
|
}
|
|
67
|
-
const query = {oldBalance};
|
|
68
|
-
// Get the order form template.
|
|
69
|
-
|
|
70
|
-
// Replace its placeholders.
|
|
71
|
-
Object.keys(query).forEach(param => {
|
|
72
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
73
|
-
});
|
|
63
|
+
const query: Record<string, any> = {oldBalance};
|
|
64
|
+
// Get the populated order form template.
|
|
65
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
74
66
|
// Return the populated page.
|
|
75
67
|
return {
|
|
76
68
|
status: 'ok',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Implements a test request approval.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
deleteTestRequests, getJSON, getNowStamp, getRandomString, isURL, jobsPath, populateTemplate
|
|
10
|
+
} from '../../util.ts';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
// FUNCTIONS
|
|
15
|
+
|
|
16
|
+
// Implements a test request approval and returns a revised request page.
|
|
17
|
+
export const answer = async (url: string, description: string, authCode: string) => {
|
|
18
|
+
// If the arguments are valid:
|
|
19
|
+
if (isURL(url) && description && authCode === process.env.AUTH_CODE) {
|
|
20
|
+
// Get the job template.
|
|
21
|
+
const jobTemplateJSON = await fs.readFile(path.join(import.meta.dirname, '..', '..', 'job.json'), 'utf8');
|
|
22
|
+
const job = JSON.parse(jobTemplateJSON);
|
|
23
|
+
const nowStamp = getNowStamp();
|
|
24
|
+
// Populate the template with job properties.
|
|
25
|
+
const jobIDSuffix = getRandomString(3);
|
|
26
|
+
const jobName = `${nowStamp}-${jobIDSuffix}`;
|
|
27
|
+
job.id = jobName;
|
|
28
|
+
job.creationTimeStamp = nowStamp;
|
|
29
|
+
job.executionTimeStamp = nowStamp;
|
|
30
|
+
job.target.what = description;
|
|
31
|
+
job.target.url = url;
|
|
32
|
+
const query: Record<string, string> = {
|
|
33
|
+
target: description,
|
|
34
|
+
jobName
|
|
35
|
+
};
|
|
36
|
+
// Save the job in the queue.
|
|
37
|
+
await fs.writeFile(
|
|
38
|
+
path.join(jobsPath(), 'queue', `${jobName}.json`), getJSON(job)
|
|
39
|
+
);
|
|
40
|
+
console.log(`Retest queued for ${description} as job ${jobName}`);
|
|
41
|
+
// Delete the test requests for the target.
|
|
42
|
+
await deleteTestRequests(url);
|
|
43
|
+
// Get the populated answer template.
|
|
44
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
45
|
+
// Return the populated page.
|
|
46
|
+
return {
|
|
47
|
+
status: 'ok',
|
|
48
|
+
answerPage
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Otherwise, i.e. if the authorization code is invalid, return an error page.
|
|
52
|
+
return {
|
|
53
|
+
status: 'error',
|
|
54
|
+
message: 'Invalid authorization code'
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -6,21 +6,21 @@
|
|
|
6
6
|
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
7
|
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
8
|
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
-
<title>
|
|
9
|
+
<title>Request approval or rejection | Kilotest</title>
|
|
10
10
|
<link rel="icon" href="/favicon.ico">
|
|
11
11
|
<link rel="stylesheet" href="/style.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<main>
|
|
15
|
-
<h1><a href="/">Kilotest</a>:
|
|
16
|
-
<p>To approve a test or retest
|
|
17
|
-
<p>To approve testing a page that
|
|
18
|
-
<p>To reject all
|
|
19
|
-
<form action="/
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Request approval or rejection</h1>
|
|
16
|
+
<p>To approve a test or retest request, check its requested page name.</p>
|
|
17
|
+
<p>To approve testing a page that has not yet been requested, first <a href="listReports.html">request it</a>.</p>
|
|
18
|
+
<p>To reject all requests for a page, check the radio button of its URL.</p>
|
|
19
|
+
<form action="/requestAction.html" method="post">
|
|
20
20
|
<fieldset>
|
|
21
|
-
<legend>
|
|
22
|
-
|
|
23
|
-
<p>
|
|
21
|
+
<legend>Requests</legend>
|
|
22
|
+
__requests__
|
|
23
|
+
<p>__noRequests__</p>
|
|
24
24
|
</fieldset>
|
|
25
25
|
<p><label>
|
|
26
26
|
Authorization code:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Serves a form for approving or rejecting a test request.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {getTestRequests, populateTemplate} from '../../util.ts';
|
|
9
|
+
|
|
10
|
+
// FUNCTIONS
|
|
11
|
+
|
|
12
|
+
// Returns a test order form.
|
|
13
|
+
export const answer = async () => {
|
|
14
|
+
const testRequests = await getTestRequests() as Record<string, {description: string}[]>;
|
|
15
|
+
const urls = Object.keys(testRequests);
|
|
16
|
+
const margin = ' '.repeat(12);
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
// For each page with any requests:
|
|
19
|
+
urls.forEach(url => {
|
|
20
|
+
// Add a radio button and its URL to the lines.
|
|
21
|
+
lines.push(`${margin}<h2><input type="radio" name="target" value="${url}" required> ${url}</h2>`);
|
|
22
|
+
// Get the requested target names for the page.
|
|
23
|
+
const targetNames = new Set(testRequests[url]!.map((req: any) => req.description));
|
|
24
|
+
// For each requested target name:
|
|
25
|
+
targetNames.forEach(description => {
|
|
26
|
+
const radio = `<input type="radio" name="target" value="${url}\t${description}" required>`;
|
|
27
|
+
// Add a radio button and the requested page name to the lines.
|
|
28
|
+
lines.push(`${margin} <p>${radio} ${description}</p>`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
const query: Record<string, string> = {
|
|
32
|
+
requests: lines.join('\n'),
|
|
33
|
+
noRequests: urls.length ? '' : 'No requests exist now.',
|
|
34
|
+
disabled: urls.length ? '' : ' disabled'
|
|
35
|
+
};
|
|
36
|
+
// Get the populated order form template.
|
|
37
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
38
|
+
// Return the populated page.
|
|
39
|
+
return {
|
|
40
|
+
status: 'ok',
|
|
41
|
+
answerPage
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/*
|
|
2
|
-
expungeReportsForm.test.
|
|
3
|
-
Unit tests for web/expungeReportsForm/index.
|
|
2
|
+
expungeReportsForm.test.ts
|
|
3
|
+
Unit tests for web/expungeReportsForm/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import {test, before, after} 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} from '../../util.ts';
|
|
15
15
|
|
|
16
16
|
// SETUP AND TEARDOWN
|
|
17
17
|
|
|
18
18
|
const savedAuthCode = process.env.AUTH_CODE;
|
|
19
19
|
const savedDBDir = process.env.DB_DIR;
|
|
20
|
-
|
|
20
|
+
import {fixtureDBDir} from '../../test/dbFixture.ts';
|
|
21
21
|
|
|
22
22
|
before(() => {
|
|
23
23
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
@@ -36,7 +36,7 @@ after(async () => {
|
|
|
36
36
|
// TESTS
|
|
37
37
|
|
|
38
38
|
test('expungeReportsForm displays a list of reports when no submission', async () => {
|
|
39
|
-
const result = await answer(null, '');
|
|
39
|
+
const result: any = await answer(null, '');
|
|
40
40
|
assert.equal(result.status, 'ok');
|
|
41
41
|
assert.ok(result.answerPage);
|
|
42
42
|
const html = parse(result.answerPage);
|
|
@@ -44,13 +44,13 @@ test('expungeReportsForm displays a list of reports when no submission', async (
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
test('expungeReportsForm returns an error for an invalid auth code', async () => {
|
|
47
|
-
const result = await answer(null, 'authCode=wrong&report=260101T0001-ct');
|
|
47
|
+
const result: any = await answer(null, 'authCode=wrong&report=260101T0001-ct');
|
|
48
48
|
assert.equal(result.status, 'error');
|
|
49
49
|
assert.equal(result.message, 'Invalid authorization code');
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('expungeReportsForm returns an error when deleting a nonexistent report', async () => {
|
|
53
|
-
const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
53
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
54
54
|
assert.equal(result.status, 'error');
|
|
55
55
|
assert.ok(result.message.includes('Deleting sole reports'));
|
|
56
56
|
});
|
|
@@ -60,7 +60,7 @@ test('expungeReportsForm deletes a sole report with valid auth code', async () =
|
|
|
60
60
|
const reportPath = path.join(reportsPath(), '260101T0001-ct.json');
|
|
61
61
|
const backup = await fs.readFile(reportPath, 'utf8');
|
|
62
62
|
try {
|
|
63
|
-
const result = await answer(null, 'authCode=test-auth-code&report=260101T0001-ct');
|
|
63
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0001-ct');
|
|
64
64
|
assert.equal(result.status, 'ok');
|
|
65
65
|
await fs.access(reportPath).then(
|
|
66
66
|
() => {throw new Error('Report should have been deleted');},
|
|
@@ -77,7 +77,7 @@ test('expungeReportsForm returns an error when a report file is corrupt', async
|
|
|
77
77
|
const backup = await fs.readFile(reportPath, 'utf8');
|
|
78
78
|
try {
|
|
79
79
|
await fs.writeFile(reportPath, 'not valid json');
|
|
80
|
-
const result = await answer(null, '');
|
|
80
|
+
const result: any = await answer(null, '');
|
|
81
81
|
assert.equal(result.status, 'error');
|
|
82
82
|
}
|
|
83
83
|
finally {
|
|
@@ -87,7 +87,7 @@ test('expungeReportsForm returns an error when a report file is corrupt', async
|
|
|
87
87
|
|
|
88
88
|
test('expungeReportsForm shows no-deletable message when every URL has at least 2 reports', async () => {
|
|
89
89
|
// Create a temp DB with two reports sharing the same URL.
|
|
90
|
-
const os =
|
|
90
|
+
const os = await import('node:os');
|
|
91
91
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'expunge-test-'));
|
|
92
92
|
const tmpReports = path.join(tmpDir, 'reports');
|
|
93
93
|
await fs.mkdir(tmpReports);
|
|
@@ -99,16 +99,14 @@ test('expungeReportsForm shows no-deletable message when every URL has at least
|
|
|
99
99
|
const savedDBDir = process.env.DB_DIR;
|
|
100
100
|
process.env.DB_DIR = tmpDir;
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const result = await answer(null, '');
|
|
102
|
+
const {answer} = await import('./index.ts');
|
|
103
|
+
const result: any = await answer(null, '');
|
|
105
104
|
assert.equal(result.status, 'ok');
|
|
106
105
|
assert.ok(result.answerPage.includes('no reports to delete'));
|
|
107
106
|
assert.ok(result.answerPage.includes('disabled'));
|
|
108
107
|
}
|
|
109
108
|
finally {
|
|
110
109
|
process.env.DB_DIR = savedDBDir;
|
|
111
|
-
delete require.cache[require.resolve('./index')];
|
|
112
110
|
await fs.rm(tmpDir, {recursive: true}).catch(() => {});
|
|
113
111
|
}
|
|
114
112
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Serves a form for deleting sole reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {reportDeletionForm} from '../reportDeletion.ts';
|
|
9
|
+
|
|
10
|
+
// FUNCTIONS
|
|
11
|
+
|
|
12
|
+
// Returns a form for deleting sole reports.
|
|
13
|
+
export const answer = async (_: any, search: string) => reportDeletionForm(import.meta.dirname, search, {
|
|
14
|
+
intro: 'Choose the sole reports to delete.',
|
|
15
|
+
emptyIntro: 'Each target has at least 2 reports, so there are no reports to delete.',
|
|
16
|
+
failurePrefix: 'Deleting sole reports failed',
|
|
17
|
+
isDeletable: (specs, index) => specs[index - 1]?.url !== specs[index]?.url && specs[index + 1]?.url !== specs[index]?.url
|
|
18
|
+
});
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/*
|
|
2
|
-
hideReportForm.test.
|
|
3
|
-
Unit tests for web/hideReportForm/index.
|
|
2
|
+
hideReportForm.test.ts
|
|
3
|
+
Unit tests for web/hideReportForm/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import {test, before, after} 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
15
|
|
|
16
16
|
// SETUP AND TEARDOWN
|
|
17
17
|
|
|
18
18
|
const savedAuthCode = process.env.AUTH_CODE;
|
|
19
19
|
const savedDBDir = process.env.DB_DIR;
|
|
20
|
-
|
|
20
|
+
import {fixtureDBDir} from '../../test/dbFixture.ts';
|
|
21
21
|
|
|
22
22
|
before(() => {
|
|
23
23
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
@@ -45,7 +45,7 @@ after(async () => {
|
|
|
45
45
|
// TESTS
|
|
46
46
|
|
|
47
47
|
test('hideReportForm displays a list of reports when no submission', async () => {
|
|
48
|
-
const result = await answer(null, '');
|
|
48
|
+
const result: any = await answer(null, '');
|
|
49
49
|
assert.equal(result.status, 'ok');
|
|
50
50
|
assert.ok(result.answerPage);
|
|
51
51
|
const html = parse(result.answerPage);
|
|
@@ -54,7 +54,7 @@ test('hideReportForm displays a list of reports when no submission', async () =>
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('hideReportForm returns an error for an invalid auth code', async () => {
|
|
57
|
-
const result = await answer(null, 'authCode=wrong&report=260101T0009-brd');
|
|
57
|
+
const result: any = await answer(null, 'authCode=wrong&report=260101T0009-brd');
|
|
58
58
|
assert.equal(result.status, 'error');
|
|
59
59
|
assert.equal(result.message, 'Invalid authorization code');
|
|
60
60
|
});
|
|
@@ -69,14 +69,14 @@ test('hideReportForm hides a report with valid auth code', async () => {
|
|
|
69
69
|
catch {
|
|
70
70
|
// Report may already be in reports.
|
|
71
71
|
}
|
|
72
|
-
const result = await answer(null, 'authCode=test-auth-code&report=260101T0009-brd');
|
|
72
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=260101T0009-brd');
|
|
73
73
|
assert.equal(result.status, 'ok');
|
|
74
74
|
// The report should now be in the hidden directory.
|
|
75
75
|
await fs.access(hiddenPath);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test('hideReportForm returns an error when hiding a nonexistent report', async () => {
|
|
79
|
-
const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
79
|
+
const result: any = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
|
|
80
80
|
assert.equal(result.status, 'error');
|
|
81
81
|
assert.ok(result.message.includes('Hiding report'));
|
|
82
82
|
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Serves a form for hiding a report.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {getReportExtracts, hiddenReportsPath, populateTemplate, reportsPath} from '../../util.ts';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
11
|
|
|
12
12
|
// FUNCTIONS
|
|
13
13
|
|
|
14
14
|
// Returns a form for hiding a report.
|
|
15
|
-
|
|
15
|
+
export const answer = async (_: any, search: string) => {
|
|
16
16
|
const searchParams = new URLSearchParams(search);
|
|
17
17
|
const authCode = searchParams?.get('authCode');
|
|
18
18
|
const jobName = searchParams?.get('report');
|
|
@@ -26,7 +26,7 @@ exports.answer = async (_, search) => {
|
|
|
26
26
|
await fs.rename(path.join(reportsPath(), fileName), path.join(hiddenReportsPath(), fileName));
|
|
27
27
|
}
|
|
28
28
|
// If this failed:
|
|
29
|
-
catch (error) {
|
|
29
|
+
catch (error: any) {
|
|
30
30
|
// Return why.
|
|
31
31
|
return {
|
|
32
32
|
status: 'error',
|
|
@@ -44,46 +44,42 @@ exports.answer = async (_, search) => {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
// Initialize an array of report specifications.
|
|
47
|
-
const reportSpecs = [];
|
|
47
|
+
const reportSpecs: any[] = [];
|
|
48
48
|
// Get data on all available reports.
|
|
49
49
|
const reportExtracts = await getReportExtracts();
|
|
50
50
|
// For each report:
|
|
51
51
|
for (const reportExtract of reportExtracts) {
|
|
52
|
-
const {jobID, timeStamp,
|
|
52
|
+
const {jobID, timeStamp, description} = reportExtract;
|
|
53
53
|
// Add the report to the array.
|
|
54
54
|
reportSpecs.push({
|
|
55
|
-
|
|
55
|
+
description,
|
|
56
56
|
timeStamp,
|
|
57
57
|
jobID
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
// Sort the data by page name and then by time stamp.
|
|
61
61
|
reportSpecs.sort((a, b) => {
|
|
62
|
-
if (a.
|
|
62
|
+
if (a.description === b.description) {
|
|
63
63
|
return a.timeStamp.localeCompare(b.timeStamp);
|
|
64
64
|
}
|
|
65
|
-
return a.
|
|
65
|
+
return a.description.localeCompare(b.description);
|
|
66
66
|
});
|
|
67
|
-
const lines = [];
|
|
67
|
+
const lines: string[] = [];
|
|
68
68
|
const margin = ' '.repeat(12);
|
|
69
69
|
// For each available report:
|
|
70
70
|
reportSpecs.forEach(spec => {
|
|
71
|
-
const {jobID, timeStamp,
|
|
72
|
-
const specString = `${
|
|
71
|
+
const {jobID, timeStamp, description} = spec;
|
|
72
|
+
const specString = `${description} (job <code>${jobID}</code> at ${timeStamp})`;
|
|
73
73
|
// Add a line with a radio button to hide it.
|
|
74
74
|
lines.push(
|
|
75
75
|
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
|
-
const query = {
|
|
78
|
+
const query: Record<string, string> = {
|
|
79
79
|
reports: lines.join('\n'),
|
|
80
80
|
};
|
|
81
|
-
// Get the hiding form template.
|
|
82
|
-
|
|
83
|
-
// Replace its placeholders.
|
|
84
|
-
Object.keys(query).forEach(param => {
|
|
85
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
86
|
-
});
|
|
81
|
+
// Get the populated hiding form template.
|
|
82
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
87
83
|
// Return the populated page.
|
|
88
84
|
return {
|
|
89
85
|
status: 'ok',
|