@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.
Files changed (63) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/api/requestFeature.test.ts +4 -2
  3. package/api/requestFeature.ts +1 -1
  4. package/api/requestRetest.test.ts +66 -7
  5. package/api/requestRetest.ts +55 -39
  6. package/api/requestTest.test.ts +63 -7
  7. package/api/requestTest.ts +36 -26
  8. package/api/util.test.ts +6 -5
  9. package/api/util.ts +2 -24
  10. package/docs/BACKLOG.md +228 -16
  11. package/docs/SERVICE.md +84 -7
  12. package/index.test.ts +354 -25
  13. package/index.ts +174 -8
  14. package/linkValidation.test.ts +1 -0
  15. package/mcp.test.ts +30 -0
  16. package/mcp.ts +9 -0
  17. package/package.json +1 -1
  18. package/smokeTest.ts +16 -0
  19. package/test/dbFixture.ts +8 -0
  20. package/util.test.ts +426 -127
  21. package/util.ts +389 -196
  22. package/web/ai0BalanceForm/ai0BalanceForm.test.ts +16 -10
  23. package/web/ai0BalanceForm/index.html +1 -1
  24. package/web/ai0BalanceForm/index.ts +10 -6
  25. package/web/enqueue/index.ts +5 -2
  26. package/web/expungeReportsForm/expungeReportsForm.test.ts +19 -11
  27. package/web/expungeReportsForm/index.html +1 -1
  28. package/web/expungeReportsForm/index.ts +1 -1
  29. package/web/hideReportForm/hideReportForm.test.ts +24 -9
  30. package/web/hideReportForm/index.html +1 -1
  31. package/web/hideReportForm/index.ts +9 -6
  32. package/web/listReports/index.ts +33 -12
  33. package/web/listReports/listReports.test.ts +1 -1
  34. package/web/listTopIssues/index.ts +1 -1
  35. package/web/manage/index.html +2 -1
  36. package/web/metrics/index.html +19 -0
  37. package/web/metrics/index.ts +126 -0
  38. package/web/metrics/metrics.test.ts +211 -0
  39. package/web/pruneReportsForm/index.html +1 -1
  40. package/web/pruneReportsForm/index.ts +1 -1
  41. package/web/pruneReportsForm/pruneReportsForm.test.ts +18 -10
  42. package/web/reannotate/index.ts +7 -3
  43. package/web/reannotate/reannotate.test.ts +1 -1
  44. package/web/renewWCAG/index.ts +4 -1
  45. package/web/renewWCAG/renewWCAG.test.ts +1 -1
  46. package/web/reportDeletion.ts +12 -7
  47. package/web/requestRetest/index.html +3 -3
  48. package/web/requestRetest/index.ts +47 -16
  49. package/web/requestRetest/requestRetest.test.ts +38 -24
  50. package/web/requestTest/index.html +4 -4
  51. package/web/requestTest/index.ts +37 -9
  52. package/web/requestTest/requestTest.test.ts +21 -6
  53. package/web/rewindReportsForm/index.html +1 -1
  54. package/web/rewindReportsForm/index.ts +1 -1
  55. package/web/rewindReportsForm/rewindReportsForm.test.ts +18 -10
  56. package/web/showHiddenReportsForm/index.html +24 -0
  57. package/web/showHiddenReportsForm/index.ts +45 -0
  58. package/web/showHiddenReportsForm/showHiddenReportsForm.test.ts +103 -0
  59. package/web/tutorialAI/index.ts +1 -1
  60. package/web/tutorialWeb/index.ts +1 -1
  61. package/web/unhideReportForm/index.html +1 -1
  62. package/web/unhideReportForm/index.ts +11 -8
  63. package/web/unhideReportForm/unhideReportForm.test.ts +19 -11
@@ -5,20 +5,48 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- import {getRequestability, processTestRequest} from '../../util.ts';
8
+ import {populateTemplate, processTestRequest} from '../../util.ts';
9
9
 
10
10
  // FUNCTIONS
11
11
 
12
- export const answer = async (description: string, url: string, why: string) => {
13
- const status = await getRequestability(url);
14
- // If the target is already claimed or queued and is thus not requestable:
15
- if (status) {
16
- // Return an answer reporting this.
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: `Page is already ${status}`
33
+ message: 'Test request duplicates an already submitted request'
20
34
  };
21
35
  }
22
- // Otherwise, i.e. if it is requestable, process the request.
23
- return await processTestRequest('test', import.meta.dirname, description, url, why);
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('__target__'));
76
- assert.ok(!result.answerPage.includes('__why__'));
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.ok(result.message.includes('queued'));
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 an invalid request', {timeout: 500}, async () => {
111
+ test('answer returns an error for a duplicate request', {timeout: 500}, async () => {
111
112
  const {answer} = await import('./index.ts');
112
- const result: any = await answer('', 'not-a-url', 'why');
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, 'Invalid request');
129
+ assert.equal(result.message, 'A report about the page is already available');
115
130
  });
@@ -14,7 +14,7 @@
14
14
  <main>
15
15
  <h1><a href="/">Kilotest</a>: Rewind reports</h1>
16
16
  <p>__intro__</p>
17
- <form action="/rewindReportsForm.html">
17
+ <form action="/rewindReportsForm.html" method="post">
18
18
  <fieldset>
19
19
  <legend>Reports</legend>
20
20
  __reports__
@@ -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 when no submission', async () => {
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 returns an error for an invalid auth code', async () => {
47
- const result = await answer(null, 'authCode=wrong&report=260202T0000-new');
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 authorization code');
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
+ });
@@ -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
  };
@@ -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
- export const answer = async (_: any, search: string) => {
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 displayed by itself after a submission and a report is to be unhidden:
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 authorization code'
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 fs.readdir(hiddenReportsPath());
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 when no submission', async () => {
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 returns an error for an invalid auth code', async () => {
76
- const result: any = await answer(null, 'authCode=wrong&report=260101T0007-hid');
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 authorization code');
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'));