@dunx/testing 0.1.0 → 0.1.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The container an app already has, with named bindings **replaced in place**, plus a
4
4
  real `Bun.serve` on port 0. No mocking framework, no fake request object, no
5
- in-memory transport Bun binds a socket in about a millisecond, so the thing under
5
+ in-memory transport - Bun binds a socket in about a millisecond, so the thing under
6
6
  test is the thing that ships.
7
7
 
8
8
  ```ts
@@ -33,7 +33,7 @@ This is the whole design, and it follows from the container being flat.
33
33
 
34
34
  `@dunx/core` collects every module's registrations into one list and **throws on a
35
35
  duplicate token**, naming both modules. So a test override cannot be an extra
36
- module appended at the end that wins there is no "wins". It would be a duplicate.
36
+ module appended at the end that wins - there is no "wins". It would be a duplicate.
37
37
 
38
38
  `createTestApp` therefore assembles the same flat list the app would have and
39
39
  substitutes by token as it goes. Three consequences worth relying on:
@@ -47,7 +47,7 @@ substitutes by token as it goes. Three consequences worth relying on:
47
47
  prevent.
48
48
  - **The discarded provider is never instantiated.** Its `useFactory` never runs and
49
49
  its `onInit` never fires. Overriding the database does not open a connection to
50
- the real database that is the one guarantee here that a hand-rolled fixture
50
+ the real database - that is the one guarantee here that a hand-rolled fixture
51
51
  usually gets wrong, and `app.test.ts` proves it with a factory that throws if it
52
52
  is ever called.
53
53
 
@@ -59,19 +59,19 @@ applies there as well.
59
59
 
60
60
  | Export | What it is |
61
61
  | --------------------------- | ----------------------------------------------------------------------- |
62
- | `createTestApp(options)` | `Promise<App>` the core container, overrides applied |
63
- | `createTestServer(options)` | `Promise<TestServer>` the same, plus `Bun.serve` on port 0 and a client |
64
- | `testClient(url)` | `TestClient` the request helpers against any base URL |
62
+ | `createTestApp(options)` | `Promise<App>` - the core container, overrides applied |
63
+ | `createTestServer(options)` | `Promise<TestServer>` - the same, plus `Bun.serve` on port 0 and a client |
64
+ | `testClient(url)` | `TestClient` - the request helpers against any base URL |
65
65
  | `testRoot(modules)` | The synthetic root module, for driving `HttpFactory` yourself |
66
66
  | `RecordingLogger` | A `Logger` that keeps entries instead of writing them |
67
67
 
68
68
  `modules` takes one module ref or several; several become the `imports` of one
69
69
  synthetic root, so no fixture module has to be written by hand. Anything a module
70
- ref can be works a class, or a `DynamicModule` from a `forRoot`.
70
+ ref can be works - a class, or a `DynamicModule` from a `forRoot`.
71
71
 
72
72
  `TestServer` is a `TestClient` plus `app` (the real `HttpApp`, for `app.get(...)`)
73
73
  and `close()`. `createTestServer` passes `HttpOptions` through, with two
74
- differences: `port` is always 0, and **`requestLogging` defaults to `false`** it
74
+ differences: `port` is always 0, and **`requestLogging` defaults to `false`** - it
75
75
  is on by default in production for good reasons, none of which apply to a suite
76
76
  that would otherwise print one JSON line per assertion. Pass
77
77
  `requestLogging: true` to test the logging itself.
@@ -87,7 +87,7 @@ const image = await server.request('avatars/7.png'); // the raw Response
87
87
  ```
88
88
 
89
89
  `json` on the init object is serialized and sets `content-type: application/json`
90
- unless `headers` already carries one one option for every verb, rather than a
90
+ unless `headers` already carries one - one option for every verb, rather than a
91
91
  `post`/`put`/`patch` triple. `json()` reads the body as text before parsing, so a
92
92
  route that answered 204, HTML or a plain-text error fails with the status,
93
93
  content-type and body rather than with `JSON.parse`'s message.
@@ -107,7 +107,7 @@ expect(logger.at(LogLevel.ERROR)).toEqual([]);
107
107
  ```
108
108
 
109
109
  It records; it does not interpret. No level filtering, no error promotion, no
110
- merging of extras those are `@arkv/logger`'s behaviour, and asserting against a
110
+ merging of extras - those are `@arkv/logger`'s behaviour, and asserting against a
111
111
  reimplementation of them would prove nothing.
112
112
 
113
113
  ## Deliberately not here
@@ -123,7 +123,7 @@ reimplementation of them would prove nothing.
123
123
  needs binding goes in a two-line `@Module`, which is also where it would live if
124
124
  it were real.
125
125
  - **A fake HTTP dispatcher.** It could only exercise the parts of the request path
126
- dunx wrote, and not the parts Bun owns route matching, params, method
126
+ dunx wrote, and not the parts Bun owns - route matching, params, method
127
127
  dispatch, upgrades. The real server is cheaper than the lie.
128
128
  - **Database fixtures, transactional rollback, seeding.** That is drizzle's
129
129
  surface, not this package's. `@dunx/infra/db` binds an in-memory `bun:sqlite`
@@ -139,7 +139,7 @@ bun add -d @dunx/testing
139
139
  ```
140
140
 
141
141
  `@dunx/core` and `@dunx/http` are `dependencies`, at a **caret** range. What matters
142
- is that your app and this package resolve to **one copy of `@dunx/core`** two
142
+ is that your app and this package resolve to **one copy of `@dunx/core`** - two
143
143
  copies means two `Logger` classes and two `RequestContext` classes, so tokens that
144
144
  match nothing and overrides that silently replace nothing. A caret range hoists to
145
145
  the copy your app already has.
package/dist/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type App, type AppOptions, type DynamicModule, type ModuleRef, type Registration } from '@dunx/core';
2
2
  export interface TestAppOptions extends AppOptions {
3
3
  /**
4
- * The graph under test. A single module, or several they become the `imports`
4
+ * The graph under test. A single module, or several - they become the `imports`
5
5
  * of one synthetic root, so no fixture module has to be written by hand.
6
6
  */
7
7
  readonly modules: ModuleRef | readonly ModuleRef[];
package/dist/client.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface JsonResponse<T> {
14
14
  export interface TestClient {
15
15
  /** The server's base URL, as `listen()` returned it. */
16
16
  readonly url: string;
17
- /** The raw `Response` for bytes, HTML, or asserting on a header. */
17
+ /** The raw `Response` - for bytes, HTML, or asserting on a header. */
18
18
  request(path?: string, init?: JsonInit): Promise<Response>;
19
19
  /** Status, headers and parsed body in one await, which is the common assertion. */
20
20
  json<T = unknown>(path?: string, init?: JsonInit): Promise<JsonResponse<T>>;
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ var testClient = (url) => ({
43
43
  ` + text.slice(0, 300);
44
44
  throw new Error(`${init.method ?? "GET"} ${target(url, path).pathname} answered ` + `${response.status} with ${body}
45
45
 
46
- That is not JSON \u2014 use request() ` + "for a response that is not.");
46
+ That is not JSON - use request() ` + "for a response that is not.");
47
47
  }
48
48
  }
49
49
  });
@@ -110,5 +110,5 @@ export {
110
110
  RecordingLogger
111
111
  };
112
112
 
113
- //# debugId=5E77DA71121848D164756E2164756E21
113
+ //# debugId=16298FAA58964A6664756E2164756E21
114
114
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,12 +2,12 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/app.ts", "../src/client.ts", "../src/logger.ts", "../src/server.ts"],
4
4
  "sourcesContent": [
5
- "import {\n AppFactory,\n type App,\n type AppOptions,\n type DynamicModule,\n type ModuleRef,\n type Registration,\n} from '@dunx/core';\n\n/**\n * The synthetic root. A named class rather than an object literal for the same\n * reason `@dunx/http`'s `HttpModule` is one: it is what a duplicate-binding error\n * would name if the harness itself ever bound anything.\n */\nclass TestModule {}\n\nexport interface TestAppOptions extends AppOptions {\n /**\n * The graph under test. A single module, or several they become the `imports`\n * of one synthetic root, so no fixture module has to be written by hand.\n */\n readonly modules: ModuleRef | readonly ModuleRef[];\n}\n\nconst isList = (\n modules: ModuleRef | readonly ModuleRef[],\n): modules is readonly ModuleRef[] => Array.isArray(modules);\n\n/**\n * The root `createTestApp` boots. Exported for the case the harness deliberately\n * does not cover: configuring an `HttpApp` before `listen()` (`enableCors`, `use`,\n * `set`), which means calling `HttpFactory.create(testRoot(modules), …)` directly.\n */\nexport const testRoot = (\n modules: ModuleRef | readonly ModuleRef[],\n): DynamicModule => ({\n module: TestModule,\n imports: isList(modules) ? modules : [modules],\n});\n\n/** `exactOptionalPropertyTypes` separates an absent key from an undefined one. */\nexport const appOptions = (\n overrides: readonly Registration[] | undefined,\n): AppOptions => (overrides ? { overrides } : {});\n\n/**\n * The container the app under test would have, with the bindings named in\n * `overrides` **replaced in place**.\n *\n * ```ts\n * const app = await createTestApp({\n * modules: [UsersModule],\n * overrides: [provide(Clock, { useValue: new FixedClock('2026-01-01') })],\n * });\n * ```\n *\n * Replacement, not addition: the discarded provider is never instantiated, so an\n * async `useFactory` that would open the real database never runs. An override\n * naming a token nobody binds throws instead of passing silently.\n */\nexport const createTestApp = (options: TestAppOptions): Promise<App> =>\n AppFactory.create(testRoot(options.modules), appOptions(options.overrides));\n",
6
- "export interface JsonInit extends RequestInit {\n /**\n * Serialized as the request body, with `content-type: application/json` set\n * unless `headers` already carries one. Covers every verb, so there is no\n * `post()`/`put()`/`patch()` triple here. Takes precedence over `body`.\n */\n readonly json?: unknown;\n}\n\nexport interface JsonResponse<T> {\n readonly status: number;\n readonly headers: Headers;\n readonly body: T;\n}\n\nexport interface TestClient {\n /** The server's base URL, as `listen()` returned it. */\n readonly url: string;\n /** The raw `Response` for bytes, HTML, or asserting on a header. */\n request(path?: string, init?: JsonInit): Promise<Response>;\n /** Status, headers and parsed body in one await, which is the common assertion. */\n json<T = unknown>(path?: string, init?: JsonInit): Promise<JsonResponse<T>>;\n}\n\nconst target = (base: string, path: string): URL => new URL(path, base);\n\nconst withJson = (init: JsonInit): RequestInit => {\n const { json, ...rest } = init;\n if (json === undefined) return rest;\n\n const headers = new Headers(init.headers);\n if (!headers.has('content-type')) {\n headers.set('content-type', 'application/json');\n }\n return { ...rest, headers, body: JSON.stringify(json) };\n};\n\n/**\n * `fetch` against one base URL, plus the JSON round-trip every suite otherwise\n * rewrites. Deliberately not an assertion DSL: it returns values that `expect`\n * already reads well, so failures point at the assertion rather than at a matcher\n * this package would have to define.\n *\n * `createTestServer` returns one of these already bound to the server it started;\n * this is here for an app booted some other way.\n */\nexport const testClient = (url: string): TestClient => ({\n url,\n request: (path = '', init: JsonInit = {}) =>\n fetch(target(url, path), withJson(init)),\n json: async <T>(path = '', init: JsonInit = {}): Promise<JsonResponse<T>> => {\n const response = await fetch(target(url, path), withJson(init));\n // Read as text first: a route that answered 204, HTML or a plain-text error\n // would otherwise fail with `JSON.parse`'s message and none of the context\n // needed to see why.\n const text = await response.text();\n try {\n return {\n status: response.status,\n headers: response.headers,\n body: JSON.parse(text) as T,\n };\n } catch {\n const body =\n text === ''\n ? 'an empty body'\n : `a ${response.headers.get('content-type') ?? 'typeless'} body:\\n\\n` +\n text.slice(0, 300);\n throw new Error(\n `${init.method ?? 'GET'} ${target(url, path).pathname} answered ` +\n `${response.status} with ${body}\\n\\nThat is not JSON use request() ` +\n 'for a response that is not.',\n );\n }\n },\n});\n",
7
- "import { Logger, LogLevel, type LogMessage } from '@dunx/core';\n\nexport interface RecordedLog {\n readonly level: LogLevel;\n readonly message: LogMessage;\n readonly params: readonly unknown[];\n}\n\n/**\n * A {@link Logger} that keeps entries instead of writing them, so a suite can\n * assert on what was logged and stays quiet when it does not care.\n *\n * It is here because the contract is seven levels of three overloads each: every\n * suite that wants a silent logger would otherwise hand-write the same thirty\n * lines. Nothing is interpreted no level filtering, no error promotion, no\n * merging of extras because those are the backing logger's behaviour and\n * asserting against a reimplementation of them would prove nothing.\n *\n * ```ts\n * const logger = new RecordingLogger();\n * await createTestApp({\n * modules: [UsersModule],\n * overrides: [provide(Logger, { useValue: logger })],\n * });\n * expect(logger.at(LogLevel.WARN)).toHaveLength(0);\n * ```\n */\nexport class RecordingLogger extends Logger {\n /** Everything is recorded, so a suite can assert on a `verbose` call too. */\n readonly logLevel: LogLevel = LogLevel.VERBOSE;\n readonly entries: RecordedLog[] = [];\n\n verbose(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.VERBOSE, message, params);\n }\n\n debug(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.DEBUG, message, params);\n }\n\n info(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.INFO, message, params);\n }\n\n /** @deprecated Use {@link RecordingLogger.info}. Recorded as `info` either way. */\n log(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.INFO, message, params);\n }\n\n warn(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.WARN, message, params);\n }\n\n error(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.ERROR, message, params);\n }\n\n fatal(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.FATAL, message, params);\n }\n\n at(level: LogLevel): readonly RecordedLog[] {\n return this.entries.filter((entry) => entry.level === level);\n }\n\n clear(): void {\n this.entries.length = 0;\n }\n\n #record(\n level: LogLevel,\n message: LogMessage,\n params: readonly unknown[],\n ): void {\n this.entries.push({ level, message, params });\n }\n}\n",
8
- "import { HttpFactory, type HttpApp, type HttpOptions } from '@dunx/http';\nimport { appOptions, testRoot, type TestAppOptions } from './app.js';\nimport { testClient, type TestClient } from './client.js';\n\nexport interface TestServerOptions\n extends TestAppOptions, Omit<HttpOptions, 'port' | 'overrides'> {\n /**\n * `setGlobalPrefix`, applied before `listen()` so the client's URLs carry it.\n *\n * Explicitly `| undefined`, unlike the rest of the options: a suite that runs\n * the same fixture prefixed and unprefixed passes a variable here, and under\n * `exactOptionalPropertyTypes` that is otherwise a conditional spread. \"No\n * prefix\" and \"absent\" mean the same thing, so nothing is lost by allowing it.\n */\n readonly prefix?: string | undefined;\n}\n\nexport interface TestServer extends TestClient {\n readonly app: HttpApp;\n /** `app.shutdown()` stops the server, then tears the container down. */\n close(): Promise<void>;\n}\n\n/**\n * A **real** `Bun.serve` on port 0, with the same override semantics as\n * {@link createTestApp}. Nothing is faked: `Bun.serve` binds in about a\n * millisecond, and a fake would only be able to prove the parts of the request\n * path dunx wrote rather than the parts Bun owns routing, params, method\n * dispatch, upgrades.\n *\n * ```ts\n * const server = await createTestServer({ modules: [ApiModule], prefix: 'api' });\n * const { status, body } = await server.json('api/users');\n * await server.close();\n * ```\n *\n * Request logging is **off** unless asked for: it is on by default in production\n * for good reasons, none of which apply to a suite that would print one JSON line\n * per assertion.\n */\nexport const createTestServer = async (\n options: TestServerOptions,\n): Promise<TestServer> => {\n const { modules, overrides, prefix, requestLogging, ...http } = options;\n\n const app = await HttpFactory.create(testRoot(modules), {\n ...http,\n ...appOptions(overrides),\n requestLogging: requestLogging ?? false,\n port: 0,\n });\n if (prefix !== undefined) app.setGlobalPrefix(prefix);\n\n return {\n ...testClient(await app.listen()),\n app,\n close: () => app.shutdown(),\n };\n};\n"
5
+ "import {\n AppFactory,\n type App,\n type AppOptions,\n type DynamicModule,\n type ModuleRef,\n type Registration,\n} from '@dunx/core';\n\n/**\n * The synthetic root. A named class rather than an object literal for the same\n * reason `@dunx/http`'s `HttpModule` is one: it is what a duplicate-binding error\n * would name if the harness itself ever bound anything.\n */\nclass TestModule {}\n\nexport interface TestAppOptions extends AppOptions {\n /**\n * The graph under test. A single module, or several - they become the `imports`\n * of one synthetic root, so no fixture module has to be written by hand.\n */\n readonly modules: ModuleRef | readonly ModuleRef[];\n}\n\nconst isList = (\n modules: ModuleRef | readonly ModuleRef[],\n): modules is readonly ModuleRef[] => Array.isArray(modules);\n\n/**\n * The root `createTestApp` boots. Exported for the case the harness deliberately\n * does not cover: configuring an `HttpApp` before `listen()` (`enableCors`, `use`,\n * `set`), which means calling `HttpFactory.create(testRoot(modules), …)` directly.\n */\nexport const testRoot = (\n modules: ModuleRef | readonly ModuleRef[],\n): DynamicModule => ({\n module: TestModule,\n imports: isList(modules) ? modules : [modules],\n});\n\n/** `exactOptionalPropertyTypes` separates an absent key from an undefined one. */\nexport const appOptions = (\n overrides: readonly Registration[] | undefined,\n): AppOptions => (overrides ? { overrides } : {});\n\n/**\n * The container the app under test would have, with the bindings named in\n * `overrides` **replaced in place**.\n *\n * ```ts\n * const app = await createTestApp({\n * modules: [UsersModule],\n * overrides: [provide(Clock, { useValue: new FixedClock('2026-01-01') })],\n * });\n * ```\n *\n * Replacement, not addition: the discarded provider is never instantiated, so an\n * async `useFactory` that would open the real database never runs. An override\n * naming a token nobody binds throws instead of passing silently.\n */\nexport const createTestApp = (options: TestAppOptions): Promise<App> =>\n AppFactory.create(testRoot(options.modules), appOptions(options.overrides));\n",
6
+ "export interface JsonInit extends RequestInit {\n /**\n * Serialized as the request body, with `content-type: application/json` set\n * unless `headers` already carries one. Covers every verb, so there is no\n * `post()`/`put()`/`patch()` triple here. Takes precedence over `body`.\n */\n readonly json?: unknown;\n}\n\nexport interface JsonResponse<T> {\n readonly status: number;\n readonly headers: Headers;\n readonly body: T;\n}\n\nexport interface TestClient {\n /** The server's base URL, as `listen()` returned it. */\n readonly url: string;\n /** The raw `Response` - for bytes, HTML, or asserting on a header. */\n request(path?: string, init?: JsonInit): Promise<Response>;\n /** Status, headers and parsed body in one await, which is the common assertion. */\n json<T = unknown>(path?: string, init?: JsonInit): Promise<JsonResponse<T>>;\n}\n\nconst target = (base: string, path: string): URL => new URL(path, base);\n\nconst withJson = (init: JsonInit): RequestInit => {\n const { json, ...rest } = init;\n if (json === undefined) return rest;\n\n const headers = new Headers(init.headers);\n if (!headers.has('content-type')) {\n headers.set('content-type', 'application/json');\n }\n return { ...rest, headers, body: JSON.stringify(json) };\n};\n\n/**\n * `fetch` against one base URL, plus the JSON round-trip every suite otherwise\n * rewrites. Deliberately not an assertion DSL: it returns values that `expect`\n * already reads well, so failures point at the assertion rather than at a matcher\n * this package would have to define.\n *\n * `createTestServer` returns one of these already bound to the server it started;\n * this is here for an app booted some other way.\n */\nexport const testClient = (url: string): TestClient => ({\n url,\n request: (path = '', init: JsonInit = {}) =>\n fetch(target(url, path), withJson(init)),\n json: async <T>(path = '', init: JsonInit = {}): Promise<JsonResponse<T>> => {\n const response = await fetch(target(url, path), withJson(init));\n // Read as text first: a route that answered 204, HTML or a plain-text error\n // would otherwise fail with `JSON.parse`'s message and none of the context\n // needed to see why.\n const text = await response.text();\n try {\n return {\n status: response.status,\n headers: response.headers,\n body: JSON.parse(text) as T,\n };\n } catch {\n const body =\n text === ''\n ? 'an empty body'\n : `a ${response.headers.get('content-type') ?? 'typeless'} body:\\n\\n` +\n text.slice(0, 300);\n throw new Error(\n `${init.method ?? 'GET'} ${target(url, path).pathname} answered ` +\n `${response.status} with ${body}\\n\\nThat is not JSON - use request() ` +\n 'for a response that is not.',\n );\n }\n },\n});\n",
7
+ "import { Logger, LogLevel, type LogMessage } from '@dunx/core';\n\nexport interface RecordedLog {\n readonly level: LogLevel;\n readonly message: LogMessage;\n readonly params: readonly unknown[];\n}\n\n/**\n * A {@link Logger} that keeps entries instead of writing them, so a suite can\n * assert on what was logged and stays quiet when it does not care.\n *\n * It is here because the contract is seven levels of three overloads each: every\n * suite that wants a silent logger would otherwise hand-write the same thirty\n * lines. Nothing is interpreted - no level filtering, no error promotion, no\n * merging of extras - because those are the backing logger's behaviour and\n * asserting against a reimplementation of them would prove nothing.\n *\n * ```ts\n * const logger = new RecordingLogger();\n * await createTestApp({\n * modules: [UsersModule],\n * overrides: [provide(Logger, { useValue: logger })],\n * });\n * expect(logger.at(LogLevel.WARN)).toHaveLength(0);\n * ```\n */\nexport class RecordingLogger extends Logger {\n /** Everything is recorded, so a suite can assert on a `verbose` call too. */\n readonly logLevel: LogLevel = LogLevel.VERBOSE;\n readonly entries: RecordedLog[] = [];\n\n verbose(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.VERBOSE, message, params);\n }\n\n debug(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.DEBUG, message, params);\n }\n\n info(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.INFO, message, params);\n }\n\n /** @deprecated Use {@link RecordingLogger.info}. Recorded as `info` either way. */\n log(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.INFO, message, params);\n }\n\n warn(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.WARN, message, params);\n }\n\n error(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.ERROR, message, params);\n }\n\n fatal(message: LogMessage, ...params: unknown[]): void {\n this.#record(LogLevel.FATAL, message, params);\n }\n\n at(level: LogLevel): readonly RecordedLog[] {\n return this.entries.filter((entry) => entry.level === level);\n }\n\n clear(): void {\n this.entries.length = 0;\n }\n\n #record(\n level: LogLevel,\n message: LogMessage,\n params: readonly unknown[],\n ): void {\n this.entries.push({ level, message, params });\n }\n}\n",
8
+ "import { HttpFactory, type HttpApp, type HttpOptions } from '@dunx/http';\nimport { appOptions, testRoot, type TestAppOptions } from './app.js';\nimport { testClient, type TestClient } from './client.js';\n\nexport interface TestServerOptions\n extends TestAppOptions, Omit<HttpOptions, 'port' | 'overrides'> {\n /**\n * `setGlobalPrefix`, applied before `listen()` so the client's URLs carry it.\n *\n * Explicitly `| undefined`, unlike the rest of the options: a suite that runs\n * the same fixture prefixed and unprefixed passes a variable here, and under\n * `exactOptionalPropertyTypes` that is otherwise a conditional spread. \"No\n * prefix\" and \"absent\" mean the same thing, so nothing is lost by allowing it.\n */\n readonly prefix?: string | undefined;\n}\n\nexport interface TestServer extends TestClient {\n readonly app: HttpApp;\n /** `app.shutdown()` - stops the server, then tears the container down. */\n close(): Promise<void>;\n}\n\n/**\n * A **real** `Bun.serve` on port 0, with the same override semantics as\n * {@link createTestApp}. Nothing is faked: `Bun.serve` binds in about a\n * millisecond, and a fake would only be able to prove the parts of the request\n * path dunx wrote rather than the parts Bun owns - routing, params, method\n * dispatch, upgrades.\n *\n * ```ts\n * const server = await createTestServer({ modules: [ApiModule], prefix: 'api' });\n * const { status, body } = await server.json('api/users');\n * await server.close();\n * ```\n *\n * Request logging is **off** unless asked for: it is on by default in production\n * for good reasons, none of which apply to a suite that would print one JSON line\n * per assertion.\n */\nexport const createTestServer = async (\n options: TestServerOptions,\n): Promise<TestServer> => {\n const { modules, overrides, prefix, requestLogging, ...http } = options;\n\n const app = await HttpFactory.create(testRoot(modules), {\n ...http,\n ...appOptions(overrides),\n requestLogging: requestLogging ?? false,\n port: 0,\n });\n if (prefix !== undefined) app.setGlobalPrefix(prefix);\n\n return {\n ...testClient(await app.listen()),\n app,\n close: () => app.shutdown(),\n };\n};\n"
9
9
  ],
10
- "mappings": "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",
11
- "debugId": "5E77DA71121848D164756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "16298FAA58964A6664756E2164756E21",
12
12
  "names": []
13
13
  }
package/dist/logger.d.ts CHANGED
@@ -10,8 +10,8 @@ export interface RecordedLog {
10
10
  *
11
11
  * It is here because the contract is seven levels of three overloads each: every
12
12
  * suite that wants a silent logger would otherwise hand-write the same thirty
13
- * lines. Nothing is interpreted no level filtering, no error promotion, no
14
- * merging of extras because those are the backing logger's behaviour and
13
+ * lines. Nothing is interpreted - no level filtering, no error promotion, no
14
+ * merging of extras - because those are the backing logger's behaviour and
15
15
  * asserting against a reimplementation of them would prove nothing.
16
16
  *
17
17
  * ```ts
package/dist/server.d.ts CHANGED
@@ -14,14 +14,14 @@ export interface TestServerOptions extends TestAppOptions, Omit<HttpOptions, 'po
14
14
  }
15
15
  export interface TestServer extends TestClient {
16
16
  readonly app: HttpApp;
17
- /** `app.shutdown()` stops the server, then tears the container down. */
17
+ /** `app.shutdown()` - stops the server, then tears the container down. */
18
18
  close(): Promise<void>;
19
19
  }
20
20
  /**
21
21
  * A **real** `Bun.serve` on port 0, with the same override semantics as
22
22
  * {@link createTestApp}. Nothing is faked: `Bun.serve` binds in about a
23
23
  * millisecond, and a fake would only be able to prove the parts of the request
24
- * path dunx wrote rather than the parts Bun owns routing, params, method
24
+ * path dunx wrote rather than the parts Bun owns - routing, params, method
25
25
  * dispatch, upgrades.
26
26
  *
27
27
  * ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/testing",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Test harness for dunx apps: a container with providers replaced in place, and a real Bun.serve on port 0",
5
5
  "keywords": [
6
6
  "bun",
@@ -12,7 +12,7 @@
12
12
  "testing"
13
13
  ],
14
14
  "homepage": "https://github.com/petarzarkov/dunx/tree/main/packages/testing#readme",
15
- "license": "MIT",
15
+ "license": "Apache-2.0",
16
16
  "author": {
17
17
  "name": "Petar Zarkov",
18
18
  "email": "pzarko1@gmail.com",
@@ -51,8 +51,8 @@
51
51
  "@dunx/http": "workspace:*"
52
52
  },
53
53
  "peerDependencies": {
54
- "@dunx/core": "0.1.0",
55
- "@dunx/http": "0.1.0",
54
+ "@dunx/core": "0.1.1",
55
+ "@dunx/http": "0.1.1",
56
56
  "@types/bun": ">=1.3.0"
57
57
  },
58
58
  "peerDependenciesMeta": {