@kozojs/testing 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/lib/index.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { Services, Kozo, KozoConfig } from '@kozojs/core';
2
+
3
+ interface InjectOptions {
4
+ method?: string;
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ body?: unknown;
8
+ /** Shorthand: appended as query string to the URL */
9
+ query?: Record<string, string>;
10
+ }
11
+ interface TestResponse {
12
+ status: number;
13
+ headers: Headers;
14
+ body: string;
15
+ ok: boolean;
16
+ /** Parse the response body as JSON */
17
+ json<T = unknown>(): T;
18
+ }
19
+ interface TestClient<TServices extends Services = Services> {
20
+ /** The underlying Kozo app instance */
21
+ app: Kozo<TServices>;
22
+ /** Make an arbitrary in-process HTTP request */
23
+ inject(options: InjectOptions): Promise<TestResponse>;
24
+ /** GET shorthand */
25
+ get(url: string, opts?: Omit<InjectOptions, 'method' | 'url'>): Promise<TestResponse>;
26
+ /** POST shorthand — body is JSON-serialized automatically */
27
+ post(url: string, body?: unknown, opts?: Omit<InjectOptions, 'method' | 'url' | 'body'>): Promise<TestResponse>;
28
+ /** PUT shorthand */
29
+ put(url: string, body?: unknown, opts?: Omit<InjectOptions, 'method' | 'url' | 'body'>): Promise<TestResponse>;
30
+ /** PATCH shorthand */
31
+ patch(url: string, body?: unknown, opts?: Omit<InjectOptions, 'method' | 'url' | 'body'>): Promise<TestResponse>;
32
+ /** DELETE shorthand */
33
+ delete(url: string, opts?: Omit<InjectOptions, 'method' | 'url'>): Promise<TestResponse>;
34
+ }
35
+ /**
36
+ * Wrap an existing Kozo app with a test client.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { createKozo, z } from '@kozojs/core';
41
+ * import { createTestClient } from '@kozojs/testing';
42
+ *
43
+ * const app = createKozo();
44
+ * app.get('/ping', {}, () => ({ pong: true }));
45
+ *
46
+ * const client = createTestClient(app);
47
+ * const res = await client.get('/ping');
48
+ * expect(res.status).toBe(200);
49
+ * expect(res.json()).toEqual({ pong: true });
50
+ * ```
51
+ */
52
+ declare function createTestClient<TServices extends Services = Services>(app: Kozo<TServices>): TestClient<TServices>;
53
+ /**
54
+ * Create a Kozo app + test client in one call.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { z } from '@kozojs/core';
59
+ * import { createTestApp } from '@kozojs/testing';
60
+ *
61
+ * const { app, post } = createTestApp();
62
+ *
63
+ * app.post('/users', {
64
+ * body: z.object({ name: z.string(), email: z.string().email() }),
65
+ * }, ({ body }) => ({ id: 1, ...body }));
66
+ *
67
+ * const res = await post('/users', { name: 'Alice', email: 'alice@example.com' });
68
+ * expect(res.status).toBe(200);
69
+ * expect(res.json()).toMatchObject({ name: 'Alice' });
70
+ *
71
+ * // Validation error
72
+ * const bad = await post('/users', { name: 'Alice', email: 'not-an-email' });
73
+ * expect(bad.status).toBe(400);
74
+ * expect(bad.json().errors[0]).toMatchObject({ field: 'email', code: 'invalid_string' });
75
+ * ```
76
+ */
77
+ declare function createTestApp<TServices extends Services = Services>(config?: KozoConfig<TServices>): TestClient<TServices>;
78
+
79
+ export { type InjectOptions, type TestClient, type TestResponse, createTestApp, createTestClient };
package/lib/index.js ADDED
@@ -0,0 +1,52 @@
1
+ // src/index.ts
2
+ import { createKozo } from "@kozojs/core";
3
+ function buildRequest(options) {
4
+ const { method = "GET", url, headers = {}, body, query } = options;
5
+ let fullUrl = url.startsWith("http") ? url : `http://localhost${url}`;
6
+ if (query && Object.keys(query).length > 0) {
7
+ const qs = new URLSearchParams(query);
8
+ fullUrl += (fullUrl.includes("?") ? "&" : "?") + qs.toString();
9
+ }
10
+ const finalHeaders = { ...headers };
11
+ let finalBody;
12
+ if (body !== void 0) {
13
+ if (!finalHeaders["content-type"] && !finalHeaders["Content-Type"]) {
14
+ finalHeaders["Content-Type"] = "application/json";
15
+ }
16
+ finalBody = typeof body === "string" ? body : JSON.stringify(body);
17
+ }
18
+ return new Request(fullUrl, { method, headers: finalHeaders, body: finalBody });
19
+ }
20
+ async function doInject(fetchFn, options) {
21
+ const req = buildRequest(options);
22
+ const res = await Promise.resolve(fetchFn(req));
23
+ const bodyText = await res.text();
24
+ return {
25
+ status: res.status,
26
+ headers: res.headers,
27
+ body: bodyText,
28
+ ok: res.ok,
29
+ json() {
30
+ return JSON.parse(bodyText);
31
+ }
32
+ };
33
+ }
34
+ function createTestClient(app) {
35
+ const fetchFn = app.fetch.bind(app);
36
+ return {
37
+ app,
38
+ inject: (opts) => doInject(fetchFn, opts),
39
+ get: (url, opts = {}) => doInject(fetchFn, { ...opts, method: "GET", url }),
40
+ post: (url, body, opts = {}) => doInject(fetchFn, { ...opts, method: "POST", url, body }),
41
+ put: (url, body, opts = {}) => doInject(fetchFn, { ...opts, method: "PUT", url, body }),
42
+ patch: (url, body, opts = {}) => doInject(fetchFn, { ...opts, method: "PATCH", url, body }),
43
+ delete: (url, opts = {}) => doInject(fetchFn, { ...opts, method: "DELETE", url })
44
+ };
45
+ }
46
+ function createTestApp(config) {
47
+ return createTestClient(createKozo(config));
48
+ }
49
+ export {
50
+ createTestApp,
51
+ createTestClient
52
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@kozojs/testing",
3
+ "version": "0.1.0",
4
+ "description": "In-process test client for Kozo Framework — no HTTP server required",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "files": [
9
+ "lib",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/index.d.ts",
15
+ "import": "./lib/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ },
24
+ "keywords": [
25
+ "kozo",
26
+ "testing",
27
+ "test",
28
+ "inject",
29
+ "supertest",
30
+ "http",
31
+ "typescript"
32
+ ],
33
+ "author": "Kozo Team",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@kozojs/core": ">=0.3.10"
40
+ },
41
+ "devDependencies": {
42
+ "@kozojs/core": "workspace:*",
43
+ "@types/node": "^22.0.0",
44
+ "tsup": "^8.3.0",
45
+ "typescript": "^5.6.0",
46
+ "vitest": "^2.1.0"
47
+ }
48
+ }