qhttpx 1.8.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.
- package/.eslintrc.json +22 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/npm-publish.yml +37 -0
- package/.github/workflows/release.yml +21 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/package.json +61 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.js +58 -0
- package/dist/src/benchmarks/ultra-mode.js +122 -0
- package/dist/src/cli/index.js +200 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/fusion.js +183 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.js +111 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.js +905 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.js +61 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.js +33 -0
- package/dist/src/middleware/rate-limit.js +77 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.js +81 -0
- package/dist/src/router/router.js +146 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.js +2 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.js +64 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.js +44 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.js +9 -0
- package/docs/AEGIS.md +76 -0
- package/docs/BENCHMARKS.md +36 -0
- package/docs/CAPABILITIES.md +70 -0
- package/docs/CLI.md +43 -0
- package/docs/DATABASE.md +142 -0
- package/docs/ECOSYSTEM.md +146 -0
- package/docs/NEXT_STEPS.md +99 -0
- package/docs/OPENAPI.md +99 -0
- package/docs/PLUGINS.md +59 -0
- package/docs/REAL_WORLD_EXAMPLES.md +109 -0
- package/docs/ROADMAP.md +366 -0
- package/docs/VALIDATION.md +136 -0
- package/eslint.config.cjs +26 -0
- package/examples/api-server.ts +254 -0
- package/package.json +61 -0
- package/src/benchmarks/compare-frameworks.ts +149 -0
- package/src/benchmarks/quantam-users.ts +70 -0
- package/src/benchmarks/simple-json.ts +71 -0
- package/src/benchmarks/ultra-mode.ts +159 -0
- package/src/cli/index.ts +214 -0
- package/src/client/index.ts +93 -0
- package/src/core/batch.ts +110 -0
- package/src/core/body-parser.ts +151 -0
- package/src/core/buffer-pool.ts +96 -0
- package/src/core/config.ts +60 -0
- package/src/core/fusion.ts +210 -0
- package/src/core/logger.ts +70 -0
- package/src/core/metrics.ts +166 -0
- package/src/core/resources.ts +38 -0
- package/src/core/scheduler.ts +126 -0
- package/src/core/scope.ts +87 -0
- package/src/core/serializer.ts +41 -0
- package/src/core/server.ts +1113 -0
- package/src/core/stream.ts +111 -0
- package/src/core/tasks.ts +138 -0
- package/src/core/types.ts +178 -0
- package/src/core/websocket.ts +112 -0
- package/src/core/worker-queue.ts +90 -0
- package/src/database/adapters/memory.ts +99 -0
- package/src/database/adapters/mongo.ts +116 -0
- package/src/database/adapters/postgres.ts +86 -0
- package/src/database/adapters/sqlite.ts +44 -0
- package/src/database/coalescer.ts +153 -0
- package/src/database/manager.ts +97 -0
- package/src/database/types.ts +24 -0
- package/src/index.ts +42 -0
- package/src/middleware/compression.ts +147 -0
- package/src/middleware/cors.ts +98 -0
- package/src/middleware/presets.ts +50 -0
- package/src/middleware/rate-limit.ts +106 -0
- package/src/middleware/security.ts +109 -0
- package/src/middleware/static.ts +216 -0
- package/src/openapi/generator.ts +167 -0
- package/src/router/radix-router.ts +119 -0
- package/src/router/radix-tree.ts +106 -0
- package/src/router/router.ts +190 -0
- package/src/testing/index.ts +104 -0
- package/src/utils/cookies.ts +67 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/signals.ts +45 -0
- package/src/utils/sse.ts +41 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/simple.ts +93 -0
- package/src/validation/types.ts +38 -0
- package/src/validation/zod.ts +14 -0
- package/src/views/index.ts +1 -0
- package/src/views/types.ts +4 -0
- package/tests/adapters.test.ts +120 -0
- package/tests/batch.test.ts +139 -0
- package/tests/body-parser.test.ts +83 -0
- package/tests/compression-sse.test.ts +98 -0
- package/tests/cookies.test.ts +74 -0
- package/tests/cors.test.ts +79 -0
- package/tests/database.test.ts +90 -0
- package/tests/dx.test.ts +78 -0
- package/tests/ecosystem.test.ts +156 -0
- package/tests/features.test.ts +51 -0
- package/tests/fusion.test.ts +121 -0
- package/tests/http-basic.test.ts +161 -0
- package/tests/logger.test.ts +48 -0
- package/tests/middleware.test.ts +137 -0
- package/tests/observability.test.ts +91 -0
- package/tests/openapi.test.ts +74 -0
- package/tests/plugin.test.ts +85 -0
- package/tests/plugins.test.ts +93 -0
- package/tests/rate-limit.test.ts +97 -0
- package/tests/resources.test.ts +64 -0
- package/tests/scheduler.test.ts +71 -0
- package/tests/schema-routes.test.ts +89 -0
- package/tests/security.test.ts +128 -0
- package/tests/server-db.test.ts +72 -0
- package/tests/smoke.test.ts +9 -0
- package/tests/sqlite-fusion.test.ts +106 -0
- package/tests/static.test.ts +111 -0
- package/tests/stream.test.ts +58 -0
- package/tests/task-metrics.test.ts +78 -0
- package/tests/tasks.test.ts +90 -0
- package/tests/testing.test.ts +53 -0
- package/tests/validation.test.ts +126 -0
- package/tests/websocket.test.ts +132 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
const logger_1 = require("../src/core/logger");
|
|
6
|
+
const client_1 = require("../src/client");
|
|
7
|
+
(0, vitest_1.describe)('Logger', () => {
|
|
8
|
+
(0, vitest_1.it)('should initialize with pino', () => {
|
|
9
|
+
const logger = new logger_1.Logger({ level: 'info' });
|
|
10
|
+
(0, vitest_1.expect)(logger).toBeDefined();
|
|
11
|
+
// Check if methods exist (can't easily mock internal pino without spy)
|
|
12
|
+
(0, vitest_1.expect)(typeof logger.info).toBe('function');
|
|
13
|
+
(0, vitest_1.expect)(typeof logger.error).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)('should be integrated into QHTTPX', () => {
|
|
16
|
+
const app = new index_1.QHTTPX();
|
|
17
|
+
(0, vitest_1.expect)(app.logger).toBeDefined();
|
|
18
|
+
(0, vitest_1.expect)(app.logger instanceof logger_1.Logger).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.describe)('RPC Client', () => {
|
|
22
|
+
(0, vitest_1.it)('should construct correct URLs and methods', async () => {
|
|
23
|
+
// We mock fetch globally
|
|
24
|
+
const originalFetch = global.fetch;
|
|
25
|
+
global.fetch = async (url, options) => {
|
|
26
|
+
return new Response(JSON.stringify({ url, method: options?.method }), {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const client = (0, client_1.hc)('http://localhost:3000');
|
|
34
|
+
const res1 = await client.api.users[':id'].$get({ param: { id: '123' } });
|
|
35
|
+
const data1 = await res1.json();
|
|
36
|
+
(0, vitest_1.expect)(data1.url).toBe('http://localhost:3000/api/users/123');
|
|
37
|
+
(0, vitest_1.expect)(data1.method).toBe('GET');
|
|
38
|
+
const res2 = await client.auth.login.$post({ json: { username: 'test' } });
|
|
39
|
+
const data2 = await res2.json();
|
|
40
|
+
(0, vitest_1.expect)(data2.url).toBe('http://localhost:3000/auth/login');
|
|
41
|
+
(0, vitest_1.expect)(data2.method).toBe('POST');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
global.fetch = originalFetch;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
(0, vitest_1.describe)('Request Fusion Engine', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app)
|
|
9
|
+
await app.close();
|
|
10
|
+
});
|
|
11
|
+
(0, vitest_1.it)('should coalesce simultaneous requests to the same endpoint', async () => {
|
|
12
|
+
app = new server_1.QHTTPX({ enableRequestFusion: true });
|
|
13
|
+
let executionCount = 0;
|
|
14
|
+
// Simulate a slow handler
|
|
15
|
+
app.get('/slow', async (ctx) => {
|
|
16
|
+
executionCount++;
|
|
17
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
18
|
+
ctx.json({ count: executionCount });
|
|
19
|
+
});
|
|
20
|
+
const { port } = await app.listen(0);
|
|
21
|
+
const url = `http://localhost:${port}/slow`;
|
|
22
|
+
// Fire 3 requests simultaneously
|
|
23
|
+
const p1 = fetch(url).then(r => r.json());
|
|
24
|
+
const p2 = fetch(url).then(r => r.json());
|
|
25
|
+
const p3 = fetch(url).then(r => r.json());
|
|
26
|
+
const results = await Promise.all([p1, p2, p3]);
|
|
27
|
+
// All should get the same result
|
|
28
|
+
(0, vitest_1.expect)(results[0]).toEqual({ count: 1 });
|
|
29
|
+
(0, vitest_1.expect)(results[1]).toEqual({ count: 1 });
|
|
30
|
+
(0, vitest_1.expect)(results[2]).toEqual({ count: 1 });
|
|
31
|
+
// Handler should have run only once
|
|
32
|
+
(0, vitest_1.expect)(executionCount).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)('should NOT coalesce requests with different query params', async () => {
|
|
35
|
+
app = new server_1.QHTTPX({ enableRequestFusion: true });
|
|
36
|
+
let executionCount = 0;
|
|
37
|
+
app.get('/echo', async (ctx) => {
|
|
38
|
+
executionCount++;
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
40
|
+
ctx.json({ q: ctx.query.q, count: executionCount });
|
|
41
|
+
});
|
|
42
|
+
const { port } = await app.listen(0);
|
|
43
|
+
const p1 = fetch(`http://localhost:${port}/echo?q=a`).then(r => r.json());
|
|
44
|
+
const p2 = fetch(`http://localhost:${port}/echo?q=b`).then(r => r.json());
|
|
45
|
+
const results = await Promise.all([p1, p2]);
|
|
46
|
+
(0, vitest_1.expect)(results[0].q).toBe('a');
|
|
47
|
+
(0, vitest_1.expect)(results[1].q).toBe('b');
|
|
48
|
+
// Should run twice because keys are different
|
|
49
|
+
(0, vitest_1.expect)(executionCount).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('should use cache window to coalesce burst traffic', async () => {
|
|
52
|
+
// 50ms window
|
|
53
|
+
app = new server_1.QHTTPX({
|
|
54
|
+
enableRequestFusion: { windowMs: 50 }
|
|
55
|
+
});
|
|
56
|
+
let executionCount = 0;
|
|
57
|
+
app.get('/burst', async (ctx) => {
|
|
58
|
+
executionCount++;
|
|
59
|
+
// Fast handler
|
|
60
|
+
ctx.json({ count: executionCount });
|
|
61
|
+
});
|
|
62
|
+
const { port } = await app.listen(0);
|
|
63
|
+
const url = `http://localhost:${port}/burst`;
|
|
64
|
+
// Request 1
|
|
65
|
+
const r1 = await fetch(url).then(r => r.json());
|
|
66
|
+
(0, vitest_1.expect)(r1.count).toBe(1);
|
|
67
|
+
// Request 2 (immediate, within window)
|
|
68
|
+
const r2 = await fetch(url).then(r => r.json());
|
|
69
|
+
(0, vitest_1.expect)(r2.count).toBe(1); // Should be cached
|
|
70
|
+
// Request 3 (wait 100ms, outside window)
|
|
71
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
72
|
+
const r3 = await fetch(url).then(r => r.json());
|
|
73
|
+
(0, vitest_1.expect)(r3.count).toBe(2); // New execution
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('should respect vary headers (Authorization)', async () => {
|
|
76
|
+
app = new server_1.QHTTPX({ enableRequestFusion: true });
|
|
77
|
+
let executionCount = 0;
|
|
78
|
+
app.get('/auth', async (ctx) => {
|
|
79
|
+
executionCount++;
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
81
|
+
ctx.json({ user: ctx.req.headers['authorization'], count: executionCount });
|
|
82
|
+
});
|
|
83
|
+
const { port } = await app.listen(0);
|
|
84
|
+
const url = `http://localhost:${port}/auth`;
|
|
85
|
+
const p1 = fetch(url, { headers: { 'Authorization': 'UserA' } }).then(r => r.json());
|
|
86
|
+
const p2 = fetch(url, { headers: { 'Authorization': 'UserB' } }).then(r => r.json());
|
|
87
|
+
const results = await Promise.all([p1, p2]);
|
|
88
|
+
(0, vitest_1.expect)(results[0].user).toBe('UserA');
|
|
89
|
+
(0, vitest_1.expect)(results[1].user).toBe('UserB');
|
|
90
|
+
(0, vitest_1.expect)(executionCount).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
(0, vitest_1.describe)('QHTTPX minimal HTTP runtime', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('handles GET / and returns JSON', async () => {
|
|
14
|
+
app = new index_1.QHTTPX();
|
|
15
|
+
app.get('/', (ctx) => {
|
|
16
|
+
ctx.json({ message: 'Hello from QHTTPX' });
|
|
17
|
+
});
|
|
18
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
19
|
+
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
20
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
21
|
+
const json = (await response.json());
|
|
22
|
+
(0, vitest_1.expect)(json.message).toBe('Hello from QHTTPX');
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)('returns 404 for unknown route', async () => {
|
|
25
|
+
app = new index_1.QHTTPX();
|
|
26
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
27
|
+
const response = await fetch(`http://127.0.0.1:${port}/unknown`);
|
|
28
|
+
(0, vitest_1.expect)(response.status).toBe(404);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)('supports params and query parsing', async () => {
|
|
31
|
+
app = new index_1.QHTTPX();
|
|
32
|
+
app.get('/users/:id', (ctx) => {
|
|
33
|
+
ctx.json({
|
|
34
|
+
id: ctx.params.id,
|
|
35
|
+
q: ctx.query.q,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
39
|
+
const response = await fetch(`http://127.0.0.1:${port}/users/123?q=test`);
|
|
40
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
41
|
+
const json = (await response.json());
|
|
42
|
+
(0, vitest_1.expect)(json.id).toBe('123');
|
|
43
|
+
(0, vitest_1.expect)(json.q).toBe('test');
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.it)('parses JSON body for POST', async () => {
|
|
46
|
+
app = new index_1.QHTTPX();
|
|
47
|
+
app.post('/echo', (ctx) => {
|
|
48
|
+
ctx.json({ body: ctx.body });
|
|
49
|
+
});
|
|
50
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
51
|
+
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'content-type': 'application/json',
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({ hello: 'world' }),
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
59
|
+
const json = (await response.json());
|
|
60
|
+
(0, vitest_1.expect)(json.body.hello).toBe('world');
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)('returns 400 for invalid JSON body', async () => {
|
|
63
|
+
app = new index_1.QHTTPX();
|
|
64
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
65
|
+
app.post('/echo', (ctx) => {
|
|
66
|
+
ctx.json({ body: ctx.body });
|
|
67
|
+
});
|
|
68
|
+
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
body: '{ invalid',
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.expect)(response.status).toBe(400);
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
(0, vitest_1.expect)(text).toBe('Invalid JSON');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('enforces maxBodyBytes with 413 status', async () => {
|
|
80
|
+
app = new index_1.QHTTPX({ maxBodyBytes: 8 });
|
|
81
|
+
app.post('/echo', (ctx) => {
|
|
82
|
+
ctx.json({ body: ctx.body });
|
|
83
|
+
});
|
|
84
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
85
|
+
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'content-type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({ hello: 'world' }),
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.expect)(response.status).toBe(413);
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
(0, vitest_1.expect)(text).toBe('Payload Too Large');
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('enforces maxConcurrency with 503 overload', async () => {
|
|
97
|
+
app = new index_1.QHTTPX({ maxConcurrency: 1 });
|
|
98
|
+
app.get('/slow', async (ctx) => {
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
setTimeout(resolve, 100);
|
|
101
|
+
});
|
|
102
|
+
ctx.json({ ok: true });
|
|
103
|
+
});
|
|
104
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
105
|
+
const url = `http://127.0.0.1:${port}/slow`;
|
|
106
|
+
const first = fetch(url);
|
|
107
|
+
const second = fetch(url);
|
|
108
|
+
const [firstRes, secondRes] = await Promise.all([first, second]);
|
|
109
|
+
const statuses = [firstRes.status, secondRes.status].sort();
|
|
110
|
+
(0, vitest_1.expect)(statuses).toEqual([200, 503]);
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.it)('applies request timeout with 504 status', async () => {
|
|
113
|
+
app = new index_1.QHTTPX({ requestTimeoutMs: 20 });
|
|
114
|
+
app.get('/timeout', async (ctx) => {
|
|
115
|
+
await new Promise((resolve) => {
|
|
116
|
+
setTimeout(resolve, 100);
|
|
117
|
+
});
|
|
118
|
+
ctx.json({ ok: true });
|
|
119
|
+
});
|
|
120
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
121
|
+
const response = await fetch(`http://127.0.0.1:${port}/timeout`);
|
|
122
|
+
(0, vitest_1.expect)(response.status).toBe(504);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const src_1 = require("../src");
|
|
5
|
+
(0, vitest_1.describe)('Logger middleware', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('logs basic request information to sink', async () => {
|
|
14
|
+
const sink = vitest_1.vi.fn();
|
|
15
|
+
app = new src_1.QHTTPX();
|
|
16
|
+
app.use((0, src_1.createLoggerMiddleware)({
|
|
17
|
+
sink,
|
|
18
|
+
}));
|
|
19
|
+
app.get('/hello', (ctx) => {
|
|
20
|
+
ctx.send('ok');
|
|
21
|
+
});
|
|
22
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
23
|
+
const response = await fetch(`http://127.0.0.1:${port}/hello`);
|
|
24
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
25
|
+
(0, vitest_1.expect)(await response.text()).toBe('ok');
|
|
26
|
+
(0, vitest_1.expect)(sink).toHaveBeenCalledTimes(1);
|
|
27
|
+
const entry = sink.mock.calls[0][0];
|
|
28
|
+
(0, vitest_1.expect)(entry.method).toBe('GET');
|
|
29
|
+
(0, vitest_1.expect)(entry.path).toBe('/hello');
|
|
30
|
+
(0, vitest_1.expect)(entry.status).toBe(200);
|
|
31
|
+
(0, vitest_1.expect)(entry.durationMs).toBeGreaterThanOrEqual(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
(0, vitest_1.describe)('QHTTPX middleware pipeline', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('runs middleware in order with await next()', async () => {
|
|
14
|
+
app = new index_1.QHTTPX();
|
|
15
|
+
const calls = [];
|
|
16
|
+
app.use(async (ctx, next) => {
|
|
17
|
+
calls.push('m1-before');
|
|
18
|
+
await next();
|
|
19
|
+
calls.push('m1-after');
|
|
20
|
+
});
|
|
21
|
+
app.use(async (ctx, next) => {
|
|
22
|
+
calls.push('m2-before');
|
|
23
|
+
await next();
|
|
24
|
+
calls.push('m2-after');
|
|
25
|
+
});
|
|
26
|
+
app.get('/', (ctx) => {
|
|
27
|
+
calls.push('handler');
|
|
28
|
+
ctx.send('ok');
|
|
29
|
+
});
|
|
30
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
31
|
+
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
32
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
33
|
+
(0, vitest_1.expect)(calls).toEqual([
|
|
34
|
+
'm1-before',
|
|
35
|
+
'm2-before',
|
|
36
|
+
'handler',
|
|
37
|
+
'm2-after',
|
|
38
|
+
'm1-after',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
(0, vitest_1.it)('allows middleware to modify response headers', async () => {
|
|
42
|
+
app = new index_1.QHTTPX();
|
|
43
|
+
app.use((ctx, next) => {
|
|
44
|
+
ctx.res.setHeader('x-powered-by', 'qhttpx');
|
|
45
|
+
return next();
|
|
46
|
+
});
|
|
47
|
+
app.get('/', (ctx) => {
|
|
48
|
+
ctx.send('ok');
|
|
49
|
+
});
|
|
50
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
51
|
+
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
52
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
53
|
+
const header = response.headers.get('x-powered-by');
|
|
54
|
+
(0, vitest_1.expect)(header).toBe('qhttpx');
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)('supports auth middleware that short-circuits unauthorized requests', async () => {
|
|
57
|
+
app = new index_1.QHTTPX();
|
|
58
|
+
app.use((ctx, next) => {
|
|
59
|
+
const auth = ctx.req.headers['authorization'];
|
|
60
|
+
if (auth !== 'Bearer secret') {
|
|
61
|
+
ctx.res.statusCode = 401;
|
|
62
|
+
ctx.res.end('Unauthorized');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return next();
|
|
66
|
+
});
|
|
67
|
+
app.get('/secure', (ctx) => {
|
|
68
|
+
ctx.send('ok');
|
|
69
|
+
});
|
|
70
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
71
|
+
const unauthorized = await fetch(`http://127.0.0.1:${port}/secure`);
|
|
72
|
+
(0, vitest_1.expect)(unauthorized.status).toBe(401);
|
|
73
|
+
const unauthorizedText = await unauthorized.text();
|
|
74
|
+
(0, vitest_1.expect)(unauthorizedText).toBe('Unauthorized');
|
|
75
|
+
const authorized = await fetch(`http://127.0.0.1:${port}/secure`, {
|
|
76
|
+
headers: {
|
|
77
|
+
authorization: 'Bearer secret',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.expect)(authorized.status).toBe(200);
|
|
81
|
+
const body = await authorized.text();
|
|
82
|
+
(0, vitest_1.expect)(body).toBe('ok');
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('supports error-handling middleware that converts errors to JSON', async () => {
|
|
85
|
+
app = new index_1.QHTTPX();
|
|
86
|
+
app.use(async (ctx, next) => {
|
|
87
|
+
try {
|
|
88
|
+
await next();
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
ctx.res.statusCode = 500;
|
|
92
|
+
ctx.res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
93
|
+
ctx.res.end(JSON.stringify({
|
|
94
|
+
error: 'internal',
|
|
95
|
+
message: err instanceof Error ? err.message : 'unknown',
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
app.get('/boom', () => {
|
|
100
|
+
throw new Error('boom');
|
|
101
|
+
});
|
|
102
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
103
|
+
const response = await fetch(`http://127.0.0.1:${port}/boom`);
|
|
104
|
+
(0, vitest_1.expect)(response.status).toBe(500);
|
|
105
|
+
const json = (await response.json());
|
|
106
|
+
(0, vitest_1.expect)(json.error).toBe('internal');
|
|
107
|
+
(0, vitest_1.expect)(json.message).toBe('boom');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
(0, vitest_1.describe)('QHTTPX observability and health', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('exposes a health endpoint with basic info', async () => {
|
|
14
|
+
app = new index_1.QHTTPX();
|
|
15
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
16
|
+
const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/health`);
|
|
17
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
18
|
+
const json = (await response.json());
|
|
19
|
+
(0, vitest_1.expect)(json.status).toBe('ok');
|
|
20
|
+
(0, vitest_1.expect)(typeof json.name).toBe('string');
|
|
21
|
+
(0, vitest_1.expect)(typeof json.version).toBe('string');
|
|
22
|
+
(0, vitest_1.expect)(json.workers).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)('exposes metrics for handled requests', async () => {
|
|
25
|
+
app = new index_1.QHTTPX();
|
|
26
|
+
app.get('/ping', (ctx) => {
|
|
27
|
+
ctx.send('pong');
|
|
28
|
+
});
|
|
29
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
30
|
+
await fetch(`http://127.0.0.1:${port}/ping`);
|
|
31
|
+
await fetch(`http://127.0.0.1:${port}/ping`);
|
|
32
|
+
const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
|
|
33
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
34
|
+
const json = (await response.json());
|
|
35
|
+
(0, vitest_1.expect)(json.totalRequests).toBeGreaterThanOrEqual(2);
|
|
36
|
+
(0, vitest_1.expect)(json.inFlightRequests).toBeGreaterThanOrEqual(0);
|
|
37
|
+
(0, vitest_1.expect)(json.latency.p50 === null || json.latency.p50 >= 0).toBe(true);
|
|
38
|
+
(0, vitest_1.expect)(json.memory.rssBytes).toBeGreaterThan(0);
|
|
39
|
+
(0, vitest_1.expect)(json.memory.heapUsedBytes).toBeGreaterThan(0);
|
|
40
|
+
(0, vitest_1.expect)(json.workers).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.it)('propagates and generates x-request-id headers', async () => {
|
|
43
|
+
app = new index_1.QHTTPX();
|
|
44
|
+
app.get('/ping', (ctx) => {
|
|
45
|
+
ctx.send('pong');
|
|
46
|
+
});
|
|
47
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
48
|
+
const first = await fetch(`http://127.0.0.1:${port}/ping`);
|
|
49
|
+
const firstId = first.headers.get('x-request-id');
|
|
50
|
+
(0, vitest_1.expect)(firstId === null || firstId.length > 0).toBe(true);
|
|
51
|
+
const second = await fetch(`http://127.0.0.1:${port}/ping`, {
|
|
52
|
+
headers: {
|
|
53
|
+
'x-request-id': 'custom-id-123',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const secondId = second.headers.get('x-request-id');
|
|
57
|
+
(0, vitest_1.expect)(secondId).toBe('custom-id-123');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
(0, vitest_1.describe)('OpenAPI Generator', () => {
|
|
6
|
+
(0, vitest_1.it)('should generate valid OpenAPI spec from routes', () => {
|
|
7
|
+
const app = new server_1.QHTTPX();
|
|
8
|
+
const userSchema = {
|
|
9
|
+
params: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'string' }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
body: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
name: { type: 'string' },
|
|
19
|
+
age: { type: 'number', min: 18 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
response: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
success: { type: 'boolean' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
app.post('/users/:id', {
|
|
30
|
+
schema: userSchema,
|
|
31
|
+
handler: (ctx) => { ctx.json({ success: true }); }
|
|
32
|
+
});
|
|
33
|
+
app.get('/health', (ctx) => { ctx.send('ok'); });
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
const spec = app.getOpenAPI({
|
|
36
|
+
info: {
|
|
37
|
+
title: 'Test API',
|
|
38
|
+
version: '1.0.0'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Check basic structure
|
|
42
|
+
(0, vitest_1.expect)(spec.openapi).toBe('3.0.0');
|
|
43
|
+
(0, vitest_1.expect)(spec.info.title).toBe('Test API');
|
|
44
|
+
// Check Path normalization
|
|
45
|
+
(0, vitest_1.expect)(spec.paths['/users/{id}']).toBeDefined();
|
|
46
|
+
(0, vitest_1.expect)(spec.paths['/health']).toBeDefined();
|
|
47
|
+
// Check Operation
|
|
48
|
+
const postUser = spec.paths['/users/{id}'].post;
|
|
49
|
+
(0, vitest_1.expect)(postUser).toBeDefined();
|
|
50
|
+
// Check Params
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const pathParam = postUser.parameters.find((p) => p.name === 'id');
|
|
53
|
+
(0, vitest_1.expect)(pathParam).toBeDefined();
|
|
54
|
+
(0, vitest_1.expect)(pathParam.in).toBe('path');
|
|
55
|
+
(0, vitest_1.expect)(pathParam.required).toBe(true);
|
|
56
|
+
// Check Request Body
|
|
57
|
+
const bodySchema = postUser.requestBody.content['application/json'].schema;
|
|
58
|
+
(0, vitest_1.expect)(bodySchema.properties.name.type).toBe('string');
|
|
59
|
+
(0, vitest_1.expect)(bodySchema.properties.age.minimum).toBe(18);
|
|
60
|
+
// Check Response
|
|
61
|
+
const responseSchema = postUser.responses['200'].content['application/json'].schema;
|
|
62
|
+
(0, vitest_1.expect)(responseSchema.properties.success.type).toBe('boolean');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
(0, vitest_1.describe)('Plugin System', () => {
|
|
6
|
+
(0, vitest_1.it)('should register a simple plugin', async () => {
|
|
7
|
+
const app = new server_1.QHTTPX();
|
|
8
|
+
let pluginRun = false;
|
|
9
|
+
const myPlugin = async (scope) => {
|
|
10
|
+
pluginRun = true;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
scope.get('/ping', (ctx) => ctx.json({ pong: true }));
|
|
13
|
+
};
|
|
14
|
+
await app.register(myPlugin);
|
|
15
|
+
(0, vitest_1.expect)(pluginRun).toBe(true);
|
|
16
|
+
const { port } = await app.listen(0);
|
|
17
|
+
const res = await fetch(`http://localhost:${port}/ping`);
|
|
18
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
19
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ pong: true });
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)('should handle prefixes', async () => {
|
|
22
|
+
const app = new server_1.QHTTPX();
|
|
23
|
+
const apiPlugin = async (scope) => {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
scope.get('/users', (ctx) => ctx.json({ users: [] }));
|
|
26
|
+
};
|
|
27
|
+
// Register with prefix /v1
|
|
28
|
+
await app.register(apiPlugin, { prefix: '/v1' });
|
|
29
|
+
const { port } = await app.listen(0);
|
|
30
|
+
// /v1/users should exist
|
|
31
|
+
const res = await fetch(`http://localhost:${port}/v1/users`);
|
|
32
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
33
|
+
// /users should NOT exist
|
|
34
|
+
const res404 = await fetch(`http://localhost:${port}/users`);
|
|
35
|
+
(0, vitest_1.expect)(res404.status).toBe(404);
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('should handle nested plugins with concatenated prefixes', async () => {
|
|
38
|
+
const app = new server_1.QHTTPX();
|
|
39
|
+
const usersPlugin = async (scope) => {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
scope.get('/list', (ctx) => ctx.json({ list: true }));
|
|
42
|
+
};
|
|
43
|
+
const v1Plugin = async (scope) => {
|
|
44
|
+
// Register nested plugin under /users
|
|
45
|
+
await scope.register(usersPlugin, { prefix: '/users' });
|
|
46
|
+
};
|
|
47
|
+
// Register v1 under /api
|
|
48
|
+
await app.register(v1Plugin, { prefix: '/api' });
|
|
49
|
+
const { port } = await app.listen(0);
|
|
50
|
+
// Result should be /api/users/list
|
|
51
|
+
const res = await fetch(`http://localhost:${port}/api/users/list`);
|
|
52
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
53
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ list: true });
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('should pass options to plugins', async () => {
|
|
56
|
+
const app = new server_1.QHTTPX();
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
let receivedOpts;
|
|
59
|
+
const configPlugin = async (scope, opts) => {
|
|
60
|
+
receivedOpts = opts;
|
|
61
|
+
};
|
|
62
|
+
await app.register(configPlugin, { prefix: '/a', secret: '123' });
|
|
63
|
+
(0, vitest_1.expect)(receivedOpts).toEqual({ prefix: '/a', secret: '123' });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
const testing_1 = require("../src/testing");
|
|
6
|
+
(0, vitest_1.describe)('Plugin System', () => {
|
|
7
|
+
(0, vitest_1.it)('should register a simple plugin', async () => {
|
|
8
|
+
const app = new server_1.QHTTPX();
|
|
9
|
+
const myPlugin = (s) => {
|
|
10
|
+
const scope = s;
|
|
11
|
+
scope.get('/plugin', (ctx) => {
|
|
12
|
+
ctx.json({ message: 'Hello from Plugin' });
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
await app.register(myPlugin);
|
|
16
|
+
const client = (0, testing_1.createTestClient)(app);
|
|
17
|
+
const res = await client.get('/plugin');
|
|
18
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
19
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ message: 'Hello from Plugin' });
|
|
20
|
+
await client.stop();
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)('should support scoped prefixes', async () => {
|
|
23
|
+
const app = new server_1.QHTTPX();
|
|
24
|
+
const v1Plugin = (s) => {
|
|
25
|
+
const scope = s;
|
|
26
|
+
scope.get('/users', (ctx) => {
|
|
27
|
+
ctx.json({ version: 'v1' });
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
await app.register(v1Plugin, { prefix: '/v1' });
|
|
31
|
+
const client = (0, testing_1.createTestClient)(app);
|
|
32
|
+
const res = await client.get('/v1/users');
|
|
33
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
34
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ version: 'v1' });
|
|
35
|
+
await client.stop();
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('should support nested plugins', async () => {
|
|
38
|
+
const app = new server_1.QHTTPX();
|
|
39
|
+
const usersPlugin = (s) => {
|
|
40
|
+
const scope = s;
|
|
41
|
+
scope.get('/list', (ctx) => {
|
|
42
|
+
ctx.json({ users: [] });
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const apiPlugin = async (s) => {
|
|
46
|
+
const scope = s;
|
|
47
|
+
await scope.register(usersPlugin, { prefix: '/users' });
|
|
48
|
+
};
|
|
49
|
+
await app.register(apiPlugin, { prefix: '/api' });
|
|
50
|
+
const client = (0, testing_1.createTestClient)(app);
|
|
51
|
+
const res = await client.get('/api/users/list');
|
|
52
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
53
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ users: [] });
|
|
54
|
+
await client.stop();
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)('should support plugin options', async () => {
|
|
57
|
+
const app = new server_1.QHTTPX();
|
|
58
|
+
const authPlugin = (s, options) => {
|
|
59
|
+
const scope = s;
|
|
60
|
+
scope.get('/secret', (ctx) => {
|
|
61
|
+
ctx.json({ secret: options.secret });
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
await app.register(authPlugin, { secret: 'super-secret' });
|
|
65
|
+
const client = (0, testing_1.createTestClient)(app);
|
|
66
|
+
const res = await client.get('/secret');
|
|
67
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
68
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ secret: 'super-secret' });
|
|
69
|
+
await client.stop();
|
|
70
|
+
});
|
|
71
|
+
});
|