@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 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
+ ```
@@ -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
+ }
@@ -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
+ }