@iskra-bun/testing-kit 0.1.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/CHANGELOG.md +14 -0
- package/README.md +88 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/index.ts +8 -0
- package/src/mock-driver.ts +79 -0
- package/src/mock-logger.ts +77 -0
- package/src/test-app.ts +36 -0
- package/src/test-server.ts +81 -0
- package/src/with-temp-dir.ts +22 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @iskra-bun/testing-kit
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f9654df: Initial public release. Test utilities for Iskra apps: `createTestApp`, `createMockLogger`, `createMockDriver`, `withTempDir`, and `createTestServer`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [f9654df]
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- Updated dependencies [f9654df]
|
|
14
|
+
- @iskra-bun/core@0.1.1
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @iskra-bun/testing-kit
|
|
2
|
+
|
|
3
|
+
Utilidades compartidas de testing para paquetes de Iskra — elimina el bootstrapping duplicado de App/Driver/Logger en cada suite de tests.
|
|
4
|
+
|
|
5
|
+
## Inicio Rapido
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
createTestApp,
|
|
10
|
+
createMockLogger,
|
|
11
|
+
createMockDriver,
|
|
12
|
+
withTempDir,
|
|
13
|
+
createTestServer,
|
|
14
|
+
} from '@iskra-bun/testing-kit';
|
|
15
|
+
|
|
16
|
+
// App real con logger silencioso
|
|
17
|
+
const app = createTestApp();
|
|
18
|
+
|
|
19
|
+
// Driver mock con registro de llamadas
|
|
20
|
+
const driver = createMockDriver();
|
|
21
|
+
app.register(driver);
|
|
22
|
+
await app.start();
|
|
23
|
+
// ['init', 'start']
|
|
24
|
+
console.log(driver.calls);
|
|
25
|
+
|
|
26
|
+
await app.stop();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### `createTestApp(overrides?)`
|
|
32
|
+
|
|
33
|
+
Devuelve una instancia real de `App` configurada para tests: logger a nivel `error`, OTel desactivado, nombre `TestApp` por defecto. Se puede iniciar y parar normalmente.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const app = createTestApp({ name: 'MiTest' });
|
|
37
|
+
app.register(myDriver);
|
|
38
|
+
await app.start();
|
|
39
|
+
await app.stop();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `createMockLogger()`
|
|
43
|
+
|
|
44
|
+
Devuelve un objeto que implementa la interfaz `Logger` de core y captura todas las llamadas en arrays para aserciones.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const logger = createMockLogger();
|
|
48
|
+
logger.info('mensaje');
|
|
49
|
+
expect(logger.logs.info[0].args[0]).toBe('mensaje');
|
|
50
|
+
logger.reset(); // borra todos los registros
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `createMockDriver(name?, hooks?)`
|
|
54
|
+
|
|
55
|
+
Devuelve un `Driver` que registra las llamadas al ciclo de vida (`init`, `start`, `stop`) y su orden. Los hooks opcionales permiten inyectar comportamiento o lanzar errores.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const driver = createMockDriver('mi-driver', {
|
|
59
|
+
stop: async () => { throw new Error('fallo intencional'); },
|
|
60
|
+
});
|
|
61
|
+
// driver.calls => ['init', 'start', 'stop']
|
|
62
|
+
// driver.initialized / driver.started / driver.stopped => boolean
|
|
63
|
+
driver.reset();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `withTempDir(fn)`
|
|
67
|
+
|
|
68
|
+
Crea un directorio temporal, ejecuta `fn(dir)` y lo elimina siempre al final, incluso si `fn` lanza.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
await withTempDir(async (dir) => {
|
|
72
|
+
await Bun.write(`${dir}/data.json`, '{}');
|
|
73
|
+
// dir se limpia al salir
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `createTestServer(handler)`
|
|
78
|
+
|
|
79
|
+
Envuelve cualquier objeto con `.request()` (compatible con Hono sin importarlo) en un cliente de test con métodos HTTP de conveniencia.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const client = createTestServer(honoApp);
|
|
83
|
+
const res = await client.get('/health');
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
|
|
86
|
+
const created = await client.post('/users', { name: 'Ana' });
|
|
87
|
+
expect(created.status).toBe(201);
|
|
88
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AppConfig, App, Logger, Driver } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a real App instance configured for testing:
|
|
5
|
+
* - Logger level defaults to 'error' so tests stay silent
|
|
6
|
+
* - OTel is disabled to avoid side effects
|
|
7
|
+
* - Config name defaults to 'TestApp'
|
|
8
|
+
* - Accepts optional overrides merged shallowly over the defaults
|
|
9
|
+
*
|
|
10
|
+
* The returned app is a genuine App so start()/stop()/register() all work
|
|
11
|
+
* exactly as in production.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const app = createTestApp();
|
|
15
|
+
* app.register(myDriver);
|
|
16
|
+
* await app.start();
|
|
17
|
+
* await app.stop();
|
|
18
|
+
*/
|
|
19
|
+
declare function createTestApp(overrides?: Partial<AppConfig>): App;
|
|
20
|
+
|
|
21
|
+
interface MockLogEntry {
|
|
22
|
+
args: unknown[];
|
|
23
|
+
}
|
|
24
|
+
interface MockLoggerLogs {
|
|
25
|
+
trace: MockLogEntry[];
|
|
26
|
+
debug: MockLogEntry[];
|
|
27
|
+
info: MockLogEntry[];
|
|
28
|
+
warn: MockLogEntry[];
|
|
29
|
+
error: MockLogEntry[];
|
|
30
|
+
fatal: MockLogEntry[];
|
|
31
|
+
}
|
|
32
|
+
interface MockLogger extends Logger {
|
|
33
|
+
logs: MockLoggerLogs;
|
|
34
|
+
reset(): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a Logger-compatible mock that captures all log calls into arrays
|
|
38
|
+
* for assertion in tests. No output is written to stdout/stderr.
|
|
39
|
+
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* const logger = createMockLogger();
|
|
42
|
+
* logger.info('hello');
|
|
43
|
+
* expect(logger.logs.info).toHaveLength(1);
|
|
44
|
+
* logger.reset();
|
|
45
|
+
*/
|
|
46
|
+
declare function createMockLogger(): MockLogger;
|
|
47
|
+
|
|
48
|
+
type LifecycleCall = 'init' | 'start' | 'stop';
|
|
49
|
+
interface MockDriverHooks {
|
|
50
|
+
init?: (app: App) => Promise<void> | void;
|
|
51
|
+
start?: () => Promise<void> | void;
|
|
52
|
+
stop?: () => Promise<void> | void;
|
|
53
|
+
}
|
|
54
|
+
interface MockDriver extends Driver {
|
|
55
|
+
/** Ordered record of which lifecycle methods were called */
|
|
56
|
+
calls: LifecycleCall[];
|
|
57
|
+
/** Whether init() was called */
|
|
58
|
+
initialized: boolean;
|
|
59
|
+
/** Whether start() was called */
|
|
60
|
+
started: boolean;
|
|
61
|
+
/** Whether stop() was called */
|
|
62
|
+
stopped: boolean;
|
|
63
|
+
/** Reset all recorded state */
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates a Driver-compatible mock that records every lifecycle call and its
|
|
68
|
+
* order. Optional hooks let tests inject arbitrary behavior or throw errors.
|
|
69
|
+
*
|
|
70
|
+
* Usage:
|
|
71
|
+
* const driver = createMockDriver({ stop: async () => { throw new Error('boom'); } });
|
|
72
|
+
* await app.register(driver).start();
|
|
73
|
+
* expect(driver.calls).toEqual(['init', 'start']);
|
|
74
|
+
*/
|
|
75
|
+
declare function createMockDriver(name?: string, hooks?: MockDriverHooks): MockDriver;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a temporary directory, invokes `fn` with its path, and always
|
|
79
|
+
* removes it afterwards — even when `fn` throws.
|
|
80
|
+
*
|
|
81
|
+
* Usage:
|
|
82
|
+
* await withTempDir(async (dir) => {
|
|
83
|
+
* await Bun.write(join(dir, 'file.txt'), 'hello');
|
|
84
|
+
* // dir is cleaned up automatically on return or throw
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
declare function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Minimal structural interface for objects that accept a fetch-style request.
|
|
91
|
+
* Matches Hono's `app.request()` signature without importing Hono.
|
|
92
|
+
*/
|
|
93
|
+
interface RequestHandler {
|
|
94
|
+
request(input: string | Request | URL, requestInit?: RequestInit, env?: unknown, executionCtx?: unknown): Response | Promise<Response>;
|
|
95
|
+
}
|
|
96
|
+
interface TestServerClient {
|
|
97
|
+
get(path: string, init?: RequestInit): Promise<Response>;
|
|
98
|
+
post(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
99
|
+
put(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
100
|
+
patch(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
101
|
+
delete(path: string, init?: RequestInit): Promise<Response>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wraps any object with a `.request()` method (structurally typed — no Hono
|
|
105
|
+
* import required) in a small fetch-style client for testing HTTP handlers.
|
|
106
|
+
*
|
|
107
|
+
* Usage:
|
|
108
|
+
* const client = createTestServer(honoApp);
|
|
109
|
+
* const res = await client.get('/health');
|
|
110
|
+
* expect(res.status).toBe(200);
|
|
111
|
+
*/
|
|
112
|
+
declare function createTestServer(handler: RequestHandler): TestServerClient;
|
|
113
|
+
|
|
114
|
+
export { type LifecycleCall, type MockDriver, type MockDriverHooks, type MockLogEntry, type MockLogger, type MockLoggerLogs, type RequestHandler, type TestServerClient, createMockDriver, createMockLogger, createTestApp, createTestServer, withTempDir };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/test-app.ts
|
|
2
|
+
import { App } from "@iskra-bun/core";
|
|
3
|
+
|
|
4
|
+
// src/mock-logger.ts
|
|
5
|
+
function makeRecorder(logs) {
|
|
6
|
+
return (...args) => {
|
|
7
|
+
logs.push({ args });
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function createMockLogger() {
|
|
11
|
+
const logs = {
|
|
12
|
+
trace: [],
|
|
13
|
+
debug: [],
|
|
14
|
+
info: [],
|
|
15
|
+
warn: [],
|
|
16
|
+
error: [],
|
|
17
|
+
fatal: []
|
|
18
|
+
};
|
|
19
|
+
const mock = {
|
|
20
|
+
logs,
|
|
21
|
+
reset() {
|
|
22
|
+
logs.trace.length = 0;
|
|
23
|
+
logs.debug.length = 0;
|
|
24
|
+
logs.info.length = 0;
|
|
25
|
+
logs.warn.length = 0;
|
|
26
|
+
logs.error.length = 0;
|
|
27
|
+
logs.fatal.length = 0;
|
|
28
|
+
},
|
|
29
|
+
trace: makeRecorder(logs.trace),
|
|
30
|
+
debug: makeRecorder(logs.debug),
|
|
31
|
+
info: makeRecorder(logs.info),
|
|
32
|
+
warn: makeRecorder(logs.warn),
|
|
33
|
+
error: makeRecorder(logs.error),
|
|
34
|
+
fatal: makeRecorder(logs.fatal),
|
|
35
|
+
silent: () => {
|
|
36
|
+
},
|
|
37
|
+
child(_bindings, _options) {
|
|
38
|
+
return mock;
|
|
39
|
+
},
|
|
40
|
+
// Minimal pino.Logger shape — fields tests never touch
|
|
41
|
+
level: "error",
|
|
42
|
+
isLevelEnabled: (_level) => false,
|
|
43
|
+
setBindings: (_bindings) => {
|
|
44
|
+
},
|
|
45
|
+
flush: (_cb) => {
|
|
46
|
+
},
|
|
47
|
+
bindings: () => ({})
|
|
48
|
+
};
|
|
49
|
+
return mock;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/test-app.ts
|
|
53
|
+
function createTestApp(overrides) {
|
|
54
|
+
const config = {
|
|
55
|
+
name: "TestApp",
|
|
56
|
+
logger: { level: "error" },
|
|
57
|
+
...overrides
|
|
58
|
+
};
|
|
59
|
+
const app = new App(config);
|
|
60
|
+
app.logger = createMockLogger();
|
|
61
|
+
return app;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/mock-driver.ts
|
|
65
|
+
function createMockDriver(name = "mock-driver", hooks = {}) {
|
|
66
|
+
const calls = [];
|
|
67
|
+
const driver = {
|
|
68
|
+
name,
|
|
69
|
+
calls,
|
|
70
|
+
initialized: false,
|
|
71
|
+
started: false,
|
|
72
|
+
stopped: false,
|
|
73
|
+
reset() {
|
|
74
|
+
calls.length = 0;
|
|
75
|
+
driver.initialized = false;
|
|
76
|
+
driver.started = false;
|
|
77
|
+
driver.stopped = false;
|
|
78
|
+
},
|
|
79
|
+
async init(app) {
|
|
80
|
+
calls.push("init");
|
|
81
|
+
driver.initialized = true;
|
|
82
|
+
if (hooks.init) {
|
|
83
|
+
await hooks.init(app);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
async start() {
|
|
87
|
+
calls.push("start");
|
|
88
|
+
driver.started = true;
|
|
89
|
+
if (hooks.start) {
|
|
90
|
+
await hooks.start();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async stop() {
|
|
94
|
+
calls.push("stop");
|
|
95
|
+
driver.stopped = true;
|
|
96
|
+
if (hooks.stop) {
|
|
97
|
+
await hooks.stop();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return driver;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/with-temp-dir.ts
|
|
105
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
106
|
+
import { tmpdir } from "os";
|
|
107
|
+
import { join } from "path";
|
|
108
|
+
async function withTempDir(fn) {
|
|
109
|
+
const dir = await mkdtemp(join(tmpdir(), "iskra-test-"));
|
|
110
|
+
try {
|
|
111
|
+
return await fn(dir);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/test-server.ts
|
|
118
|
+
function buildJsonInit(body, base = {}) {
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
headers: {
|
|
122
|
+
"content-type": "application/json",
|
|
123
|
+
...base.headers
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(body)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function createTestServer(handler) {
|
|
129
|
+
return {
|
|
130
|
+
get(path, init) {
|
|
131
|
+
return Promise.resolve(handler.request(path, { method: "GET", ...init }));
|
|
132
|
+
},
|
|
133
|
+
post(path, body, init) {
|
|
134
|
+
return Promise.resolve(
|
|
135
|
+
handler.request(
|
|
136
|
+
path,
|
|
137
|
+
body !== void 0 ? { method: "POST", ...buildJsonInit(body, init) } : { method: "POST", ...init }
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
put(path, body, init) {
|
|
142
|
+
return Promise.resolve(
|
|
143
|
+
handler.request(
|
|
144
|
+
path,
|
|
145
|
+
body !== void 0 ? { method: "PUT", ...buildJsonInit(body, init) } : { method: "PUT", ...init }
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
patch(path, body, init) {
|
|
150
|
+
return Promise.resolve(
|
|
151
|
+
handler.request(
|
|
152
|
+
path,
|
|
153
|
+
body !== void 0 ? { method: "PATCH", ...buildJsonInit(body, init) } : { method: "PATCH", ...init }
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
delete(path, init) {
|
|
158
|
+
return Promise.resolve(handler.request(path, { method: "DELETE", ...init }));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
createMockDriver,
|
|
164
|
+
createMockLogger,
|
|
165
|
+
createTestApp,
|
|
166
|
+
createTestServer,
|
|
167
|
+
withTempDir
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/test-app.ts","../src/mock-logger.ts","../src/mock-driver.ts","../src/with-temp-dir.ts","../src/test-server.ts"],"sourcesContent":["import { App } from '@iskra-bun/core';\nimport type { AppConfig } from '@iskra-bun/core';\nimport type { Logger } from '@iskra-bun/core';\nimport { createMockLogger } from './mock-logger';\n\n/**\n * Creates a real App instance configured for testing:\n * - Logger level defaults to 'error' so tests stay silent\n * - OTel is disabled to avoid side effects\n * - Config name defaults to 'TestApp'\n * - Accepts optional overrides merged shallowly over the defaults\n *\n * The returned app is a genuine App so start()/stop()/register() all work\n * exactly as in production.\n *\n * Usage:\n * const app = createTestApp();\n * app.register(myDriver);\n * await app.start();\n * await app.stop();\n */\nexport function createTestApp(overrides?: Partial<AppConfig>): App {\n const config: AppConfig = {\n name: 'TestApp',\n logger: { level: 'error' },\n ...overrides,\n };\n\n const app = new App(config);\n\n // Replace the pino logger with a mock logger so tests produce no output\n // and can assert on logged messages when needed.\n app.logger = createMockLogger() as unknown as Logger;\n\n return app;\n}\n","import type { Logger } from '@iskra-bun/core';\n\nexport interface MockLogEntry {\n args: unknown[];\n}\n\nexport interface MockLoggerLogs {\n trace: MockLogEntry[];\n debug: MockLogEntry[];\n info: MockLogEntry[];\n warn: MockLogEntry[];\n error: MockLogEntry[];\n fatal: MockLogEntry[];\n}\n\nexport interface MockLogger extends Logger {\n logs: MockLoggerLogs;\n reset(): void;\n}\n\nfunction makeRecorder(logs: MockLogEntry[]): (...args: unknown[]) => void {\n return (...args: unknown[]) => {\n logs.push({ args });\n };\n}\n\n/**\n * Creates a Logger-compatible mock that captures all log calls into arrays\n * for assertion in tests. No output is written to stdout/stderr.\n *\n * Usage:\n * const logger = createMockLogger();\n * logger.info('hello');\n * expect(logger.logs.info).toHaveLength(1);\n * logger.reset();\n */\nexport function createMockLogger(): MockLogger {\n const logs: MockLoggerLogs = {\n trace: [],\n debug: [],\n info: [],\n warn: [],\n error: [],\n fatal: [],\n };\n\n const mock = {\n logs,\n reset() {\n logs.trace.length = 0;\n logs.debug.length = 0;\n logs.info.length = 0;\n logs.warn.length = 0;\n logs.error.length = 0;\n logs.fatal.length = 0;\n },\n trace: makeRecorder(logs.trace),\n debug: makeRecorder(logs.debug),\n info: makeRecorder(logs.info),\n warn: makeRecorder(logs.warn),\n error: makeRecorder(logs.error),\n fatal: makeRecorder(logs.fatal),\n silent: () => {},\n child(_bindings: Record<string, unknown>, _options?: unknown) {\n // Child loggers share parent capture arrays for simplicity\n return mock as unknown as Logger;\n },\n // Minimal pino.Logger shape — fields tests never touch\n level: 'error' as const,\n isLevelEnabled: (_level: string) => false,\n setBindings: (_bindings: Record<string, unknown>) => {},\n flush: (_cb?: (err?: Error) => void) => {},\n bindings: () => ({} as Record<string, unknown>),\n } as unknown as MockLogger;\n\n return mock;\n}\n","import type { App, Driver } from '@iskra-bun/core';\n\nexport type LifecycleCall = 'init' | 'start' | 'stop';\n\nexport interface MockDriverHooks {\n init?: (app: App) => Promise<void> | void;\n start?: () => Promise<void> | void;\n stop?: () => Promise<void> | void;\n}\n\nexport interface MockDriver extends Driver {\n /** Ordered record of which lifecycle methods were called */\n calls: LifecycleCall[];\n /** Whether init() was called */\n initialized: boolean;\n /** Whether start() was called */\n started: boolean;\n /** Whether stop() was called */\n stopped: boolean;\n /** Reset all recorded state */\n reset(): void;\n}\n\n/**\n * Creates a Driver-compatible mock that records every lifecycle call and its\n * order. Optional hooks let tests inject arbitrary behavior or throw errors.\n *\n * Usage:\n * const driver = createMockDriver({ stop: async () => { throw new Error('boom'); } });\n * await app.register(driver).start();\n * expect(driver.calls).toEqual(['init', 'start']);\n */\nexport function createMockDriver(\n name: string = 'mock-driver',\n hooks: MockDriverHooks = {},\n): MockDriver {\n const calls: LifecycleCall[] = [];\n\n const driver: MockDriver = {\n name,\n calls,\n initialized: false,\n started: false,\n stopped: false,\n\n reset() {\n calls.length = 0;\n driver.initialized = false;\n driver.started = false;\n driver.stopped = false;\n },\n\n async init(app: App) {\n calls.push('init');\n driver.initialized = true;\n if (hooks.init) {\n await hooks.init(app);\n }\n },\n\n async start() {\n calls.push('start');\n driver.started = true;\n if (hooks.start) {\n await hooks.start();\n }\n },\n\n async stop() {\n calls.push('stop');\n driver.stopped = true;\n if (hooks.stop) {\n await hooks.stop();\n }\n },\n };\n\n return driver;\n}\n","import { mkdtemp, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\n/**\n * Creates a temporary directory, invokes `fn` with its path, and always\n * removes it afterwards — even when `fn` throws.\n *\n * Usage:\n * await withTempDir(async (dir) => {\n * await Bun.write(join(dir, 'file.txt'), 'hello');\n * // dir is cleaned up automatically on return or throw\n * });\n */\nexport async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {\n const dir = await mkdtemp(join(tmpdir(), 'iskra-test-'));\n try {\n return await fn(dir);\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n}\n","/**\n * Minimal structural interface for objects that accept a fetch-style request.\n * Matches Hono's `app.request()` signature without importing Hono.\n */\nexport interface RequestHandler {\n request(\n input: string | Request | URL,\n requestInit?: RequestInit,\n env?: unknown,\n executionCtx?: unknown,\n ): Response | Promise<Response>;\n}\n\nexport interface TestServerClient {\n get(path: string, init?: RequestInit): Promise<Response>;\n post(path: string, body?: unknown, init?: RequestInit): Promise<Response>;\n put(path: string, body?: unknown, init?: RequestInit): Promise<Response>;\n patch(path: string, body?: unknown, init?: RequestInit): Promise<Response>;\n delete(path: string, init?: RequestInit): Promise<Response>;\n}\n\nfunction buildJsonInit(body: unknown, base: RequestInit = {}): RequestInit {\n return {\n ...base,\n headers: {\n 'content-type': 'application/json',\n ...base.headers,\n },\n body: JSON.stringify(body),\n };\n}\n\n/**\n * Wraps any object with a `.request()` method (structurally typed — no Hono\n * import required) in a small fetch-style client for testing HTTP handlers.\n *\n * Usage:\n * const client = createTestServer(honoApp);\n * const res = await client.get('/health');\n * expect(res.status).toBe(200);\n */\nexport function createTestServer(handler: RequestHandler): TestServerClient {\n return {\n get(path, init) {\n return Promise.resolve(handler.request(path, { method: 'GET', ...init }));\n },\n post(path, body, init) {\n return Promise.resolve(\n handler.request(\n path,\n body !== undefined\n ? { method: 'POST', ...buildJsonInit(body, init) }\n : { method: 'POST', ...init },\n ),\n );\n },\n put(path, body, init) {\n return Promise.resolve(\n handler.request(\n path,\n body !== undefined\n ? { method: 'PUT', ...buildJsonInit(body, init) }\n : { method: 'PUT', ...init },\n ),\n );\n },\n patch(path, body, init) {\n return Promise.resolve(\n handler.request(\n path,\n body !== undefined\n ? { method: 'PATCH', ...buildJsonInit(body, init) }\n : { method: 'PATCH', ...init },\n ),\n );\n },\n delete(path, init) {\n return Promise.resolve(handler.request(path, { method: 'DELETE', ...init }));\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iskra-bun/testing-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utilidades compartidas de testing para paquetes de Iskra.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iskra",
|
|
7
|
+
"bun",
|
|
8
|
+
"typescript",
|
|
9
|
+
"testing",
|
|
10
|
+
"mocks"
|
|
11
|
+
],
|
|
12
|
+
"author": "Joan Lascano",
|
|
13
|
+
"license": "AGPL-3.0-or-later",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/fearful/iskra.git",
|
|
17
|
+
"directory": "packages/testing-kit"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/fearful/iskra/tree/main/packages/testing-kit#readme",
|
|
20
|
+
"bugs": "https://github.com/fearful/iskra/issues",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"source": "./src/index.ts",
|
|
28
|
+
"bun": "./src/index.ts",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "bun test",
|
|
45
|
+
"build": "tsup --config ../../tsup.config.ts"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@iskra-bun/core": "0.1.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "^1.3.5",
|
|
52
|
+
"@types/node": "^22.10.2"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createTestApp } from './test-app';
|
|
2
|
+
export { createMockLogger } from './mock-logger';
|
|
3
|
+
export type { MockLogger, MockLoggerLogs, MockLogEntry } from './mock-logger';
|
|
4
|
+
export { createMockDriver } from './mock-driver';
|
|
5
|
+
export type { MockDriver, MockDriverHooks, LifecycleCall } from './mock-driver';
|
|
6
|
+
export { withTempDir } from './with-temp-dir';
|
|
7
|
+
export { createTestServer } from './test-server';
|
|
8
|
+
export type { RequestHandler, TestServerClient } from './test-server';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { App, Driver } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
export type LifecycleCall = 'init' | 'start' | 'stop';
|
|
4
|
+
|
|
5
|
+
export interface MockDriverHooks {
|
|
6
|
+
init?: (app: App) => Promise<void> | void;
|
|
7
|
+
start?: () => Promise<void> | void;
|
|
8
|
+
stop?: () => Promise<void> | void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MockDriver extends Driver {
|
|
12
|
+
/** Ordered record of which lifecycle methods were called */
|
|
13
|
+
calls: LifecycleCall[];
|
|
14
|
+
/** Whether init() was called */
|
|
15
|
+
initialized: boolean;
|
|
16
|
+
/** Whether start() was called */
|
|
17
|
+
started: boolean;
|
|
18
|
+
/** Whether stop() was called */
|
|
19
|
+
stopped: boolean;
|
|
20
|
+
/** Reset all recorded state */
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a Driver-compatible mock that records every lifecycle call and its
|
|
26
|
+
* order. Optional hooks let tests inject arbitrary behavior or throw errors.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* const driver = createMockDriver({ stop: async () => { throw new Error('boom'); } });
|
|
30
|
+
* await app.register(driver).start();
|
|
31
|
+
* expect(driver.calls).toEqual(['init', 'start']);
|
|
32
|
+
*/
|
|
33
|
+
export function createMockDriver(
|
|
34
|
+
name: string = 'mock-driver',
|
|
35
|
+
hooks: MockDriverHooks = {},
|
|
36
|
+
): MockDriver {
|
|
37
|
+
const calls: LifecycleCall[] = [];
|
|
38
|
+
|
|
39
|
+
const driver: MockDriver = {
|
|
40
|
+
name,
|
|
41
|
+
calls,
|
|
42
|
+
initialized: false,
|
|
43
|
+
started: false,
|
|
44
|
+
stopped: false,
|
|
45
|
+
|
|
46
|
+
reset() {
|
|
47
|
+
calls.length = 0;
|
|
48
|
+
driver.initialized = false;
|
|
49
|
+
driver.started = false;
|
|
50
|
+
driver.stopped = false;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async init(app: App) {
|
|
54
|
+
calls.push('init');
|
|
55
|
+
driver.initialized = true;
|
|
56
|
+
if (hooks.init) {
|
|
57
|
+
await hooks.init(app);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async start() {
|
|
62
|
+
calls.push('start');
|
|
63
|
+
driver.started = true;
|
|
64
|
+
if (hooks.start) {
|
|
65
|
+
await hooks.start();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async stop() {
|
|
70
|
+
calls.push('stop');
|
|
71
|
+
driver.stopped = true;
|
|
72
|
+
if (hooks.stop) {
|
|
73
|
+
await hooks.stop();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return driver;
|
|
79
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Logger } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
export interface MockLogEntry {
|
|
4
|
+
args: unknown[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface MockLoggerLogs {
|
|
8
|
+
trace: MockLogEntry[];
|
|
9
|
+
debug: MockLogEntry[];
|
|
10
|
+
info: MockLogEntry[];
|
|
11
|
+
warn: MockLogEntry[];
|
|
12
|
+
error: MockLogEntry[];
|
|
13
|
+
fatal: MockLogEntry[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MockLogger extends Logger {
|
|
17
|
+
logs: MockLoggerLogs;
|
|
18
|
+
reset(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeRecorder(logs: MockLogEntry[]): (...args: unknown[]) => void {
|
|
22
|
+
return (...args: unknown[]) => {
|
|
23
|
+
logs.push({ args });
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a Logger-compatible mock that captures all log calls into arrays
|
|
29
|
+
* for assertion in tests. No output is written to stdout/stderr.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* const logger = createMockLogger();
|
|
33
|
+
* logger.info('hello');
|
|
34
|
+
* expect(logger.logs.info).toHaveLength(1);
|
|
35
|
+
* logger.reset();
|
|
36
|
+
*/
|
|
37
|
+
export function createMockLogger(): MockLogger {
|
|
38
|
+
const logs: MockLoggerLogs = {
|
|
39
|
+
trace: [],
|
|
40
|
+
debug: [],
|
|
41
|
+
info: [],
|
|
42
|
+
warn: [],
|
|
43
|
+
error: [],
|
|
44
|
+
fatal: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mock = {
|
|
48
|
+
logs,
|
|
49
|
+
reset() {
|
|
50
|
+
logs.trace.length = 0;
|
|
51
|
+
logs.debug.length = 0;
|
|
52
|
+
logs.info.length = 0;
|
|
53
|
+
logs.warn.length = 0;
|
|
54
|
+
logs.error.length = 0;
|
|
55
|
+
logs.fatal.length = 0;
|
|
56
|
+
},
|
|
57
|
+
trace: makeRecorder(logs.trace),
|
|
58
|
+
debug: makeRecorder(logs.debug),
|
|
59
|
+
info: makeRecorder(logs.info),
|
|
60
|
+
warn: makeRecorder(logs.warn),
|
|
61
|
+
error: makeRecorder(logs.error),
|
|
62
|
+
fatal: makeRecorder(logs.fatal),
|
|
63
|
+
silent: () => {},
|
|
64
|
+
child(_bindings: Record<string, unknown>, _options?: unknown) {
|
|
65
|
+
// Child loggers share parent capture arrays for simplicity
|
|
66
|
+
return mock as unknown as Logger;
|
|
67
|
+
},
|
|
68
|
+
// Minimal pino.Logger shape — fields tests never touch
|
|
69
|
+
level: 'error' as const,
|
|
70
|
+
isLevelEnabled: (_level: string) => false,
|
|
71
|
+
setBindings: (_bindings: Record<string, unknown>) => {},
|
|
72
|
+
flush: (_cb?: (err?: Error) => void) => {},
|
|
73
|
+
bindings: () => ({} as Record<string, unknown>),
|
|
74
|
+
} as unknown as MockLogger;
|
|
75
|
+
|
|
76
|
+
return mock;
|
|
77
|
+
}
|
package/src/test-app.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { App } from '@iskra-bun/core';
|
|
2
|
+
import type { AppConfig } from '@iskra-bun/core';
|
|
3
|
+
import type { Logger } from '@iskra-bun/core';
|
|
4
|
+
import { createMockLogger } from './mock-logger';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a real App instance configured for testing:
|
|
8
|
+
* - Logger level defaults to 'error' so tests stay silent
|
|
9
|
+
* - OTel is disabled to avoid side effects
|
|
10
|
+
* - Config name defaults to 'TestApp'
|
|
11
|
+
* - Accepts optional overrides merged shallowly over the defaults
|
|
12
|
+
*
|
|
13
|
+
* The returned app is a genuine App so start()/stop()/register() all work
|
|
14
|
+
* exactly as in production.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const app = createTestApp();
|
|
18
|
+
* app.register(myDriver);
|
|
19
|
+
* await app.start();
|
|
20
|
+
* await app.stop();
|
|
21
|
+
*/
|
|
22
|
+
export function createTestApp(overrides?: Partial<AppConfig>): App {
|
|
23
|
+
const config: AppConfig = {
|
|
24
|
+
name: 'TestApp',
|
|
25
|
+
logger: { level: 'error' },
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const app = new App(config);
|
|
30
|
+
|
|
31
|
+
// Replace the pino logger with a mock logger so tests produce no output
|
|
32
|
+
// and can assert on logged messages when needed.
|
|
33
|
+
app.logger = createMockLogger() as unknown as Logger;
|
|
34
|
+
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural interface for objects that accept a fetch-style request.
|
|
3
|
+
* Matches Hono's `app.request()` signature without importing Hono.
|
|
4
|
+
*/
|
|
5
|
+
export interface RequestHandler {
|
|
6
|
+
request(
|
|
7
|
+
input: string | Request | URL,
|
|
8
|
+
requestInit?: RequestInit,
|
|
9
|
+
env?: unknown,
|
|
10
|
+
executionCtx?: unknown,
|
|
11
|
+
): Response | Promise<Response>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TestServerClient {
|
|
15
|
+
get(path: string, init?: RequestInit): Promise<Response>;
|
|
16
|
+
post(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
17
|
+
put(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
18
|
+
patch(path: string, body?: unknown, init?: RequestInit): Promise<Response>;
|
|
19
|
+
delete(path: string, init?: RequestInit): Promise<Response>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildJsonInit(body: unknown, base: RequestInit = {}): RequestInit {
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
headers: {
|
|
26
|
+
'content-type': 'application/json',
|
|
27
|
+
...base.headers,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wraps any object with a `.request()` method (structurally typed — no Hono
|
|
35
|
+
* import required) in a small fetch-style client for testing HTTP handlers.
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* const client = createTestServer(honoApp);
|
|
39
|
+
* const res = await client.get('/health');
|
|
40
|
+
* expect(res.status).toBe(200);
|
|
41
|
+
*/
|
|
42
|
+
export function createTestServer(handler: RequestHandler): TestServerClient {
|
|
43
|
+
return {
|
|
44
|
+
get(path, init) {
|
|
45
|
+
return Promise.resolve(handler.request(path, { method: 'GET', ...init }));
|
|
46
|
+
},
|
|
47
|
+
post(path, body, init) {
|
|
48
|
+
return Promise.resolve(
|
|
49
|
+
handler.request(
|
|
50
|
+
path,
|
|
51
|
+
body !== undefined
|
|
52
|
+
? { method: 'POST', ...buildJsonInit(body, init) }
|
|
53
|
+
: { method: 'POST', ...init },
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
put(path, body, init) {
|
|
58
|
+
return Promise.resolve(
|
|
59
|
+
handler.request(
|
|
60
|
+
path,
|
|
61
|
+
body !== undefined
|
|
62
|
+
? { method: 'PUT', ...buildJsonInit(body, init) }
|
|
63
|
+
: { method: 'PUT', ...init },
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
patch(path, body, init) {
|
|
68
|
+
return Promise.resolve(
|
|
69
|
+
handler.request(
|
|
70
|
+
path,
|
|
71
|
+
body !== undefined
|
|
72
|
+
? { method: 'PATCH', ...buildJsonInit(body, init) }
|
|
73
|
+
: { method: 'PATCH', ...init },
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
delete(path, init) {
|
|
78
|
+
return Promise.resolve(handler.request(path, { method: 'DELETE', ...init }));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a temporary directory, invokes `fn` with its path, and always
|
|
7
|
+
* removes it afterwards — even when `fn` throws.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* await withTempDir(async (dir) => {
|
|
11
|
+
* await Bun.write(join(dir, 'file.txt'), 'hello');
|
|
12
|
+
* // dir is cleaned up automatically on return or throw
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
16
|
+
const dir = await mkdtemp(join(tmpdir(), 'iskra-test-'));
|
|
17
|
+
try {
|
|
18
|
+
return await fn(dir);
|
|
19
|
+
} finally {
|
|
20
|
+
await rm(dir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
}
|