@vertz/testing 0.0.2

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.
@@ -0,0 +1,57 @@
1
+ import { NamedMiddlewareDef, NamedModule, NamedServiceDef } from "@vertz/core";
2
+ type DeepPartial<T> = { [P in keyof T]? : T[P] extends object ? DeepPartial<T[P]> : T[P] };
3
+ interface TestResponse {
4
+ status: number;
5
+ body: unknown;
6
+ headers: Record<string, string>;
7
+ ok: boolean;
8
+ }
9
+ interface TestRequestBuilder extends PromiseLike<TestResponse> {
10
+ mock<
11
+ TDeps,
12
+ TState,
13
+ TMethods
14
+ >(service: NamedServiceDef<TDeps, TState, TMethods>, impl: DeepPartial<TMethods>): TestRequestBuilder;
15
+ mockMiddleware<
16
+ TReq extends Record<string, unknown>,
17
+ TProv extends Record<string, unknown>
18
+ >(middleware: NamedMiddlewareDef<TReq, TProv>, result: TProv): TestRequestBuilder;
19
+ }
20
+ interface RequestOptions {
21
+ body?: unknown;
22
+ headers?: Record<string, string>;
23
+ }
24
+ interface TestApp {
25
+ register(module: NamedModule, options?: Record<string, unknown>): TestApp;
26
+ mock<
27
+ TDeps,
28
+ TState,
29
+ TMethods
30
+ >(service: NamedServiceDef<TDeps, TState, TMethods>, impl: DeepPartial<TMethods>): TestApp;
31
+ mockMiddleware<
32
+ TReq extends Record<string, unknown>,
33
+ TProv extends Record<string, unknown>
34
+ >(middleware: NamedMiddlewareDef<TReq, TProv>, result: TProv): TestApp;
35
+ env(vars: Record<string, unknown>): TestApp;
36
+ get(path: string, options?: RequestOptions): TestRequestBuilder;
37
+ post(path: string, options?: RequestOptions): TestRequestBuilder;
38
+ put(path: string, options?: RequestOptions): TestRequestBuilder;
39
+ patch(path: string, options?: RequestOptions): TestRequestBuilder;
40
+ delete(path: string, options?: RequestOptions): TestRequestBuilder;
41
+ head(path: string, options?: RequestOptions): TestRequestBuilder;
42
+ }
43
+ declare function createTestApp(): TestApp;
44
+ import { NamedServiceDef as NamedServiceDef2 } from "@vertz/core";
45
+ interface TestServiceBuilder<TMethods> extends PromiseLike<TMethods> {
46
+ mock<
47
+ TDep,
48
+ TState,
49
+ TMock
50
+ >(service: NamedServiceDef2<TDep, TState, TMock>, impl: DeepPartial<TMock>): TestServiceBuilder<TMethods>;
51
+ }
52
+ declare function createTestService<
53
+ TDeps,
54
+ TState,
55
+ TMethods
56
+ >(serviceDef: NamedServiceDef2<TDeps, TState, TMethods>): TestServiceBuilder<TMethods>;
57
+ export { createTestService, createTestApp, TestServiceBuilder, TestResponse, TestRequestBuilder, TestApp, DeepPartial };
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ // src/test-app.ts
2
+ import {
3
+ BadRequestException
4
+ } from "@vertz/core";
5
+ import {
6
+ buildCtx,
7
+ createErrorResponse,
8
+ createJsonResponse,
9
+ parseBody,
10
+ parseRequest,
11
+ runMiddlewareChain,
12
+ Trie
13
+ } from "@vertz/core/internals";
14
+
15
+ class ResponseValidationError extends Error {
16
+ constructor(message) {
17
+ super(`Response validation failed: ${message}`);
18
+ this.name = "ResponseValidationError";
19
+ }
20
+ }
21
+ function validateSchema(schema, value, label) {
22
+ try {
23
+ return schema.parse(value);
24
+ } catch (error) {
25
+ if (error instanceof BadRequestException)
26
+ throw error;
27
+ const message = error instanceof Error ? error.message : `Invalid ${label}`;
28
+ throw new BadRequestException(message);
29
+ }
30
+ }
31
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
32
+ function createTestApp() {
33
+ const serviceMocks = new Map;
34
+ const middlewareMocks = new Map;
35
+ const registrations = [];
36
+ let envOverrides = {};
37
+ function buildHandler(perRequest) {
38
+ const trie = new Trie;
39
+ const realServices = new Map;
40
+ for (const { module } of registrations) {
41
+ for (const service of module.services) {
42
+ if (!realServices.has(service)) {
43
+ realServices.set(service, service.methods({}, undefined));
44
+ }
45
+ }
46
+ }
47
+ const serviceMap = new Map([...realServices, ...serviceMocks, ...perRequest.services]);
48
+ for (const { module, options } of registrations) {
49
+ for (const router of module.routers) {
50
+ const resolvedServices = {};
51
+ if (router.inject) {
52
+ for (const [name, serviceDef] of Object.entries(router.inject)) {
53
+ resolvedServices[name] = serviceMap.get(serviceDef);
54
+ }
55
+ }
56
+ for (const route of router.routes) {
57
+ const fullPath = router.prefix + route.path;
58
+ const entry = {
59
+ handler: route.config.handler,
60
+ options: options ?? {},
61
+ services: resolvedServices,
62
+ responseSchema: route.config.response,
63
+ bodySchema: route.config.body,
64
+ querySchema: route.config.query,
65
+ headersSchema: route.config.headers
66
+ };
67
+ trie.add(route.method, fullPath, entry);
68
+ }
69
+ }
70
+ }
71
+ const effectiveMiddlewareMocks = new Map([...middlewareMocks, ...perRequest.middlewares]);
72
+ return async (request) => {
73
+ try {
74
+ const parsed = parseRequest(request);
75
+ const match = trie.match(parsed.method, parsed.path);
76
+ if (!match) {
77
+ const allowed = trie.getAllowedMethods(parsed.path);
78
+ if (allowed.length > 0) {
79
+ return createJsonResponse({ error: "MethodNotAllowed", message: "Method Not Allowed", statusCode: 405 }, 405, { allow: allowed.join(", ") });
80
+ }
81
+ return createJsonResponse({ error: "NotFound", message: "Not Found", statusCode: 404 }, 404);
82
+ }
83
+ const body = await parseBody(request);
84
+ const raw = {
85
+ request: parsed.raw,
86
+ method: parsed.method,
87
+ url: parsed.raw.url,
88
+ headers: parsed.raw.headers
89
+ };
90
+ const shared = {
91
+ params: match.params,
92
+ body,
93
+ query: parsed.query,
94
+ headers: parsed.headers,
95
+ raw
96
+ };
97
+ const resolvedMiddlewares = [...effectiveMiddlewareMocks].map(([mw, mockResult]) => ({
98
+ name: mw.name,
99
+ handler: () => mockResult,
100
+ resolvedInject: {}
101
+ }));
102
+ const middlewareState = await runMiddlewareChain(resolvedMiddlewares, shared);
103
+ const entry = match.handler;
104
+ const validatedBody = entry.bodySchema ? validateSchema(entry.bodySchema, body, "body") : body;
105
+ const validatedQuery = entry.querySchema ? validateSchema(entry.querySchema, parsed.query, "query") : parsed.query;
106
+ const validatedHeaders = entry.headersSchema ? validateSchema(entry.headersSchema, parsed.headers, "headers") : parsed.headers;
107
+ const ctx = buildCtx({
108
+ params: match.params,
109
+ body: validatedBody,
110
+ query: validatedQuery,
111
+ headers: validatedHeaders,
112
+ raw,
113
+ middlewareState,
114
+ services: entry.services,
115
+ options: entry.options,
116
+ env: envOverrides
117
+ });
118
+ const result = await entry.handler(ctx);
119
+ if (entry.responseSchema) {
120
+ const validation = entry.responseSchema.safeParse(result);
121
+ if (!validation.success) {
122
+ throw new ResponseValidationError(validation.error?.message ?? "Unknown validation error");
123
+ }
124
+ }
125
+ return result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
126
+ } catch (error) {
127
+ if (error instanceof ResponseValidationError)
128
+ throw error;
129
+ return createErrorResponse(error);
130
+ }
131
+ };
132
+ }
133
+ async function executeRequest(method, path, options, perRequest) {
134
+ const handler = buildHandler(perRequest);
135
+ const { body, headers: customHeaders } = options ?? {};
136
+ const headers = { ...customHeaders };
137
+ if (body !== undefined) {
138
+ headers["content-type"] = "application/json";
139
+ }
140
+ const request = new Request(`http://localhost${path}`, {
141
+ method,
142
+ body: body !== undefined ? JSON.stringify(body) : undefined,
143
+ headers
144
+ });
145
+ const response = await handler(request);
146
+ const isJson = response.headers.get("content-type")?.includes("application/json");
147
+ const responseBody = isJson ? await response.json() : null;
148
+ return {
149
+ status: response.status,
150
+ body: responseBody,
151
+ headers: Object.fromEntries(response.headers),
152
+ ok: response.ok
153
+ };
154
+ }
155
+ function createRequestBuilder(method, path, options) {
156
+ const perRequest = {
157
+ services: new Map,
158
+ middlewares: new Map
159
+ };
160
+ const builder = {
161
+ mock(service, impl) {
162
+ perRequest.services.set(service, impl);
163
+ return builder;
164
+ },
165
+ mockMiddleware(middleware, result) {
166
+ perRequest.middlewares.set(middleware, result);
167
+ return builder;
168
+ },
169
+ then(onfulfilled, onrejected) {
170
+ return executeRequest(method, path, options, perRequest).then(onfulfilled, onrejected);
171
+ }
172
+ };
173
+ return builder;
174
+ }
175
+ const httpMethods = Object.fromEntries(HTTP_METHODS.map((m) => [
176
+ m.toLowerCase(),
177
+ (path, options) => createRequestBuilder(m, path, options)
178
+ ]));
179
+ const app = {
180
+ register(module, options) {
181
+ registrations.push({ module, options });
182
+ return app;
183
+ },
184
+ mock(service, impl) {
185
+ serviceMocks.set(service, impl);
186
+ return app;
187
+ },
188
+ mockMiddleware(middleware, result) {
189
+ middlewareMocks.set(middleware, result);
190
+ return app;
191
+ },
192
+ env(vars) {
193
+ envOverrides = vars;
194
+ return app;
195
+ },
196
+ ...httpMethods
197
+ };
198
+ return app;
199
+ }
200
+ // src/test-service.ts
201
+ function createTestService(serviceDef) {
202
+ const serviceMocks = new Map;
203
+ async function resolve() {
204
+ const deps = {};
205
+ if (serviceDef.inject) {
206
+ for (const [name, depDef] of Object.entries(serviceDef.inject)) {
207
+ const mock = serviceMocks.get(depDef);
208
+ if (mock === undefined) {
209
+ throw new Error(`Missing mock for injected dependency "${name}". Call .mock(${name}Service, impl) before awaiting.`);
210
+ }
211
+ deps[name] = mock;
212
+ }
213
+ }
214
+ const state = serviceDef.onInit ? await serviceDef.onInit(deps) : undefined;
215
+ return serviceDef.methods(deps, state);
216
+ }
217
+ const builder = {
218
+ mock(service, impl) {
219
+ serviceMocks.set(service, impl);
220
+ return builder;
221
+ },
222
+ then(onfulfilled, onrejected) {
223
+ return resolve().then(onfulfilled, onrejected);
224
+ }
225
+ };
226
+ return builder;
227
+ }
228
+ export {
229
+ createTestService,
230
+ createTestApp
231
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@vertz/testing",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "bunup",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@vertz/core": "0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.2.1",
27
+ "@vertz/schema": "0.1.0",
28
+ "bunup": "latest",
29
+ "typescript": "^5.7.0",
30
+ "vitest": "^3.0.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ }
35
+ }