jsgui3-server 0.0.150 → 0.0.152
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/instructions/copilot.instructions.md +1 -0
- package/AGENTS.md +2 -0
- package/README.md +89 -13
- package/admin-ui/v1/controls/admin_shell.js +702 -669
- package/admin-ui/v1/server.js +14 -1
- package/docs/api-reference.md +504 -306
- package/docs/books/creating-a-new-admin-ui/README.md +20 -20
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/comprehensive-documentation.md +220 -220
- package/docs/configuration-reference.md +281 -204
- package/docs/middleware-guide.md +236 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/docs/system-architecture.md +24 -18
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/compression.js +217 -0
- package/middleware/index.js +15 -0
- package/middleware/json-body.js +126 -0
- package/module.js +3 -0
- package/openapi.js +474 -0
- package/package.json +11 -8
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +756 -18
- package/server.js +502 -123
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +1 -0
package/tests/serve.test.js
CHANGED
|
@@ -6,14 +6,14 @@ const EventEmitter = require('events');
|
|
|
6
6
|
|
|
7
7
|
const dummy_client_path = require.resolve('./dummy-client.js');
|
|
8
8
|
|
|
9
|
-
const fake_webpage_publisher_path = require.resolve('../publishers/http-webpage-publisher');
|
|
10
|
-
const fake_website_publisher_path = require.resolve('../publishers/http-website-publisher');
|
|
11
|
-
const original_webpage_publisher_module = require.cache[fake_webpage_publisher_path];
|
|
12
|
-
const original_website_publisher_module = require.cache[fake_website_publisher_path];
|
|
13
|
-
|
|
14
|
-
class Fake_Publisher_Base extends EventEmitter {
|
|
15
|
-
constructor(html_route, html_body) {
|
|
16
|
-
super();
|
|
9
|
+
const fake_webpage_publisher_path = require.resolve('../publishers/http-webpage-publisher');
|
|
10
|
+
const fake_website_publisher_path = require.resolve('../publishers/http-website-publisher');
|
|
11
|
+
const original_webpage_publisher_module = require.cache[fake_webpage_publisher_path];
|
|
12
|
+
const original_website_publisher_module = require.cache[fake_website_publisher_path];
|
|
13
|
+
|
|
14
|
+
class Fake_Publisher_Base extends EventEmitter {
|
|
15
|
+
constructor(html_route, html_body) {
|
|
16
|
+
super();
|
|
17
17
|
this.html_route = html_route;
|
|
18
18
|
this.html_body = html_body;
|
|
19
19
|
const buffer = Buffer.from(this.html_body, 'utf8');
|
|
@@ -42,48 +42,48 @@ class Fake_Publisher_Base extends EventEmitter {
|
|
|
42
42
|
};
|
|
43
43
|
this.type = 'html';
|
|
44
44
|
this.extension = 'html';
|
|
45
|
-
const ready_delay_ms = Number(this.constructor.ready_delay_ms) || 0;
|
|
46
|
-
const emit_ready = () => {
|
|
47
|
-
this.emit('ready', {
|
|
48
|
-
_arr: [{
|
|
49
|
-
type: this.type,
|
|
50
|
-
extension: this.extension,
|
|
51
|
-
route: this.route,
|
|
52
|
-
response_headers: this.response_headers,
|
|
53
|
-
response_buffers: this.response_buffers
|
|
54
|
-
}]
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (ready_delay_ms > 0) {
|
|
59
|
-
setTimeout(emit_ready, ready_delay_ms);
|
|
60
|
-
} else {
|
|
61
|
-
setImmediate(emit_ready);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
handle_http(req, res) {
|
|
66
|
-
res.writeHead(200, {
|
|
67
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
68
|
-
'Content-Length': Buffer.byteLength(this.html_body, 'utf8')
|
|
69
|
-
});
|
|
70
|
-
res.end(this.html_body);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
meets_requirements() {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
start(callback) {
|
|
78
|
-
if (typeof callback === 'function') callback(null, true);
|
|
79
|
-
return Promise.resolve(true);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
stop(callback) {
|
|
83
|
-
if (typeof callback === 'function') callback(null, true);
|
|
84
|
-
return Promise.resolve(true);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
45
|
+
const ready_delay_ms = Number(this.constructor.ready_delay_ms) || 0;
|
|
46
|
+
const emit_ready = () => {
|
|
47
|
+
this.emit('ready', {
|
|
48
|
+
_arr: [{
|
|
49
|
+
type: this.type,
|
|
50
|
+
extension: this.extension,
|
|
51
|
+
route: this.route,
|
|
52
|
+
response_headers: this.response_headers,
|
|
53
|
+
response_buffers: this.response_buffers
|
|
54
|
+
}]
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (ready_delay_ms > 0) {
|
|
59
|
+
setTimeout(emit_ready, ready_delay_ms);
|
|
60
|
+
} else {
|
|
61
|
+
setImmediate(emit_ready);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handle_http(req, res) {
|
|
66
|
+
res.writeHead(200, {
|
|
67
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
68
|
+
'Content-Length': Buffer.byteLength(this.html_body, 'utf8')
|
|
69
|
+
});
|
|
70
|
+
res.end(this.html_body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
meets_requirements() {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
start(callback) {
|
|
78
|
+
if (typeof callback === 'function') callback(null, true);
|
|
79
|
+
return Promise.resolve(true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stop(callback) {
|
|
83
|
+
if (typeof callback === 'function') callback(null, true);
|
|
84
|
+
return Promise.resolve(true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
87
|
|
|
88
88
|
class Fake_Webpage_Publisher extends Fake_Publisher_Base {
|
|
89
89
|
constructor(opts = {}) {
|
|
@@ -95,17 +95,17 @@ class Fake_Webpage_Publisher extends Fake_Publisher_Base {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
class Fake_Website_Publisher extends Fake_Publisher_Base {
|
|
98
|
+
class Fake_Website_Publisher extends Fake_Publisher_Base {
|
|
99
99
|
constructor(opts = {}) {
|
|
100
100
|
const route = '/*';
|
|
101
101
|
const title = (opts.website && opts.website.name) || 'Test Website';
|
|
102
102
|
const body = `<html><head><title>${title}</title></head><body><div class="dummy-control">website</div></body></html>`;
|
|
103
103
|
super(route, body);
|
|
104
104
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
Fake_Webpage_Publisher.ready_delay_ms = 0;
|
|
108
|
-
Fake_Website_Publisher.ready_delay_ms = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Fake_Webpage_Publisher.ready_delay_ms = 0;
|
|
108
|
+
Fake_Website_Publisher.ready_delay_ms = 0;
|
|
109
109
|
|
|
110
110
|
require.cache[fake_webpage_publisher_path] = { exports: Fake_Webpage_Publisher };
|
|
111
111
|
require.cache[fake_website_publisher_path] = { exports: Fake_Website_Publisher };
|
|
@@ -146,35 +146,68 @@ const get_http_response = (port, route_path = '/') => new Promise((resolve, reje
|
|
|
146
146
|
req.on('error', reject);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
describe('Server.serve', function() {
|
|
150
|
-
this.timeout(10000);
|
|
151
|
-
let server_instance;
|
|
152
|
-
|
|
153
|
-
after(() => {
|
|
154
|
-
if (original_webpage_publisher_module) {
|
|
155
|
-
require.cache[fake_webpage_publisher_path] = original_webpage_publisher_module;
|
|
156
|
-
} else {
|
|
157
|
-
delete require.cache[fake_webpage_publisher_path];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (original_website_publisher_module) {
|
|
161
|
-
require.cache[fake_website_publisher_path] = original_website_publisher_module;
|
|
162
|
-
} else {
|
|
163
|
-
delete require.cache[fake_website_publisher_path];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
delete require.cache[require.resolve('../server')];
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
afterEach(async () => {
|
|
170
|
-
Fake_Webpage_Publisher.ready_delay_ms = 0;
|
|
171
|
-
Fake_Website_Publisher.ready_delay_ms = 0;
|
|
172
|
-
|
|
173
|
-
if (server_instance) {
|
|
174
|
-
await new Promise(resolve => server_instance.close(resolve));
|
|
175
|
-
server_instance = null;
|
|
176
|
-
}
|
|
177
|
-
});
|
|
149
|
+
describe('Server.serve', function () {
|
|
150
|
+
this.timeout(10000);
|
|
151
|
+
let server_instance;
|
|
152
|
+
|
|
153
|
+
after(() => {
|
|
154
|
+
if (original_webpage_publisher_module) {
|
|
155
|
+
require.cache[fake_webpage_publisher_path] = original_webpage_publisher_module;
|
|
156
|
+
} else {
|
|
157
|
+
delete require.cache[fake_webpage_publisher_path];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (original_website_publisher_module) {
|
|
161
|
+
require.cache[fake_website_publisher_path] = original_website_publisher_module;
|
|
162
|
+
} else {
|
|
163
|
+
delete require.cache[fake_website_publisher_path];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
delete require.cache[require.resolve('../server')];
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
afterEach(async () => {
|
|
170
|
+
Fake_Webpage_Publisher.ready_delay_ms = 0;
|
|
171
|
+
Fake_Website_Publisher.ready_delay_ms = 0;
|
|
172
|
+
|
|
173
|
+
if (server_instance) {
|
|
174
|
+
await new Promise(resolve => server_instance.close(resolve));
|
|
175
|
+
server_instance = null;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('helper methods are safe before start and after close', async () => {
|
|
180
|
+
const manual_server = new Server({ website: false });
|
|
181
|
+
|
|
182
|
+
assert.deepStrictEqual(manual_server.get_listening_endpoints(), []);
|
|
183
|
+
assert.strictEqual(manual_server.get_primary_endpoint(), null);
|
|
184
|
+
assert.deepStrictEqual(
|
|
185
|
+
manual_server.print_endpoints({ logger: () => { } }),
|
|
186
|
+
[]
|
|
187
|
+
);
|
|
188
|
+
assert.strictEqual(manual_server.get_startup_diagnostics(), null);
|
|
189
|
+
|
|
190
|
+
const port = await get_free_port();
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
manual_server.on('ready', () => {
|
|
193
|
+
manual_server.start(port, (err) => {
|
|
194
|
+
if (err) reject(err);
|
|
195
|
+
else resolve();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
manual_server.raise('ready');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const startup_diagnostics = manual_server.get_startup_diagnostics();
|
|
202
|
+
assert(startup_diagnostics);
|
|
203
|
+
assert.strictEqual(startup_diagnostics.requested_port, port);
|
|
204
|
+
|
|
205
|
+
await new Promise((resolve) => manual_server.close(resolve));
|
|
206
|
+
|
|
207
|
+
assert.deepStrictEqual(manual_server.get_listening_endpoints(), []);
|
|
208
|
+
assert.strictEqual(manual_server.get_primary_endpoint(), null);
|
|
209
|
+
assert.strictEqual(manual_server.get_startup_diagnostics(), null);
|
|
210
|
+
});
|
|
178
211
|
|
|
179
212
|
it('should serve a simple control', async () => {
|
|
180
213
|
const port = await get_free_port();
|
|
@@ -184,6 +217,23 @@ describe('Server.serve', function() {
|
|
|
184
217
|
host: '127.0.0.1',
|
|
185
218
|
port
|
|
186
219
|
});
|
|
220
|
+
const endpoints = server_instance.get_listening_endpoints();
|
|
221
|
+
assert(Array.isArray(endpoints));
|
|
222
|
+
assert(endpoints.length >= 1);
|
|
223
|
+
assert.strictEqual(endpoints[0].port, port);
|
|
224
|
+
assert.strictEqual(endpoints[0].protocol, 'http');
|
|
225
|
+
assert.strictEqual(server_instance.get_primary_endpoint(), endpoints[0].url);
|
|
226
|
+
|
|
227
|
+
const printed_lines = [];
|
|
228
|
+
const returned_lines = server_instance.print_endpoints({
|
|
229
|
+
logger: (line) => printed_lines.push(line),
|
|
230
|
+
include_index: true
|
|
231
|
+
});
|
|
232
|
+
assert(Array.isArray(returned_lines));
|
|
233
|
+
assert(returned_lines.length >= 1);
|
|
234
|
+
assert.strictEqual(printed_lines[0], returned_lines[0]);
|
|
235
|
+
assert(printed_lines[0].includes(endpoints[0].url));
|
|
236
|
+
|
|
187
237
|
const { res, body } = await get_http_response(port);
|
|
188
238
|
assert.strictEqual(res.statusCode, 200);
|
|
189
239
|
assert(body.includes('<div class="dummy-control"'));
|
|
@@ -240,41 +290,294 @@ describe('Server.serve', function() {
|
|
|
240
290
|
assert.strictEqual(body, 'ok');
|
|
241
291
|
});
|
|
242
292
|
|
|
243
|
-
it('should return 404 for unknown routes', async () => {
|
|
244
|
-
const port = await get_free_port();
|
|
245
|
-
server_instance = await Server.serve({
|
|
246
|
-
Ctrl: Dummy_Control,
|
|
247
|
-
src_path_client_js: dummy_client_path,
|
|
248
|
-
host: '127.0.0.1',
|
|
249
|
-
port
|
|
250
|
-
});
|
|
251
|
-
const { res, body } = await get_http_response(port, '/missing');
|
|
252
|
-
assert.strictEqual(res.statusCode, 404);
|
|
253
|
-
assert.strictEqual(body, 'Not Found');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
293
|
+
it('should return 404 for unknown routes', async () => {
|
|
294
|
+
const port = await get_free_port();
|
|
295
|
+
server_instance = await Server.serve({
|
|
296
|
+
Ctrl: Dummy_Control,
|
|
297
|
+
src_path_client_js: dummy_client_path,
|
|
298
|
+
host: '127.0.0.1',
|
|
299
|
+
port
|
|
300
|
+
});
|
|
301
|
+
const { res, body } = await get_http_response(port, '/missing');
|
|
302
|
+
assert.strictEqual(res.statusCode, 404);
|
|
303
|
+
assert.strictEqual(body, 'Not Found');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('falls back to auto-loopback port on conflict when configured', async () => {
|
|
307
|
+
const blocked_port = await get_free_port();
|
|
308
|
+
const blocker = net.createServer();
|
|
309
|
+
|
|
310
|
+
await new Promise((resolve, reject) => {
|
|
311
|
+
blocker.listen(blocked_port, '127.0.0.1', resolve);
|
|
312
|
+
blocker.on('error', reject);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
server_instance = await Server.serve({
|
|
317
|
+
Ctrl: Dummy_Control,
|
|
318
|
+
src_path_client_js: dummy_client_path,
|
|
319
|
+
host: '127.0.0.1',
|
|
320
|
+
port: blocked_port,
|
|
321
|
+
on_port_conflict: 'auto-loopback'
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
assert.notStrictEqual(
|
|
325
|
+
server_instance.port,
|
|
326
|
+
blocked_port,
|
|
327
|
+
'Expected fallback to choose a different free port'
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const { res, body } = await get_http_response(server_instance.port, '/');
|
|
331
|
+
assert.strictEqual(res.statusCode, 200);
|
|
332
|
+
assert(body.includes('<div class="dummy-control"'));
|
|
333
|
+
|
|
334
|
+
assert(server_instance.startup_diagnostics);
|
|
335
|
+
assert.strictEqual(server_instance.startup_diagnostics.requested_port, blocked_port);
|
|
336
|
+
assert.strictEqual(server_instance.startup_diagnostics.fallback_host, '127.0.0.1');
|
|
337
|
+
assert(server_instance.startup_diagnostics.fallback_port > 0);
|
|
338
|
+
const diagnostics = server_instance.get_startup_diagnostics();
|
|
339
|
+
assert(diagnostics);
|
|
340
|
+
assert.strictEqual(diagnostics.fallback_host, '127.0.0.1');
|
|
341
|
+
assert.strictEqual(
|
|
342
|
+
server_instance.get_primary_endpoint(),
|
|
343
|
+
`http://127.0.0.1:${server_instance.startup_diagnostics.fallback_port}/`
|
|
344
|
+
);
|
|
345
|
+
} finally {
|
|
346
|
+
await new Promise(resolve => blocker.close(resolve));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('waits for webpage publisher readiness before resolving serve()', async () => {
|
|
351
|
+
Fake_Webpage_Publisher.ready_delay_ms = 2600;
|
|
352
|
+
|
|
353
|
+
const port = await get_free_port();
|
|
354
|
+
const started_at = Date.now();
|
|
355
|
+
|
|
356
|
+
server_instance = await Server.serve({
|
|
357
|
+
Ctrl: Dummy_Control,
|
|
358
|
+
src_path_client_js: dummy_client_path,
|
|
359
|
+
host: '127.0.0.1',
|
|
360
|
+
port,
|
|
361
|
+
readyTimeoutMs: 12000
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const elapsed_ms = Date.now() - started_at;
|
|
365
|
+
assert(
|
|
366
|
+
elapsed_ms >= 2400,
|
|
367
|
+
`Expected serve() to wait for delayed readiness, elapsed=${elapsed_ms}ms`
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const { res, body } = await get_http_response(port, '/');
|
|
371
|
+
assert.strictEqual(res.statusCode, 200);
|
|
372
|
+
assert(body.includes('<div class="dummy-control"'));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('serves a webpage-like input object at its declared path without forcing root', async () => {
|
|
376
|
+
const port = await get_free_port();
|
|
377
|
+
const webpage_like = {
|
|
378
|
+
[Symbol.for('jsgui3.webpage')]: true,
|
|
379
|
+
name: 'About Page',
|
|
380
|
+
path: '/about',
|
|
381
|
+
ctrl: Dummy_Control
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
server_instance = await Server.serve({
|
|
385
|
+
...webpage_like,
|
|
386
|
+
src_path_client_js: dummy_client_path,
|
|
387
|
+
host: '127.0.0.1',
|
|
388
|
+
port
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const about_response = await get_http_response(port, '/about');
|
|
392
|
+
assert.strictEqual(about_response.res.statusCode, 200);
|
|
393
|
+
assert(about_response.body.includes('dummy-control">/about</div>'));
|
|
394
|
+
|
|
395
|
+
assert(server_instance.website_manifest);
|
|
396
|
+
assert.strictEqual(server_instance.website_manifest.source, 'webpage');
|
|
397
|
+
assert.deepStrictEqual(
|
|
398
|
+
server_instance.publication_summary.page_routes,
|
|
399
|
+
['/about']
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('serves website-like inputs with base_path pages and endpoint metadata', async () => {
|
|
404
|
+
const port = await get_free_port();
|
|
405
|
+
const website_like = {
|
|
406
|
+
[Symbol.for('jsgui3.website')]: true,
|
|
407
|
+
name: 'Docs Site',
|
|
408
|
+
base_path: '/docs',
|
|
409
|
+
pages: [
|
|
410
|
+
{ path: '/', content: Dummy_Control, title: 'Docs Home' },
|
|
411
|
+
{ path: '/guide', content: Dummy_Control, title: 'Guide' }
|
|
412
|
+
],
|
|
413
|
+
api_endpoints: [
|
|
414
|
+
{
|
|
415
|
+
name: 'status',
|
|
416
|
+
method: 'POST',
|
|
417
|
+
handler: () => 'ok'
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: 'health',
|
|
421
|
+
method: 'GET',
|
|
422
|
+
path: '/healthz',
|
|
423
|
+
handler: () => 'up'
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
server_instance = await Server.serve({
|
|
429
|
+
...website_like,
|
|
430
|
+
src_path_client_js: dummy_client_path,
|
|
431
|
+
host: '127.0.0.1',
|
|
432
|
+
port
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const docs_root_response = await get_http_response(port, '/docs');
|
|
436
|
+
assert.strictEqual(docs_root_response.res.statusCode, 200);
|
|
437
|
+
assert(docs_root_response.body.includes('dummy-control">/docs</div>'));
|
|
438
|
+
|
|
439
|
+
const docs_guide_response = await get_http_response(port, '/docs/guide');
|
|
440
|
+
assert.strictEqual(docs_guide_response.res.statusCode, 200);
|
|
441
|
+
assert(docs_guide_response.body.includes('dummy-control">/docs/guide</div>'));
|
|
442
|
+
|
|
443
|
+
const api_status_get_response = await get_http_response(port, '/docs/api/status');
|
|
444
|
+
assert.strictEqual(api_status_get_response.res.statusCode, 405);
|
|
445
|
+
|
|
446
|
+
const api_status_post_response = await new Promise((resolve) => {
|
|
447
|
+
const req = http.request({
|
|
448
|
+
hostname: '127.0.0.1',
|
|
449
|
+
port,
|
|
450
|
+
path: '/docs/api/status',
|
|
451
|
+
method: 'POST'
|
|
452
|
+
}, res => {
|
|
453
|
+
let body = '';
|
|
454
|
+
res.setEncoding('utf8');
|
|
455
|
+
res.on('data', chunk => body += chunk);
|
|
456
|
+
res.on('end', () => resolve({ res, body }));
|
|
457
|
+
});
|
|
458
|
+
req.end();
|
|
459
|
+
});
|
|
460
|
+
assert.strictEqual(api_status_post_response.res.statusCode, 200);
|
|
461
|
+
assert.strictEqual(api_status_post_response.body, 'ok');
|
|
462
|
+
|
|
463
|
+
const api_health_response = await get_http_response(port, '/healthz');
|
|
464
|
+
assert.strictEqual(api_health_response.res.statusCode, 200);
|
|
465
|
+
assert.strictEqual(api_health_response.body, 'up');
|
|
466
|
+
|
|
467
|
+
assert(server_instance.website_manifest);
|
|
468
|
+
assert.strictEqual(server_instance.website_manifest.base_path, '/docs');
|
|
469
|
+
assert.deepStrictEqual(
|
|
470
|
+
server_instance.publication_summary.page_routes,
|
|
471
|
+
['/docs', '/docs/guide']
|
|
472
|
+
);
|
|
473
|
+
assert(
|
|
474
|
+
server_instance.publication_summary.warnings.some((warning_message) => warning_message.includes('non-GET methods'))
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('enforces HTTP methods on API endpoints and returns 405 for mismatched methods', async () => {
|
|
479
|
+
const port = await get_free_port();
|
|
480
|
+
const website_like = {
|
|
481
|
+
[Symbol.for('jsgui3.website')]: true,
|
|
482
|
+
name: 'API Enforcement Site',
|
|
483
|
+
api_endpoints: [
|
|
484
|
+
{
|
|
485
|
+
name: 'submit_data',
|
|
486
|
+
method: 'POST',
|
|
487
|
+
path: '/api/submit',
|
|
488
|
+
handler: () => 'submitted'
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'get_data',
|
|
492
|
+
method: 'GET',
|
|
493
|
+
path: '/api/data',
|
|
494
|
+
handler: () => 'data'
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: 'any_method',
|
|
498
|
+
method: 'ANY',
|
|
499
|
+
path: '/api/any',
|
|
500
|
+
handler: () => 'any'
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
server_instance = await Server.serve({
|
|
506
|
+
...website_like,
|
|
507
|
+
src_path_client_js: dummy_client_path,
|
|
508
|
+
host: '127.0.0.1',
|
|
509
|
+
port
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const get_submit_res = await get_http_response(port, '/api/submit');
|
|
513
|
+
assert.strictEqual(get_submit_res.res.statusCode, 405);
|
|
514
|
+
assert.strictEqual(get_submit_res.res.headers['allow'], 'POST');
|
|
515
|
+
assert.strictEqual(get_submit_res.body, 'Method Not Allowed');
|
|
516
|
+
|
|
517
|
+
const post_submit_opts = {
|
|
518
|
+
hostname: '127.0.0.1',
|
|
519
|
+
port,
|
|
520
|
+
path: '/api/submit',
|
|
521
|
+
method: 'POST',
|
|
522
|
+
headers: { 'Accept-Encoding': 'identity' }
|
|
523
|
+
};
|
|
524
|
+
const post_submit_res = await new Promise((resolve) => {
|
|
525
|
+
const req = http.request(post_submit_opts, res => {
|
|
526
|
+
let body = '';
|
|
527
|
+
res.setEncoding('utf8');
|
|
528
|
+
res.on('data', chunk => body += chunk);
|
|
529
|
+
res.on('end', () => resolve({ res, body }));
|
|
530
|
+
});
|
|
531
|
+
req.end();
|
|
532
|
+
});
|
|
533
|
+
assert.strictEqual(post_submit_res.res.statusCode, 200);
|
|
534
|
+
assert.strictEqual(post_submit_res.body, 'submitted');
|
|
535
|
+
|
|
536
|
+
const post_data_opts = {
|
|
537
|
+
hostname: '127.0.0.1',
|
|
538
|
+
port,
|
|
539
|
+
path: '/api/data',
|
|
540
|
+
method: 'POST'
|
|
541
|
+
};
|
|
542
|
+
const post_data_res = await new Promise((resolve) => {
|
|
543
|
+
const req = http.request(post_data_opts, res => resolve({ res }));
|
|
544
|
+
req.end();
|
|
545
|
+
});
|
|
546
|
+
assert.strictEqual(post_data_res.res.statusCode, 405);
|
|
547
|
+
assert.strictEqual(post_data_res.res.headers['allow'], 'GET');
|
|
548
|
+
|
|
549
|
+
const get_data_res = await get_http_response(port, '/api/data');
|
|
550
|
+
assert.strictEqual(get_data_res.res.statusCode, 200);
|
|
551
|
+
|
|
552
|
+
const get_any_res = await get_http_response(port, '/api/any');
|
|
553
|
+
assert.strictEqual(get_any_res.res.statusCode, 200);
|
|
554
|
+
|
|
555
|
+
const post_any_res = await new Promise((resolve) => {
|
|
556
|
+
const req = http.request({ ...post_data_opts, path: '/api/any' }, res => resolve({ res }));
|
|
557
|
+
req.end();
|
|
558
|
+
});
|
|
559
|
+
assert.strictEqual(post_any_res.res.statusCode, 200);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('rejects duplicate normalized page routes', async () => {
|
|
563
|
+
const port = await get_free_port();
|
|
564
|
+
await assert.rejects(
|
|
565
|
+
async () => {
|
|
566
|
+
await Server.serve({
|
|
567
|
+
pages: {
|
|
568
|
+
'/about': {
|
|
569
|
+
content: Dummy_Control
|
|
570
|
+
},
|
|
571
|
+
'/about/': {
|
|
572
|
+
content: Dummy_Control
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
src_path_client_js: dummy_client_path,
|
|
576
|
+
host: '127.0.0.1',
|
|
577
|
+
port
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
/duplicate_route: \/about/
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
});
|