@jrpool/kilotest 46.0.7 → 46.0.8
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/.github/workflows/smoke-test.yml +26 -0
- package/apiIntegration.test.js +82 -0
- package/docs/SERVICE.md +25 -1
- package/docs/Smoke Test.json +54 -0
- package/index.js +58 -2
- package/package.json +2 -1
- package/routes.test.js +69 -0
- package/smokeTest.js +123 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Smoke Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
smoke:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set Node.js version
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '>=22.12.0'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Run smoke test against deployed server
|
|
26
|
+
run: node smokeTest.js
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
apiIntegration.test.js
|
|
3
|
+
Minimal HTTP integration tests verifying routing, body parsing, and JSON serialization, using the fixture corpus and a temporary in-process server.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {test, before, after} = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const http = require('node:http');
|
|
12
|
+
const {requestHandler} = require('./index');
|
|
13
|
+
|
|
14
|
+
// CONSTANTS
|
|
15
|
+
|
|
16
|
+
const port = 3998;
|
|
17
|
+
|
|
18
|
+
// SETUP AND TEARDOWN
|
|
19
|
+
|
|
20
|
+
const savedDBDir = process.env.DB_DIR;
|
|
21
|
+
let server;
|
|
22
|
+
|
|
23
|
+
before(async () => {
|
|
24
|
+
process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
25
|
+
server = http.createServer(requestHandler);
|
|
26
|
+
await new Promise(resolve => server.listen(port, resolve));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
after(async () => {
|
|
30
|
+
if (savedDBDir !== undefined) {
|
|
31
|
+
process.env.DB_DIR = savedDBDir;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
delete process.env.DB_DIR;
|
|
35
|
+
}
|
|
36
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// HELPERS
|
|
40
|
+
|
|
41
|
+
// Sends an HTTP request and returns the parsed JSON response body.
|
|
42
|
+
const request = (method, requestPath, body = null) => new Promise((resolve, reject) => {
|
|
43
|
+
const options = {method, host: 'localhost', port, path: requestPath};
|
|
44
|
+
if (body) {
|
|
45
|
+
const bodyJSON = JSON.stringify(body);
|
|
46
|
+
options.headers = {
|
|
47
|
+
'content-type': 'application/json; charset=utf-8',
|
|
48
|
+
'content-length': Buffer.byteLength(bodyJSON)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const req = http.request(options, response => {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
54
|
+
response.on('end', () => {
|
|
55
|
+
const bodyString = chunks.join('');
|
|
56
|
+
try {
|
|
57
|
+
resolve({statusCode: response.statusCode, body: JSON.parse(bodyString)});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
resolve({statusCode: response.statusCode, body: bodyString});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
req.on('error', reject);
|
|
65
|
+
req.end(body ? JSON.stringify(body) : '');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// TESTS
|
|
69
|
+
|
|
70
|
+
test('GET /api/listReports returns valid JSON through the HTTP routing layer', async () => {
|
|
71
|
+
const {body} = await request('GET', '/api/listReports');
|
|
72
|
+
assert.equal(body['tool name'], 'listReports');
|
|
73
|
+
const reports = body['response content']['basics about all available reports'];
|
|
74
|
+
assert.equal(reports.length, 6);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('POST /api/requestFeature parses a JSON body and returns a JSON response through the HTTP routing layer', async () => {
|
|
78
|
+
const {body} = await request('POST', '/api/requestFeature', {feature: 'Add a dark mode toggle'});
|
|
79
|
+
const details = body['response content']['details about your request'];
|
|
80
|
+
assert.equal(details.error, undefined);
|
|
81
|
+
assert.equal(details.disposition, 'received and logged; manager notified');
|
|
82
|
+
});
|
package/docs/SERVICE.md
CHANGED
|
@@ -203,7 +203,7 @@ kilotest.com {
|
|
|
203
203
|
}
|
|
204
204
|
@allowedPOST {
|
|
205
205
|
method POST
|
|
206
|
-
path /mcp /requestRetest.html/* /requestTest.html /recAction.html /reannotate.html /renewWCAG.html /api/* /tutorialComment.html
|
|
206
|
+
path /mcp /requestRetest.html/* /requestTest.html /recAction.html /reannotate.html /renewWCAG.html /api/* /tutorialComment.html /worker/job /worker/report
|
|
207
207
|
}
|
|
208
208
|
# Respond to OPTIONS requests.
|
|
209
209
|
@allowedOPTIONS method OPTIONS
|
|
@@ -259,6 +259,30 @@ When a new version of the `kilotest` package has been published, the service can
|
|
|
259
259
|
1. Update the Playwright browsers: `npx playwright install`
|
|
260
260
|
1. Restart the service: `pm2 restart kilotest`
|
|
261
261
|
|
|
262
|
+
## Branch protection
|
|
263
|
+
|
|
264
|
+
The `main` branch on GitHub is protected to prevent direct pushes and to require a passing smoke test before any pull request can merge. The settings are configured in the GitHub repository as a ruleset under Settings > Branches > Branch protection rules.
|
|
265
|
+
|
|
266
|
+
The required settings are:
|
|
267
|
+
|
|
268
|
+
1. Require a pull request before merging.
|
|
269
|
+
1. Require status checks to pass before merging.
|
|
270
|
+
1. Add "Smoke Test" as a required status check.
|
|
271
|
+
1. Require branches to be up to date before merging.
|
|
272
|
+
1. Do not allow bypassing the above settings.
|
|
273
|
+
|
|
274
|
+
A copy of the ruleset is located at `docs/Smoke Test.json`.
|
|
275
|
+
|
|
276
|
+
The smoke-test check is defined in `.github/workflows/smoke-test.yml` and runs `smokeTest.js`, which sends HTTPS requests to all valid GET and POST paths on the deployed server and verifies that Caddy forwards each one to Kilotest rather than returning a bare 404.
|
|
277
|
+
|
|
278
|
+
When a pull request adds a new route to `index.js`, the Caddyfile at `/etc/caddy/Caddyfile` on the server must be updated to forward the new path before the pull request can merge. The workflow to follow is:
|
|
279
|
+
|
|
280
|
+
1. Add the new route to the `routes` constant and the dispatch chain in `index.js`.
|
|
281
|
+
1. Update the Caddyfile (`/etc/caddy/Caddyfile`) on the server to forward the new path.
|
|
282
|
+
1. Open or update the pull request.
|
|
283
|
+
1. The smoke test workflow runs and verifies that all paths, including the new one, are forwarded by Caddy.
|
|
284
|
+
1. If the Caddyfile was not updated, the smoke test reports a bare 404 for the new path, the required status check fails, and the pull request cannot merge.
|
|
285
|
+
|
|
262
286
|
## Performance
|
|
263
287
|
|
|
264
288
|
The Cloud Compute host, in initial testing, took about 2.5 as long to process an example job as an Apple M2 Pro MacBook Pro with 16GB of memory. After tuning, the ratio was reduced to about 1.7.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": 22391521,
|
|
3
|
+
"name": "Smoke Test",
|
|
4
|
+
"target": "branch",
|
|
5
|
+
"source_type": "Repository",
|
|
6
|
+
"source": "jrpool/kilotest",
|
|
7
|
+
"enforcement": "active",
|
|
8
|
+
"conditions": {
|
|
9
|
+
"ref_name": {
|
|
10
|
+
"exclude": [],
|
|
11
|
+
"include": [
|
|
12
|
+
"~DEFAULT_BRANCH"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"rules": [
|
|
17
|
+
{
|
|
18
|
+
"type": "deletion"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "non_fast_forward"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "pull_request",
|
|
25
|
+
"parameters": {
|
|
26
|
+
"required_approving_review_count": 0,
|
|
27
|
+
"dismiss_stale_reviews_on_push": true,
|
|
28
|
+
"required_reviewers": [],
|
|
29
|
+
"require_code_owner_review": false,
|
|
30
|
+
"require_last_push_approval": false,
|
|
31
|
+
"required_review_thread_resolution": false,
|
|
32
|
+
"require_extra_approval_for_unattributed_changes": true,
|
|
33
|
+
"allowed_merge_methods": [
|
|
34
|
+
"merge",
|
|
35
|
+
"squash",
|
|
36
|
+
"rebase"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "required_status_checks",
|
|
42
|
+
"parameters": {
|
|
43
|
+
"strict_required_status_checks_policy": true,
|
|
44
|
+
"do_not_enforce_on_create": false,
|
|
45
|
+
"required_status_checks": [
|
|
46
|
+
{
|
|
47
|
+
"context": "Smoke Test"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"bypass_actors": []
|
|
54
|
+
}
|
package/index.js
CHANGED
|
@@ -63,6 +63,54 @@ const answer = {
|
|
|
63
63
|
|
|
64
64
|
// CONSTANTS
|
|
65
65
|
|
|
66
|
+
// Paths that the application is authorized to handle, by method, as glob-style patterns where * matches any sequence of characters.
|
|
67
|
+
const routes = exports.routes = {
|
|
68
|
+
GET: [
|
|
69
|
+
'/mcp',
|
|
70
|
+
'/',
|
|
71
|
+
'/index.html',
|
|
72
|
+
'/robots.txt',
|
|
73
|
+
'/openapi.yaml',
|
|
74
|
+
'/openapi.json',
|
|
75
|
+
'/swagger.yaml',
|
|
76
|
+
'/swagger.json',
|
|
77
|
+
'/api-docs',
|
|
78
|
+
'/llms.txt',
|
|
79
|
+
'/llms-full.txt',
|
|
80
|
+
'/sitemap.xml',
|
|
81
|
+
'/style.css',
|
|
82
|
+
'/fullReport.json/*',
|
|
83
|
+
'/api/*',
|
|
84
|
+
'/tutorial/images/*',
|
|
85
|
+
'/favicon.*',
|
|
86
|
+
'*.html*'
|
|
87
|
+
],
|
|
88
|
+
POST: [
|
|
89
|
+
'/mcp',
|
|
90
|
+
'/requestTest.html',
|
|
91
|
+
'/requestRetest.html/*',
|
|
92
|
+
'/recAction.html',
|
|
93
|
+
'/reannotate.html',
|
|
94
|
+
'/renewWCAG.html',
|
|
95
|
+
'/worker/job',
|
|
96
|
+
'/worker/report',
|
|
97
|
+
'/api/*',
|
|
98
|
+
'/tutorialComment.html'
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
// Returns whether a pathname matches a glob-style pattern.
|
|
102
|
+
const matchPath = (pattern, pathname) => {
|
|
103
|
+
const regex = new RegExp(
|
|
104
|
+
'^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$'
|
|
105
|
+
);
|
|
106
|
+
return regex.test(pathname);
|
|
107
|
+
};
|
|
108
|
+
// Returns whether a pathname is authorized for a method.
|
|
109
|
+
const isPathAllowed = (method, pathname) => {
|
|
110
|
+
const patterns = routes[method] || [];
|
|
111
|
+
return patterns.some(pattern => matchPath(pattern, pathname));
|
|
112
|
+
};
|
|
113
|
+
|
|
66
114
|
const protocol = process.env.PROTOCOL || 'http';
|
|
67
115
|
const queuePath = () => path.join(jobsPath(), 'queue');
|
|
68
116
|
const claimedPath = () => path.join(jobsPath(), 'claimed');
|
|
@@ -321,8 +369,12 @@ const requestHandler = async (request, response) => {
|
|
|
321
369
|
const pathTail = pathname.split('/').slice(2).join('/');
|
|
322
370
|
// If the request is a GET request:
|
|
323
371
|
if (method === 'GET') {
|
|
372
|
+
// If the path is not authorized for GET requests:
|
|
373
|
+
if (!isPathAllowed('GET', pathname)) {
|
|
374
|
+
await serveError({message: `ERROR: Invalid GET request (${pathname})`}, response, true);
|
|
375
|
+
}
|
|
324
376
|
// If it is for the model context protocol server:
|
|
325
|
-
if (pathname === mcpPath) {
|
|
377
|
+
else if (pathname === mcpPath) {
|
|
326
378
|
// Handle the MCP request.
|
|
327
379
|
await handleMCP(request, response);
|
|
328
380
|
}
|
|
@@ -554,8 +606,12 @@ const requestHandler = async (request, response) => {
|
|
|
554
606
|
}
|
|
555
607
|
// Otherwise, if the request is a POST request:
|
|
556
608
|
else if (method === 'POST') {
|
|
609
|
+
// If the path is not authorized for POST requests:
|
|
610
|
+
if (!isPathAllowed('POST', pathname)) {
|
|
611
|
+
await serveError({message: 'ERROR: Invalid POST request'}, response, true);
|
|
612
|
+
}
|
|
557
613
|
// If it is for the model context protocol server:
|
|
558
|
-
if (pageName === 'mcp') {
|
|
614
|
+
else if (pageName === 'mcp') {
|
|
559
615
|
await handleMCP(request, response);
|
|
560
616
|
}
|
|
561
617
|
// Otherwise, i.e. if it is not for the MCP server:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jrpool/kilotest",
|
|
3
|
-
"version": "46.0.
|
|
3
|
+
"version": "46.0.8",
|
|
4
4
|
"description": "Ensemble testing for front-end web quality",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"start": "node index",
|
|
11
11
|
"lint": "npx eslint && npx markdownlint-cli2",
|
|
12
12
|
"test": "c8 node --test",
|
|
13
|
+
"test:smoke": "node smokeTest",
|
|
13
14
|
"generate:openapi": "node scripts/generate-openapi"
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|
package/routes.test.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
routes.test.js
|
|
3
|
+
Tests for the centralized routes table exported from index.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {test} = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const {routes} = require('./index');
|
|
11
|
+
|
|
12
|
+
// TESTS
|
|
13
|
+
|
|
14
|
+
test('routes has GET and POST arrays', () => {
|
|
15
|
+
assert.ok(Array.isArray(routes.GET));
|
|
16
|
+
assert.ok(Array.isArray(routes.POST));
|
|
17
|
+
assert.ok(routes.GET.length > 0);
|
|
18
|
+
assert.ok(routes.POST.length > 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('routes GET includes all known GET paths', () => {
|
|
22
|
+
const required = [
|
|
23
|
+
'/mcp',
|
|
24
|
+
'/',
|
|
25
|
+
'/index.html',
|
|
26
|
+
'/robots.txt',
|
|
27
|
+
'/openapi.yaml',
|
|
28
|
+
'/openapi.json',
|
|
29
|
+
'/swagger.yaml',
|
|
30
|
+
'/swagger.json',
|
|
31
|
+
'/api-docs',
|
|
32
|
+
'/llms.txt',
|
|
33
|
+
'/llms-full.txt',
|
|
34
|
+
'/sitemap.xml',
|
|
35
|
+
'/style.css'
|
|
36
|
+
];
|
|
37
|
+
for (const path of required) {
|
|
38
|
+
assert.ok(routes.GET.includes(path), `routes.GET missing ${path}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('routes POST includes all known POST paths', () => {
|
|
43
|
+
const required = [
|
|
44
|
+
'/mcp',
|
|
45
|
+
'/requestTest.html',
|
|
46
|
+
'/recAction.html',
|
|
47
|
+
'/reannotate.html',
|
|
48
|
+
'/renewWCAG.html',
|
|
49
|
+
'/worker/job',
|
|
50
|
+
'/worker/report',
|
|
51
|
+
'/tutorialComment.html'
|
|
52
|
+
];
|
|
53
|
+
for (const path of required) {
|
|
54
|
+
assert.ok(routes.POST.includes(path), `routes.POST missing ${path}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('routes GET includes wildcard patterns for dynamic paths', () => {
|
|
59
|
+
assert.ok(routes.GET.some(p => p.includes('fullReport.json')));
|
|
60
|
+
assert.ok(routes.GET.some(p => p.includes('/api/')));
|
|
61
|
+
assert.ok(routes.GET.some(p => p.includes('tutorial/images')));
|
|
62
|
+
assert.ok(routes.GET.some(p => p.includes('favicon')));
|
|
63
|
+
assert.ok(routes.GET.some(p => p.includes('.html')));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('routes POST includes wildcard patterns for dynamic paths', () => {
|
|
67
|
+
assert.ok(routes.POST.some(p => p.includes('requestRetest.html')));
|
|
68
|
+
assert.ok(routes.POST.some(p => p.includes('/api/')));
|
|
69
|
+
});
|
package/smokeTest.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
smokeTest.js
|
|
3
|
+
Sends requests to all valid GET and POST paths on the deployed server and verifies that Caddy forwards them to Kilotest (i.e., the response is not a bare Caddy 404).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const {routes} = require('./index');
|
|
10
|
+
|
|
11
|
+
// CONSTANTS
|
|
12
|
+
|
|
13
|
+
const host = process.env.SMOKE_HOST || 'kilotest.com';
|
|
14
|
+
// Concrete paths matching each wildcard pattern in the routes table, for smoke testing.
|
|
15
|
+
const concretePaths = {
|
|
16
|
+
GET: {
|
|
17
|
+
'/mcp': '/mcp',
|
|
18
|
+
'/': '/',
|
|
19
|
+
'/index.html': '/index.html',
|
|
20
|
+
'/robots.txt': '/robots.txt',
|
|
21
|
+
'/openapi.yaml': '/openapi.yaml',
|
|
22
|
+
'/openapi.json': '/openapi.json',
|
|
23
|
+
'/swagger.yaml': '/swagger.yaml',
|
|
24
|
+
'/swagger.json': '/swagger.json',
|
|
25
|
+
'/api-docs': '/api-docs',
|
|
26
|
+
'/llms.txt': '/llms.txt',
|
|
27
|
+
'/llms-full.txt': '/llms-full.txt',
|
|
28
|
+
'/sitemap.xml': '/sitemap.xml',
|
|
29
|
+
'/style.css': '/style.css',
|
|
30
|
+
'/fullReport.json/*': '/fullReport.json/260101T0000/mix',
|
|
31
|
+
'/api/*': '/api/listReports',
|
|
32
|
+
'/tutorial/images/*': '/tutorial/images/diagram.png',
|
|
33
|
+
'/favicon.*': '/favicon.ico',
|
|
34
|
+
'*.html*': '/listReports.html'
|
|
35
|
+
},
|
|
36
|
+
POST: {
|
|
37
|
+
'/mcp': '/mcp',
|
|
38
|
+
'/requestTest.html': '/requestTest.html',
|
|
39
|
+
'/requestRetest.html/*': '/requestRetest.html/260101T0001/ct',
|
|
40
|
+
'/recAction.html': '/recAction.html',
|
|
41
|
+
'/reannotate.html': '/reannotate.html',
|
|
42
|
+
'/renewWCAG.html': '/renewWCAG.html',
|
|
43
|
+
'/worker/job': '/worker/job',
|
|
44
|
+
'/worker/report': '/worker/report',
|
|
45
|
+
'/api/*': '/api/requestFeature',
|
|
46
|
+
'/tutorialComment.html': '/tutorialComment.html'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// Minimal POST bodies for paths that require them.
|
|
50
|
+
const postBodies = {
|
|
51
|
+
'/api/requestFeature': {feature: 'smoke test'},
|
|
52
|
+
'/requestTest.html': {what: 'Smoke Test Page', url: 'https://smoketest.example.com', why: 'smoke test'},
|
|
53
|
+
'/requestRetest.html/260101T0001/ct': {why: 'smoke test'},
|
|
54
|
+
'/recAction.html': {target: 'https://smoketest.example.com\tSmoke Test Page', authCode: 'invalid'},
|
|
55
|
+
'/reannotate.html': {authCode: 'invalid'},
|
|
56
|
+
'/renewWCAG.html': {authCode: 'invalid'},
|
|
57
|
+
'/worker/job': {},
|
|
58
|
+
'/worker/report': {},
|
|
59
|
+
'/tutorialComment.html': {content: 'smoke test'},
|
|
60
|
+
'/mcp': {}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// FUNCTIONS
|
|
64
|
+
|
|
65
|
+
// Sends an HTTPS request and returns the status code and body length.
|
|
66
|
+
const sendRequest = (method, requestPath) => new Promise((resolve, reject) => {
|
|
67
|
+
const body = method === 'POST' ? JSON.stringify(postBodies[requestPath] || {}) : null;
|
|
68
|
+
const options = {
|
|
69
|
+
method,
|
|
70
|
+
host,
|
|
71
|
+
path: requestPath,
|
|
72
|
+
headers: body ? {
|
|
73
|
+
'content-type': 'application/json; charset=utf-8',
|
|
74
|
+
'content-length': Buffer.byteLength(body)
|
|
75
|
+
} : {}
|
|
76
|
+
};
|
|
77
|
+
const req = https.request(options, response => {
|
|
78
|
+
const chunks = [];
|
|
79
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
80
|
+
response.on('end', () => {
|
|
81
|
+
const responseBody = chunks.join('');
|
|
82
|
+
resolve({statusCode: response.statusCode, bodyLength: responseBody.length});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
req.end(body || '');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Returns whether a response is a bare Caddy 404 (the failure this test detects).
|
|
90
|
+
const isCaddy404 = result => result.statusCode === 404 && result.bodyLength === 0;
|
|
91
|
+
|
|
92
|
+
// EXECUTION
|
|
93
|
+
|
|
94
|
+
(async () => {
|
|
95
|
+
let failures = 0;
|
|
96
|
+
for (const method of ['GET', 'POST']) {
|
|
97
|
+
console.log(`\n=== ${method} paths ===`);
|
|
98
|
+
for (const pattern of routes[method]) {
|
|
99
|
+
const requestPath = concretePaths[method][pattern];
|
|
100
|
+
if (!requestPath) {
|
|
101
|
+
console.log(`FAIL: no concrete path defined for pattern ${pattern}`);
|
|
102
|
+
failures++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const result = await sendRequest(method, requestPath);
|
|
107
|
+
if (isCaddy404(result)) {
|
|
108
|
+
console.log(`FAIL: ${method} ${requestPath} -> Caddy 404 (path not forwarded)`);
|
|
109
|
+
failures++;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`PASS: ${method} ${requestPath} -> ${result.statusCode} (body ${result.bodyLength} bytes)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.log(`FAIL: ${method} ${requestPath} -> error: ${error.message}`);
|
|
117
|
+
failures++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`\n=== ${failures === 0 ? 'All paths forwarded' : `${failures} failure(s)`} ===`);
|
|
122
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
123
|
+
})();
|