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.
Files changed (86) hide show
  1. package/.github/instructions/copilot.instructions.md +1 -0
  2. package/AGENTS.md +2 -0
  3. package/README.md +89 -13
  4. package/admin-ui/v1/controls/admin_shell.js +702 -669
  5. package/admin-ui/v1/server.js +14 -1
  6. package/docs/api-reference.md +504 -306
  7. package/docs/books/creating-a-new-admin-ui/README.md +20 -20
  8. package/docs/books/website-design/01-introduction.md +73 -0
  9. package/docs/books/website-design/02-current-state.md +195 -0
  10. package/docs/books/website-design/03-base-class.md +181 -0
  11. package/docs/books/website-design/04-webpage.md +307 -0
  12. package/docs/books/website-design/05-website.md +456 -0
  13. package/docs/books/website-design/06-pages-storage.md +170 -0
  14. package/docs/books/website-design/07-api-layer.md +285 -0
  15. package/docs/books/website-design/08-server-integration.md +271 -0
  16. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  17. package/docs/books/website-design/10-open-questions.md +196 -0
  18. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  19. package/docs/books/website-design/12-content-model.md +395 -0
  20. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  21. package/docs/books/website-design/14-website-module-spec.md +541 -0
  22. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  23. package/docs/books/website-design/16-minimal-first.md +203 -0
  24. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  25. package/docs/books/website-design/README.md +43 -0
  26. package/docs/comprehensive-documentation.md +220 -220
  27. package/docs/configuration-reference.md +281 -204
  28. package/docs/middleware-guide.md +236 -0
  29. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  30. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  31. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  32. package/docs/swagger.md +316 -0
  33. package/docs/system-architecture.md +24 -18
  34. package/examples/controls/1) window/server.js +6 -1
  35. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  36. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  37. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  38. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  39. declarative api/e2e-screenshot-1-name-change.png +0 -0
  40. declarative api/e2e-screenshot-2-toggled.png +0 -0
  41. declarative api/e2e-screenshot-3-final.png +0 -0
  42. declarative api/e2e-screenshot-final.png +0 -0
  43. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  44. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  45. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  46. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  47. package/examples/data-views/01) query-endpoint/server.js +61 -0
  48. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  49. package/labs/website-design/002-pages-storage/check.js +244 -0
  50. package/labs/website-design/002-pages-storage/results.txt +0 -0
  51. package/labs/website-design/003-type-detection/check.js +193 -0
  52. package/labs/website-design/003-type-detection/results.txt +0 -0
  53. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  54. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  55. package/labs/website-design/005-normalize-input/check.js +303 -0
  56. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  57. package/labs/website-design/README.md +34 -0
  58. package/labs/website-design/manifest.json +68 -0
  59. package/labs/website-design/run-all.js +60 -0
  60. package/middleware/compression.js +217 -0
  61. package/middleware/index.js +15 -0
  62. package/middleware/json-body.js +126 -0
  63. package/module.js +3 -0
  64. package/openapi.js +474 -0
  65. package/package.json +11 -8
  66. package/publishers/Publishers.js +6 -5
  67. package/publishers/http-function-publisher.js +135 -126
  68. package/publishers/http-webpage-publisher.js +89 -11
  69. package/publishers/query-publisher.js +116 -0
  70. package/publishers/swagger-publisher.js +203 -0
  71. package/publishers/swagger-ui.js +578 -0
  72. package/resources/adapters/array-adapter.js +143 -0
  73. package/resources/query-resource.js +131 -0
  74. package/serve-factory.js +756 -18
  75. package/server.js +502 -123
  76. package/tests/README.md +23 -1
  77. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  78. package/tests/helpers/playwright-e2e-harness.js +326 -0
  79. package/tests/openapi.test.js +319 -0
  80. package/tests/playwright-smoke.test.js +134 -0
  81. package/tests/publish-enhancements.test.js +673 -0
  82. package/tests/query-publisher.test.js +430 -0
  83. package/tests/quick-json-body-test.js +169 -0
  84. package/tests/serve.test.js +425 -122
  85. package/tests/swagger-publisher.test.js +1076 -0
  86. package/tests/test-runner.js +1 -0
@@ -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('waits for webpage publisher readiness before resolving serve()', async () => {
257
- Fake_Webpage_Publisher.ready_delay_ms = 2600;
258
-
259
- const port = await get_free_port();
260
- const started_at = Date.now();
261
-
262
- server_instance = await Server.serve({
263
- Ctrl: Dummy_Control,
264
- src_path_client_js: dummy_client_path,
265
- host: '127.0.0.1',
266
- port,
267
- readyTimeoutMs: 12000
268
- });
269
-
270
- const elapsed_ms = Date.now() - started_at;
271
- assert(
272
- elapsed_ms >= 2400,
273
- `Expected serve() to wait for delayed readiness, elapsed=${elapsed_ms}ms`
274
- );
275
-
276
- const { res, body } = await get_http_response(port, '/');
277
- assert.strictEqual(res.statusCode, 200);
278
- assert(body.includes('<div class="dummy-control"'));
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
+ });