@dangao/bun-server 2.3.0 → 3.0.1
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/README.md +81 -3
- package/dist/auth/jwt.d.ts.map +1 -1
- package/dist/config/service.d.ts +0 -1
- package/dist/config/service.d.ts.map +1 -1
- package/dist/core/application.d.ts +13 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/server.d.ts +12 -9
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts.map +1 -1
- package/dist/database/connection-manager.d.ts.map +1 -1
- package/dist/database/connection-pool.d.ts +3 -3
- package/dist/database/connection-pool.d.ts.map +1 -1
- package/dist/database/service.d.ts +2 -1
- package/dist/database/service.d.ts.map +1 -1
- package/dist/database/sql-manager.d.ts +8 -4
- package/dist/database/sql-manager.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.d.ts +7 -3
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/debug/recorder.d.ts +0 -1
- package/dist/debug/recorder.d.ts.map +1 -1
- package/dist/files/static-middleware.d.ts.map +1 -1
- package/dist/files/storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39487 -3496
- package/dist/index.node.mjs +17723 -0
- package/dist/middleware/builtin/static-file.d.ts +4 -2
- package/dist/middleware/builtin/static-file.d.ts.map +1 -1
- package/dist/platform/bun/crypto.d.ts +3 -0
- package/dist/platform/bun/crypto.d.ts.map +1 -0
- package/dist/platform/bun/fs.d.ts +3 -0
- package/dist/platform/bun/fs.d.ts.map +1 -0
- package/dist/platform/bun/http.d.ts +15 -0
- package/dist/platform/bun/http.d.ts.map +1 -0
- package/dist/platform/bun/index.d.ts +3 -0
- package/dist/platform/bun/index.d.ts.map +1 -0
- package/dist/platform/bun/parser.d.ts +3 -0
- package/dist/platform/bun/parser.d.ts.map +1 -0
- package/dist/platform/bun/process.d.ts +3 -0
- package/dist/platform/bun/process.d.ts.map +1 -0
- package/dist/platform/detector.d.ts +9 -0
- package/dist/platform/detector.d.ts.map +1 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/node/crypto.d.ts +3 -0
- package/dist/platform/node/crypto.d.ts.map +1 -0
- package/dist/platform/node/fs.d.ts +3 -0
- package/dist/platform/node/fs.d.ts.map +1 -0
- package/dist/platform/node/http.d.ts +3 -0
- package/dist/platform/node/http.d.ts.map +1 -0
- package/dist/platform/node/index.d.ts +3 -0
- package/dist/platform/node/index.d.ts.map +1 -0
- package/dist/platform/node/parser.d.ts +3 -0
- package/dist/platform/node/parser.d.ts.map +1 -0
- package/dist/platform/node/process.d.ts +3 -0
- package/dist/platform/node/process.d.ts.map +1 -0
- package/dist/platform/runtime.d.ts +14 -0
- package/dist/platform/runtime.d.ts.map +1 -0
- package/dist/platform/types.d.ts +139 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -1
- package/dist/rag/service.d.ts.map +1 -1
- package/dist/request/response.d.ts +3 -1
- package/dist/request/response.d.ts.map +1 -1
- package/dist/security/guards/execution-context.d.ts +2 -2
- package/dist/security/guards/execution-context.d.ts.map +1 -1
- package/dist/security/guards/types.d.ts +2 -2
- package/dist/security/guards/types.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/websocket/registry.d.ts +4 -4
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/deployment.md +31 -7
- package/docs/idle-timeout.md +6 -4
- package/docs/migration.md +43 -0
- package/docs/platform.md +299 -0
- package/docs/testing.md +60 -0
- package/docs/zh/deployment.md +30 -7
- package/docs/zh/idle-timeout.md +6 -4
- package/docs/zh/migration.md +42 -0
- package/docs/zh/platform.md +299 -0
- package/docs/zh/testing.md +60 -0
- package/package.json +24 -7
- package/src/auth/jwt.ts +4 -3
- package/src/config/service.ts +7 -6
- package/src/core/application.ts +19 -1
- package/src/core/cluster.ts +16 -14
- package/src/core/server.ts +48 -35
- package/src/dashboard/controller.ts +3 -2
- package/src/database/connection-manager.ts +19 -12
- package/src/database/connection-pool.ts +33 -20
- package/src/database/database-module.ts +1 -1
- package/src/database/db-proxy.ts +2 -2
- package/src/database/orm/transaction-manager.ts +1 -1
- package/src/database/service.ts +21 -5
- package/src/database/sql-manager.ts +48 -13
- package/src/database/sqlite-adapter.ts +54 -13
- package/src/debug/recorder.ts +4 -3
- package/src/files/static-middleware.ts +3 -2
- package/src/files/storage.ts +2 -1
- package/src/index.ts +13 -0
- package/src/middleware/builtin/static-file.ts +8 -5
- package/src/platform/bun/crypto.ts +30 -0
- package/src/platform/bun/fs.ts +52 -0
- package/src/platform/bun/http.ts +106 -0
- package/src/platform/bun/index.ts +17 -0
- package/src/platform/bun/parser.ts +19 -0
- package/src/platform/bun/process.ts +37 -0
- package/src/platform/detector.ts +36 -0
- package/src/platform/index.ts +20 -0
- package/src/platform/node/crypto.ts +40 -0
- package/src/platform/node/fs.ts +115 -0
- package/src/platform/node/http.ts +196 -0
- package/src/platform/node/index.ts +17 -0
- package/src/platform/node/parser.ts +34 -0
- package/src/platform/node/process.ts +51 -0
- package/src/platform/runtime.ts +50 -0
- package/src/platform/types.ts +150 -0
- package/src/prompt/stores/file-store.ts +6 -5
- package/src/rag/service.ts +2 -1
- package/src/request/response.ts +7 -4
- package/src/security/guards/execution-context.ts +4 -4
- package/src/security/guards/types.ts +2 -2
- package/src/swagger/generator.ts +2 -1
- package/src/websocket/registry.ts +6 -7
- package/tests/controller/path-combination.test.ts +196 -2
- package/tests/files/static-middleware.test.ts +5 -2
- package/tests/middleware/static-file.test.ts +5 -2
- package/tests/platform/bun/crypto.test.ts +8 -0
- package/tests/platform/bun/database.test.ts +8 -0
- package/tests/platform/bun/fs.test.ts +8 -0
- package/tests/platform/bun/parser.test.ts +8 -0
- package/tests/platform/bun/process.test.ts +8 -0
- package/tests/platform/bun/websocket.test.ts +8 -0
- package/tests/platform/detector.test.ts +57 -0
- package/tests/platform/node/build-smoke.test.ts +92 -0
- package/tests/platform/node/crypto.test.ts +9 -0
- package/tests/platform/node/database.test.ts +9 -0
- package/tests/platform/node/fs.test.ts +9 -0
- package/tests/platform/node/parser.test.ts +9 -0
- package/tests/platform/node/process.test.ts +9 -0
- package/tests/platform/node/websocket.test.ts +9 -0
- package/tests/platform/shared/crypto.cases.ts +49 -0
- package/tests/platform/shared/database.cases.ts +43 -0
- package/tests/platform/shared/fs.cases.ts +82 -0
- package/tests/platform/shared/parser.cases.ts +55 -0
- package/tests/platform/shared/process.cases.ts +26 -0
- package/tests/platform/shared/suite.ts +33 -0
- package/tests/platform/shared/websocket.cases.ts +61 -0
- package/tests/request/response.test.ts +5 -2
- package/tests/router/router-extended.test.ts +53 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { TestSuite } from './suite';
|
|
4
|
+
import type { PlatformEngine } from '../../../src/platform/types';
|
|
5
|
+
import { SqliteManager } from '../../../src/database/sqlite-adapter';
|
|
6
|
+
|
|
7
|
+
export function runDatabaseCases(suite: TestSuite, engine: PlatformEngine): void {
|
|
8
|
+
const { test, expect } = suite;
|
|
9
|
+
|
|
10
|
+
// @vscode/sqlite3 is a native Node.js addon; skip when Node-platform tests run under Bun runtime
|
|
11
|
+
const skipNodeSqlite = engine === 'node' && typeof (globalThis as any).Bun !== 'undefined';
|
|
12
|
+
|
|
13
|
+
test('SqliteAdapter: create table, insert and query', async () => {
|
|
14
|
+
if (skipNodeSqlite) {
|
|
15
|
+
// @vscode/sqlite3 is not available in Bun — this test must run under Node.js
|
|
16
|
+
console.log('[skip] @vscode/sqlite3 not available in Bun runtime; run with vitest for Node platform');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const dbPath = join(tmpdir(), `platform-db-test-${engine}-${Date.now()}.db`);
|
|
20
|
+
const manager = new SqliteManager();
|
|
21
|
+
const adapter = manager.getOrCreate('test', { database: dbPath, wal: false });
|
|
22
|
+
|
|
23
|
+
await adapter.query('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
|
|
24
|
+
await adapter.query("INSERT INTO users (name) VALUES ('Alice')");
|
|
25
|
+
const rows = await adapter.query<{ id: number; name: string }>('SELECT * FROM users');
|
|
26
|
+
|
|
27
|
+
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
28
|
+
expect(rows[0]!.name).toBe('Alice');
|
|
29
|
+
|
|
30
|
+
manager.destroy('test');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('SqliteManager.getDefault() throws when not initialized', () => {
|
|
34
|
+
const manager = new SqliteManager();
|
|
35
|
+
let threw = false;
|
|
36
|
+
try {
|
|
37
|
+
manager.getDefault();
|
|
38
|
+
} catch {
|
|
39
|
+
threw = true;
|
|
40
|
+
}
|
|
41
|
+
expect(threw).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
4
|
+
import type { TestSuite } from './suite';
|
|
5
|
+
import type { IFsAdapter } from '../../../src/platform/types';
|
|
6
|
+
|
|
7
|
+
export function runFsCases(suite: TestSuite, getAdapter: () => IFsAdapter): void {
|
|
8
|
+
const { test, expect, beforeEach } = suite;
|
|
9
|
+
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = join(tmpdir(), `platform-fs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
14
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('write and read text', async () => {
|
|
18
|
+
const adapter = getAdapter();
|
|
19
|
+
const filePath = join(tmpDir, 'hello.txt');
|
|
20
|
+
await adapter.write(filePath, 'hello world');
|
|
21
|
+
const content = await adapter.file(filePath).text();
|
|
22
|
+
expect(content).toBe('hello world');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('write and read bytes', async () => {
|
|
26
|
+
const adapter = getAdapter();
|
|
27
|
+
const filePath = join(tmpDir, 'bytes.bin');
|
|
28
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
29
|
+
await adapter.write(filePath, data);
|
|
30
|
+
const result = await adapter.file(filePath).bytes();
|
|
31
|
+
expect(result[0]).toBe(1);
|
|
32
|
+
expect(result[4]).toBe(5);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('file.exists() returns true for existing file', async () => {
|
|
36
|
+
const adapter = getAdapter();
|
|
37
|
+
const filePath = join(tmpDir, 'exists.txt');
|
|
38
|
+
await adapter.write(filePath, 'content');
|
|
39
|
+
const exists = await adapter.file(filePath).exists();
|
|
40
|
+
expect(exists).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('file.exists() returns false for missing file', async () => {
|
|
44
|
+
const adapter = getAdapter();
|
|
45
|
+
const filePath = join(tmpDir, 'does-not-exist.txt');
|
|
46
|
+
const exists = await adapter.file(filePath).exists();
|
|
47
|
+
expect(exists).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('file.type returns MIME type', async () => {
|
|
51
|
+
const adapter = getAdapter();
|
|
52
|
+
const filePath = join(tmpDir, 'image.png');
|
|
53
|
+
await adapter.write(filePath, new Uint8Array([0x89, 0x50, 0x4e, 0x47]));
|
|
54
|
+
const mime = adapter.file(filePath).type;
|
|
55
|
+
expect(mime).toContain('png');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('stream() produces readable stream with correct content', async () => {
|
|
59
|
+
const adapter = getAdapter();
|
|
60
|
+
const filePath = join(tmpDir, 'stream.txt');
|
|
61
|
+
await adapter.write(filePath, 'stream content');
|
|
62
|
+
const stream = adapter.file(filePath).stream();
|
|
63
|
+
const reader = stream.getReader();
|
|
64
|
+
const chunks: Uint8Array[] = [];
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
chunks.push(value);
|
|
69
|
+
}
|
|
70
|
+
const combined = Buffer.concat(chunks).toString('utf-8');
|
|
71
|
+
expect(combined).toBe('stream content');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('glob finds matching files', async () => {
|
|
75
|
+
const adapter = getAdapter();
|
|
76
|
+
await adapter.write(join(tmpDir, 'a.json'), '{}');
|
|
77
|
+
await adapter.write(join(tmpDir, 'b.json'), '{}');
|
|
78
|
+
await adapter.write(join(tmpDir, 'c.txt'), 'text');
|
|
79
|
+
const files = adapter.glob('*.json', tmpDir);
|
|
80
|
+
expect(files.length).toBeGreaterThanOrEqual(2);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { TestSuite } from './suite';
|
|
2
|
+
import type { IParserAdapter } from '../../../src/platform/types';
|
|
3
|
+
|
|
4
|
+
export function runParserCases(suite: TestSuite, getAdapter: () => IParserAdapter): void {
|
|
5
|
+
const { test, expect } = suite;
|
|
6
|
+
|
|
7
|
+
test('parseJSONC parses standard JSON', () => {
|
|
8
|
+
const adapter = getAdapter();
|
|
9
|
+
const result = adapter.parseJSONC('{"a": 1, "b": "hello"}') as Record<string, unknown>;
|
|
10
|
+
expect(result['a']).toBe(1);
|
|
11
|
+
expect(result['b']).toBe('hello');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('parseJSONC ignores line comments', () => {
|
|
15
|
+
const adapter = getAdapter();
|
|
16
|
+
const result = adapter.parseJSONC('{\n// comment\n"a": 1}') as Record<string, unknown>;
|
|
17
|
+
expect(result['a']).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('parseJSON5 parses JSON5 with trailing commas', () => {
|
|
21
|
+
const adapter = getAdapter();
|
|
22
|
+
const result = adapter.parseJSON5('{a: 1, b: "hello",}') as Record<string, unknown>;
|
|
23
|
+
expect(result['a']).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('parseJSONL parses multiple JSON lines', () => {
|
|
27
|
+
const adapter = getAdapter();
|
|
28
|
+
const content = '{"id":1}\n{"id":2}\n{"id":3}';
|
|
29
|
+
const records = adapter.parseJSONL(content);
|
|
30
|
+
expect(records.length).toBe(3);
|
|
31
|
+
expect((records[0] as Record<string, unknown>)['id']).toBe(1);
|
|
32
|
+
expect((records[2] as Record<string, unknown>)['id']).toBe(3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('parseJSONL ignores empty lines', () => {
|
|
36
|
+
const adapter = getAdapter();
|
|
37
|
+
const content = '{"id":1}\n\n{"id":2}\n';
|
|
38
|
+
const records = adapter.parseJSONL(content);
|
|
39
|
+
expect(records.length).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('renderMarkdown converts h1 heading', () => {
|
|
43
|
+
const adapter = getAdapter();
|
|
44
|
+
const html = adapter.renderMarkdown('# Hello World');
|
|
45
|
+
expect(html).toContain('<h1');
|
|
46
|
+
expect(html).toContain('Hello World');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('renderMarkdown converts bold text', () => {
|
|
50
|
+
const adapter = getAdapter();
|
|
51
|
+
const html = adapter.renderMarkdown('**bold**');
|
|
52
|
+
expect(html).toContain('<strong>');
|
|
53
|
+
expect(html).toContain('bold');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TestSuite } from './suite';
|
|
2
|
+
import type { IProcessAdapter } from '../../../src/platform/types';
|
|
3
|
+
|
|
4
|
+
export function runProcessCases(suite: TestSuite, getAdapter: () => IProcessAdapter): void {
|
|
5
|
+
const { test, expect } = suite;
|
|
6
|
+
|
|
7
|
+
test('sleep waits at least the specified time', async () => {
|
|
8
|
+
const adapter = getAdapter();
|
|
9
|
+
const start = Date.now();
|
|
10
|
+
await adapter.sleep(100);
|
|
11
|
+
const elapsed = Date.now() - start;
|
|
12
|
+
expect(elapsed).toBeGreaterThanOrEqual(90);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('spawn runs a process and returns exit code 0', async () => {
|
|
16
|
+
const adapter = getAdapter();
|
|
17
|
+
const child = adapter.spawn({
|
|
18
|
+
cmd: ['node', '--version'],
|
|
19
|
+
stdout: 'pipe',
|
|
20
|
+
stderr: 'ignore',
|
|
21
|
+
});
|
|
22
|
+
expect(child.pid).toBeGreaterThan(0);
|
|
23
|
+
const exitCode = await child.exited;
|
|
24
|
+
expect(exitCode).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 框架无关的测试套件接口
|
|
3
|
+
* 让 shared cases 文件在 bun:test 和 vitest 中都能运行,无需任何适配层
|
|
4
|
+
*/
|
|
5
|
+
export interface TestSuite {
|
|
6
|
+
test: (name: string, fn: () => void | Promise<void>) => void;
|
|
7
|
+
expect: (actual: unknown) => {
|
|
8
|
+
toBe(e: unknown): void;
|
|
9
|
+
toEqual(e: unknown): void;
|
|
10
|
+
toBeTruthy(): void;
|
|
11
|
+
toBeFalsy(): void;
|
|
12
|
+
toContain(e: unknown): void;
|
|
13
|
+
toBeGreaterThan(e: number): void;
|
|
14
|
+
toBeGreaterThanOrEqual(e: number): void;
|
|
15
|
+
toBeLessThan(e: number): void;
|
|
16
|
+
toBeLessThanOrEqual(e: number): void;
|
|
17
|
+
toBeInstanceOf(e: unknown): void;
|
|
18
|
+
toHaveLength(e: number): void;
|
|
19
|
+
not: {
|
|
20
|
+
toBe(e: unknown): void;
|
|
21
|
+
toBeNull(): void;
|
|
22
|
+
toBeUndefined(): void;
|
|
23
|
+
toThrow(): void;
|
|
24
|
+
};
|
|
25
|
+
resolves: {
|
|
26
|
+
toBeTruthy(): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
rejects: {
|
|
29
|
+
toThrow(): Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
beforeEach: (fn: () => void | Promise<void>) => void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { TestSuite } from './suite';
|
|
2
|
+
import type { IPlatform } from '../../../src/platform/types';
|
|
3
|
+
|
|
4
|
+
export function runWebSocketCases(suite: TestSuite, getPlatform: () => IPlatform): void {
|
|
5
|
+
const { test, expect } = suite;
|
|
6
|
+
|
|
7
|
+
test('WebSocket server accepts connection and receives message', async () => {
|
|
8
|
+
const platform = getPlatform();
|
|
9
|
+
const messages: string[] = [];
|
|
10
|
+
let openCalled = false;
|
|
11
|
+
|
|
12
|
+
// Use a promise to track the server-side close event
|
|
13
|
+
let resolveServerClose!: () => void;
|
|
14
|
+
const serverClosePromise = new Promise<void>((r) => { resolveServerClose = r; });
|
|
15
|
+
|
|
16
|
+
const server = await platform.http.serve({
|
|
17
|
+
port: 0,
|
|
18
|
+
fetch: (req, handle) => {
|
|
19
|
+
const upgraded = handle.upgrade?.(req, { data: {} });
|
|
20
|
+
if (upgraded) return undefined as unknown as Response;
|
|
21
|
+
return new Response('not ws', { status: 400 });
|
|
22
|
+
},
|
|
23
|
+
websocket: {
|
|
24
|
+
open: (_ws) => { openCalled = true; },
|
|
25
|
+
message: (ws, msg) => {
|
|
26
|
+
messages.push(msg.toString());
|
|
27
|
+
ws.send('echo: ' + msg.toString());
|
|
28
|
+
},
|
|
29
|
+
close: (_ws) => { resolveServerClose(); },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const port = server.port;
|
|
34
|
+
|
|
35
|
+
// Connect using native WebSocket (available in both Bun and Node 22+)
|
|
36
|
+
await new Promise<void>((resolve, reject) => {
|
|
37
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
38
|
+
ws.addEventListener('open', () => {
|
|
39
|
+
ws.send('hello');
|
|
40
|
+
});
|
|
41
|
+
ws.addEventListener('message', (e) => {
|
|
42
|
+
if (e.data === 'echo: hello') {
|
|
43
|
+
ws.close();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
ws.addEventListener('close', () => {
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
ws.addEventListener('error', reject);
|
|
50
|
+
setTimeout(reject, 5000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Wait for server-side close handler to fire before asserting
|
|
54
|
+
await serverClosePromise;
|
|
55
|
+
|
|
56
|
+
server.stop();
|
|
57
|
+
|
|
58
|
+
expect(openCalled).toBe(true);
|
|
59
|
+
expect(messages).toContain('hello');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { ResponseBuilder } from '../../src/request/response';
|
|
6
|
+
import { initRuntime } from '../../src/platform/runtime';
|
|
7
|
+
|
|
8
|
+
initRuntime('bun');
|
|
6
9
|
|
|
7
10
|
describe('ResponseBuilder', () => {
|
|
8
11
|
let tmpDir: string;
|
|
@@ -71,7 +74,7 @@ describe('ResponseBuilder', () => {
|
|
|
71
74
|
|
|
72
75
|
test('should create file response from path', async () => {
|
|
73
76
|
const filePath = join(tmpDir, 'hello.txt');
|
|
74
|
-
await
|
|
77
|
+
await writeFile(filePath, 'file-content', 'utf-8');
|
|
75
78
|
|
|
76
79
|
const response = ResponseBuilder.file(filePath, {
|
|
77
80
|
fileName: 'download.txt',
|
|
@@ -94,6 +94,42 @@ describe('Router', () => {
|
|
|
94
94
|
const route = router.findRoute('POST', '/users');
|
|
95
95
|
expect(route).toBeUndefined();
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
test('should distinguish GET POST PUT DELETE PATCH on the same dynamic route', () => {
|
|
99
|
+
router.get('/api/:id', async () => new Response('get'));
|
|
100
|
+
router.post('/api/:id', async () => new Response('post'));
|
|
101
|
+
router.put('/api/:id', async () => new Response('put'));
|
|
102
|
+
router.delete('/api/:id', async () => new Response('delete'));
|
|
103
|
+
router.patch('/api/:id', async () => new Response('patch'));
|
|
104
|
+
|
|
105
|
+
const getRoute = router.findRoute('GET', '/api/123');
|
|
106
|
+
const postRoute = router.findRoute('POST', '/api/123');
|
|
107
|
+
const putRoute = router.findRoute('PUT', '/api/123');
|
|
108
|
+
const deleteRoute = router.findRoute('DELETE', '/api/123');
|
|
109
|
+
const patchRoute = router.findRoute('PATCH', '/api/123');
|
|
110
|
+
|
|
111
|
+
expect(getRoute).toBeDefined();
|
|
112
|
+
expect(postRoute).toBeDefined();
|
|
113
|
+
expect(putRoute).toBeDefined();
|
|
114
|
+
expect(deleteRoute).toBeDefined();
|
|
115
|
+
expect(patchRoute).toBeDefined();
|
|
116
|
+
|
|
117
|
+
expect(getRoute?.method).toBe('GET');
|
|
118
|
+
expect(postRoute?.method).toBe('POST');
|
|
119
|
+
expect(putRoute?.method).toBe('PUT');
|
|
120
|
+
expect(deleteRoute?.method).toBe('DELETE');
|
|
121
|
+
expect(patchRoute?.method).toBe('PATCH');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should not cross-match methods on same dynamic route', () => {
|
|
125
|
+
router.get('/api/:id', async () => new Response('get'));
|
|
126
|
+
router.post('/api/:id', async () => new Response('post'));
|
|
127
|
+
|
|
128
|
+
expect(router.findRoute('GET', '/api/123')).toBeDefined();
|
|
129
|
+
expect(router.findRoute('POST', '/api/123')).toBeDefined();
|
|
130
|
+
expect(router.findRoute('PUT', '/api/123')).toBeUndefined();
|
|
131
|
+
expect(router.findRoute('DELETE', '/api/123')).toBeUndefined();
|
|
132
|
+
});
|
|
97
133
|
});
|
|
98
134
|
|
|
99
135
|
describe('findRouteWithMatch', () => {
|
|
@@ -178,6 +214,23 @@ describe('Router', () => {
|
|
|
178
214
|
expect((context as any).routeHandler.controller).toBe(MyController);
|
|
179
215
|
expect((context as any).routeHandler.method).toBe('testMethod');
|
|
180
216
|
});
|
|
217
|
+
|
|
218
|
+
test('should route each HTTP method to its own handler on the same dynamic path /api/:id', async () => {
|
|
219
|
+
router.get('/api/:id', async () => new Response('get'));
|
|
220
|
+
router.post('/api/:id', async () => new Response('post'));
|
|
221
|
+
router.put('/api/:id', async () => new Response('put'));
|
|
222
|
+
router.delete('/api/:id', async () => new Response('delete'));
|
|
223
|
+
router.patch('/api/:id', async () => new Response('patch'));
|
|
224
|
+
|
|
225
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
|
|
226
|
+
for (const method of methods) {
|
|
227
|
+
const request = new Request('http://localhost/api/123', { method });
|
|
228
|
+
const context = new Context(request, new Container());
|
|
229
|
+
const response = await router.handle(context);
|
|
230
|
+
expect(response).toBeDefined();
|
|
231
|
+
expect(await response?.text()).toBe(method.toLowerCase());
|
|
232
|
+
}
|
|
233
|
+
});
|
|
181
234
|
});
|
|
182
235
|
|
|
183
236
|
describe('preHandle', () => {
|