@typokit/testing 0.1.4
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/dist/contract-generator.d.ts +65 -0
- package/dist/contract-generator.d.ts.map +1 -0
- package/dist/contract-generator.js +325 -0
- package/dist/contract-generator.js.map +1 -0
- package/dist/factory.d.ts +27 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +194 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/integration-suite.d.ts +66 -0
- package/dist/integration-suite.d.ts.map +1 -0
- package/dist/integration-suite.js +110 -0
- package/dist/integration-suite.js.map +1 -0
- package/dist/schema-matcher.d.ts +78 -0
- package/dist/schema-matcher.d.ts.map +1 -0
- package/dist/schema-matcher.js +99 -0
- package/dist/schema-matcher.js.map +1 -0
- package/package.json +34 -0
- package/src/contract-generator.test.ts +356 -0
- package/src/contract-generator.ts +412 -0
- package/src/factory.test.ts +217 -0
- package/src/factory.ts +248 -0
- package/src/index.test.ts +275 -0
- package/src/index.ts +284 -0
- package/src/integration-suite.test.ts +336 -0
- package/src/integration-suite.ts +191 -0
- package/src/schema-matcher.test.ts +293 -0
- package/src/schema-matcher.ts +160 -0
package/src/factory.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @typokit/testing — Test Factories
|
|
2
|
+
//
|
|
3
|
+
// Type-safe test factories that produce valid/invalid fixture data
|
|
4
|
+
// from TypeMetadata. Deterministic when seeded.
|
|
5
|
+
|
|
6
|
+
import type { TypeMetadata } from "@typokit/types";
|
|
7
|
+
|
|
8
|
+
// ─── Seeded PRNG (mulberry32) ─────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function mulberry32(seed: number): () => number {
|
|
11
|
+
let s = seed | 0;
|
|
12
|
+
return () => {
|
|
13
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
14
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
15
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
16
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Types ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Options for creating a factory */
|
|
23
|
+
export interface FactoryOptions {
|
|
24
|
+
/** Seed for deterministic random generation */
|
|
25
|
+
seed?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A test factory that produces typed instances */
|
|
29
|
+
export interface Factory<T> {
|
|
30
|
+
/** Build a single valid instance with optional field overrides */
|
|
31
|
+
build(overrides?: Partial<T>): T;
|
|
32
|
+
/** Build multiple valid instances */
|
|
33
|
+
buildMany(count: number, overrides?: Partial<T>): T[];
|
|
34
|
+
/** Build an instance with a specific field set to an invalid value */
|
|
35
|
+
buildInvalid(field: keyof T & string): T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Random Data Generators ───────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function randomString(rand: () => number, length: number): string {
|
|
41
|
+
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
42
|
+
let result = "";
|
|
43
|
+
for (let i = 0; i < length; i++) {
|
|
44
|
+
result += chars[Math.floor(rand() * chars.length)];
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function randomEmail(rand: () => number): string {
|
|
50
|
+
return `${randomString(rand, 8)}@${randomString(rand, 5)}.com`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function randomUrl(rand: () => number): string {
|
|
54
|
+
return `https://${randomString(rand, 8)}.com/${randomString(rand, 4)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function randomUuid(rand: () => number): string {
|
|
58
|
+
const hex = "0123456789abcdef";
|
|
59
|
+
const segments = [8, 4, 4, 4, 12];
|
|
60
|
+
return segments
|
|
61
|
+
.map((len) => {
|
|
62
|
+
let s = "";
|
|
63
|
+
for (let i = 0; i < len; i++) {
|
|
64
|
+
s += hex[Math.floor(rand() * 16)];
|
|
65
|
+
}
|
|
66
|
+
return s;
|
|
67
|
+
})
|
|
68
|
+
.join("-");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function randomInt(rand: () => number, min: number, max: number): number {
|
|
72
|
+
return Math.floor(rand() * (max - min + 1)) + min;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function randomDate(rand: () => number): string {
|
|
76
|
+
const year = randomInt(rand, 2020, 2030);
|
|
77
|
+
const month = String(randomInt(rand, 1, 12)).padStart(2, "0");
|
|
78
|
+
const day = String(randomInt(rand, 1, 28)).padStart(2, "0");
|
|
79
|
+
return `${year}-${month}-${day}T00:00:00.000Z`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Value Generator ──────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function generateValue(
|
|
85
|
+
type: string,
|
|
86
|
+
jsdoc: Record<string, string> | undefined,
|
|
87
|
+
rand: () => number,
|
|
88
|
+
): unknown {
|
|
89
|
+
const format = jsdoc?.["format"];
|
|
90
|
+
const minLengthStr = jsdoc?.["minLength"];
|
|
91
|
+
const maxLengthStr = jsdoc?.["maxLength"];
|
|
92
|
+
const minStr = jsdoc?.["minimum"];
|
|
93
|
+
const maxStr = jsdoc?.["maximum"];
|
|
94
|
+
|
|
95
|
+
// Check for JSDoc format constraints first
|
|
96
|
+
if (format === "email") return randomEmail(rand);
|
|
97
|
+
if (format === "url" || format === "uri") return randomUrl(rand);
|
|
98
|
+
if (format === "uuid") return randomUuid(rand);
|
|
99
|
+
if (format === "date" || format === "date-time") return randomDate(rand);
|
|
100
|
+
|
|
101
|
+
// String union types like '"a" | "b" | "c"'
|
|
102
|
+
if (type.includes('" | "') || type.includes("' | '")) {
|
|
103
|
+
const values = type
|
|
104
|
+
.split("|")
|
|
105
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ""));
|
|
106
|
+
return values[Math.floor(rand() * values.length)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle base types
|
|
110
|
+
const baseType = type.replace(/\[\]$/, "");
|
|
111
|
+
const isArray = type.endsWith("[]");
|
|
112
|
+
|
|
113
|
+
const gen = (): unknown => {
|
|
114
|
+
switch (baseType) {
|
|
115
|
+
case "string": {
|
|
116
|
+
const minLen = minLengthStr ? parseInt(minLengthStr, 10) : 5;
|
|
117
|
+
const maxLen = maxLengthStr ? parseInt(maxLengthStr, 10) : 20;
|
|
118
|
+
const len = randomInt(rand, minLen, maxLen);
|
|
119
|
+
return randomString(rand, len);
|
|
120
|
+
}
|
|
121
|
+
case "number": {
|
|
122
|
+
const min = minStr ? parseInt(minStr, 10) : 1;
|
|
123
|
+
const max = maxStr ? parseInt(maxStr, 10) : 1000;
|
|
124
|
+
return randomInt(rand, min, max);
|
|
125
|
+
}
|
|
126
|
+
case "boolean":
|
|
127
|
+
return rand() > 0.5;
|
|
128
|
+
case "Date":
|
|
129
|
+
return randomDate(rand);
|
|
130
|
+
default:
|
|
131
|
+
return randomString(rand, 10);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (isArray) {
|
|
136
|
+
const count = randomInt(rand, 1, 3);
|
|
137
|
+
return Array.from({ length: count }, gen);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return gen();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Invalid Value Generator ──────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function generateInvalidValue(
|
|
146
|
+
type: string,
|
|
147
|
+
jsdoc: Record<string, string> | undefined,
|
|
148
|
+
): unknown {
|
|
149
|
+
const format = jsdoc?.["format"];
|
|
150
|
+
const minLengthStr = jsdoc?.["minLength"];
|
|
151
|
+
const maxStr = jsdoc?.["maximum"];
|
|
152
|
+
const minStr = jsdoc?.["minimum"];
|
|
153
|
+
|
|
154
|
+
if (format === "email") return "not-an-email";
|
|
155
|
+
if (format === "url" || format === "uri") return "not a url";
|
|
156
|
+
if (format === "uuid") return "not-a-uuid";
|
|
157
|
+
if (format === "date" || format === "date-time") return "not-a-date";
|
|
158
|
+
|
|
159
|
+
if (type.includes('" | "') || type.includes("' | '")) {
|
|
160
|
+
return "__invalid_enum_value__";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const baseType = type.replace(/\[\]$/, "");
|
|
164
|
+
|
|
165
|
+
switch (baseType) {
|
|
166
|
+
case "string": {
|
|
167
|
+
if (minLengthStr) {
|
|
168
|
+
const minLen = parseInt(minLengthStr, 10);
|
|
169
|
+
return minLen > 1 ? "x" : "";
|
|
170
|
+
}
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
case "number": {
|
|
174
|
+
if (maxStr) return parseInt(maxStr, 10) + 100;
|
|
175
|
+
if (minStr) return parseInt(minStr, 10) - 100;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
case "boolean":
|
|
179
|
+
return "not-a-boolean";
|
|
180
|
+
default:
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── createFactory ────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a type-safe test factory from TypeMetadata.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* const userFactory = createFactory<User>(userMetadata, { seed: 42 });
|
|
192
|
+
* const user = userFactory.build();
|
|
193
|
+
* const admin = userFactory.build({ role: "admin" });
|
|
194
|
+
* const invalid = userFactory.buildInvalid("email");
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function createFactory<T>(
|
|
198
|
+
metadata: TypeMetadata,
|
|
199
|
+
options: FactoryOptions = {},
|
|
200
|
+
): Factory<T> {
|
|
201
|
+
const seed = options.seed ?? 12345;
|
|
202
|
+
|
|
203
|
+
function buildOne(rand: () => number, overrides?: Partial<T>): T {
|
|
204
|
+
const result: Record<string, unknown> = {};
|
|
205
|
+
|
|
206
|
+
for (const [key, prop] of Object.entries(metadata.properties)) {
|
|
207
|
+
if (prop.optional && rand() > 0.7) {
|
|
208
|
+
continue; // skip some optional fields
|
|
209
|
+
}
|
|
210
|
+
result[key] = generateValue(prop.type, prop.jsdoc, rand);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (overrides) {
|
|
214
|
+
Object.assign(result, overrides);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result as T;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
build(overrides?: Partial<T>): T {
|
|
222
|
+
const rand = mulberry32(seed);
|
|
223
|
+
return buildOne(rand, overrides);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
buildMany(count: number, overrides?: Partial<T>): T[] {
|
|
227
|
+
const rand = mulberry32(seed);
|
|
228
|
+
const results: T[] = [];
|
|
229
|
+
for (let i = 0; i < count; i++) {
|
|
230
|
+
results.push(buildOne(rand, overrides));
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
buildInvalid(field: keyof T & string): T {
|
|
236
|
+
const rand = mulberry32(seed);
|
|
237
|
+
const instance = buildOne(rand);
|
|
238
|
+
const prop = metadata.properties[field];
|
|
239
|
+
if (prop) {
|
|
240
|
+
(instance as Record<string, unknown>)[field] = generateInvalidValue(
|
|
241
|
+
prop.type,
|
|
242
|
+
prop.jsdoc,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return instance;
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// @typokit/testing — Integration Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import type {
|
|
5
|
+
CompiledRoute,
|
|
6
|
+
CompiledRouteTable,
|
|
7
|
+
HandlerMap,
|
|
8
|
+
MiddlewareChain,
|
|
9
|
+
RouteContract,
|
|
10
|
+
TypoKitRequest,
|
|
11
|
+
TypoKitResponse,
|
|
12
|
+
} from "@typokit/types";
|
|
13
|
+
import { createApp } from "@typokit/core";
|
|
14
|
+
import { nativeServer } from "@typokit/server-native";
|
|
15
|
+
import { createTestClient } from "./index.js";
|
|
16
|
+
|
|
17
|
+
// ─── Test Helpers ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function makeRouteTable(): CompiledRouteTable {
|
|
20
|
+
const root: CompiledRoute = {
|
|
21
|
+
segment: "",
|
|
22
|
+
handlers: {
|
|
23
|
+
GET: { ref: "root#index", middleware: [] },
|
|
24
|
+
},
|
|
25
|
+
children: {
|
|
26
|
+
users: {
|
|
27
|
+
segment: "users",
|
|
28
|
+
handlers: {
|
|
29
|
+
GET: { ref: "users#list", middleware: [] },
|
|
30
|
+
POST: { ref: "users#create", middleware: [] },
|
|
31
|
+
},
|
|
32
|
+
paramChild: {
|
|
33
|
+
segment: ":id",
|
|
34
|
+
paramName: "id",
|
|
35
|
+
handlers: {
|
|
36
|
+
GET: { ref: "users#get", middleware: [] },
|
|
37
|
+
PUT: { ref: "users#update", middleware: [] },
|
|
38
|
+
DELETE: { ref: "users#delete", middleware: [] },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
echo: {
|
|
43
|
+
segment: "echo",
|
|
44
|
+
handlers: {
|
|
45
|
+
POST: { ref: "echo#post", middleware: [] },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeHandlerMap(): HandlerMap {
|
|
54
|
+
const users = [
|
|
55
|
+
{ id: "1", name: "Alice" },
|
|
56
|
+
{ id: "2", name: "Bob" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"root#index": async (): Promise<TypoKitResponse> => ({
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { "content-type": "application/json" },
|
|
63
|
+
body: { message: "Hello, TypoKit!" },
|
|
64
|
+
}),
|
|
65
|
+
"users#list": async (req: TypoKitRequest): Promise<TypoKitResponse> => {
|
|
66
|
+
const limit = req.query["limit"];
|
|
67
|
+
const data = limit ? users.slice(0, Number(limit)) : users;
|
|
68
|
+
return {
|
|
69
|
+
status: 200,
|
|
70
|
+
headers: { "content-type": "application/json" },
|
|
71
|
+
body: { data },
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
"users#create": async (req: TypoKitRequest): Promise<TypoKitResponse> => {
|
|
75
|
+
const newUser = req.body as { name: string };
|
|
76
|
+
return {
|
|
77
|
+
status: 201,
|
|
78
|
+
headers: { "content-type": "application/json" },
|
|
79
|
+
body: { id: "3", name: newUser.name },
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
"users#get": async (req: TypoKitRequest): Promise<TypoKitResponse> => {
|
|
83
|
+
const user = users.find((u) => u.id === req.params["id"]);
|
|
84
|
+
if (!user) {
|
|
85
|
+
return {
|
|
86
|
+
status: 404,
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
body: { error: "Not Found" },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: 200,
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: user,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
"users#update": async (req: TypoKitRequest): Promise<TypoKitResponse> => {
|
|
98
|
+
const updates = req.body as { name: string };
|
|
99
|
+
return {
|
|
100
|
+
status: 200,
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body: { id: req.params["id"], name: updates.name },
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
"users#delete": async (_req: TypoKitRequest): Promise<TypoKitResponse> => ({
|
|
106
|
+
status: 204,
|
|
107
|
+
headers: {},
|
|
108
|
+
body: null,
|
|
109
|
+
}),
|
|
110
|
+
"echo#post": async (req: TypoKitRequest): Promise<TypoKitResponse> => ({
|
|
111
|
+
status: 200,
|
|
112
|
+
headers: {
|
|
113
|
+
"content-type": "application/json",
|
|
114
|
+
"x-custom": (req.headers["x-custom"] as string) ?? "",
|
|
115
|
+
},
|
|
116
|
+
body: { echo: req.body },
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createTestApp() {
|
|
122
|
+
const adapter = nativeServer();
|
|
123
|
+
const routeTable = makeRouteTable();
|
|
124
|
+
const handlerMap = makeHandlerMap();
|
|
125
|
+
const middlewareChain: MiddlewareChain = { entries: [] };
|
|
126
|
+
|
|
127
|
+
adapter.registerRoutes(routeTable, handlerMap, middlewareChain);
|
|
128
|
+
|
|
129
|
+
return createApp({
|
|
130
|
+
server: adapter,
|
|
131
|
+
routes: [],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Tests ───────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe("createTestClient", () => {
|
|
138
|
+
it("should start the app and return a client with baseUrl", async () => {
|
|
139
|
+
const app = createTestApp();
|
|
140
|
+
const client = await createTestClient(app);
|
|
141
|
+
try {
|
|
142
|
+
expect(client.baseUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
|
|
143
|
+
} finally {
|
|
144
|
+
await client.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should GET / and return JSON body", async () => {
|
|
149
|
+
const app = createTestApp();
|
|
150
|
+
const client = await createTestClient(app);
|
|
151
|
+
try {
|
|
152
|
+
const res = await client.get<{ message: string }>("/");
|
|
153
|
+
expect(res.status).toBe(200);
|
|
154
|
+
expect(res.body).toEqual({ message: "Hello, TypoKit!" });
|
|
155
|
+
expect(res.headers["content-type"]).toContain("application/json");
|
|
156
|
+
} finally {
|
|
157
|
+
await client.close();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should GET /users and list users", async () => {
|
|
162
|
+
const app = createTestApp();
|
|
163
|
+
const client = await createTestClient(app);
|
|
164
|
+
try {
|
|
165
|
+
const res = await client.get<{
|
|
166
|
+
data: Array<{ id: string; name: string }>;
|
|
167
|
+
}>("/users");
|
|
168
|
+
expect(res.status).toBe(200);
|
|
169
|
+
expect(res.body.data).toHaveLength(2);
|
|
170
|
+
expect(res.body.data[0].name).toBe("Alice");
|
|
171
|
+
} finally {
|
|
172
|
+
await client.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should support query parameters", async () => {
|
|
177
|
+
const app = createTestApp();
|
|
178
|
+
const client = await createTestClient(app);
|
|
179
|
+
try {
|
|
180
|
+
const res = await client.get<{
|
|
181
|
+
data: Array<{ id: string; name: string }>;
|
|
182
|
+
}>("/users", { query: { limit: "1" } });
|
|
183
|
+
expect(res.status).toBe(200);
|
|
184
|
+
expect(res.body.data).toHaveLength(1);
|
|
185
|
+
} finally {
|
|
186
|
+
await client.close();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should POST and create a resource", async () => {
|
|
191
|
+
const app = createTestApp();
|
|
192
|
+
const client = await createTestClient(app);
|
|
193
|
+
try {
|
|
194
|
+
const res = await client.post<{ id: string; name: string }>("/users", {
|
|
195
|
+
body: { name: "Charlie" },
|
|
196
|
+
});
|
|
197
|
+
expect(res.status).toBe(201);
|
|
198
|
+
expect(res.body.name).toBe("Charlie");
|
|
199
|
+
expect(res.body.id).toBe("3");
|
|
200
|
+
} finally {
|
|
201
|
+
await client.close();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should PUT to update a resource", async () => {
|
|
206
|
+
const app = createTestApp();
|
|
207
|
+
const client = await createTestClient(app);
|
|
208
|
+
try {
|
|
209
|
+
const res = await client.put<{ id: string; name: string }>("/users/1", {
|
|
210
|
+
body: { name: "Alice Updated" },
|
|
211
|
+
});
|
|
212
|
+
expect(res.status).toBe(200);
|
|
213
|
+
expect(res.body.name).toBe("Alice Updated");
|
|
214
|
+
} finally {
|
|
215
|
+
await client.close();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should DELETE a resource", async () => {
|
|
220
|
+
const app = createTestApp();
|
|
221
|
+
const client = await createTestClient(app);
|
|
222
|
+
try {
|
|
223
|
+
const res = await client.delete("/users/1");
|
|
224
|
+
expect(res.status).toBe(204);
|
|
225
|
+
} finally {
|
|
226
|
+
await client.close();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should pass custom headers", async () => {
|
|
231
|
+
const app = createTestApp();
|
|
232
|
+
const client = await createTestClient(app);
|
|
233
|
+
try {
|
|
234
|
+
const res = await client.post<{ echo: unknown }>("/echo", {
|
|
235
|
+
body: { test: true },
|
|
236
|
+
headers: { "x-custom": "my-value" },
|
|
237
|
+
});
|
|
238
|
+
expect(res.status).toBe(200);
|
|
239
|
+
expect(res.headers["x-custom"]).toBe("my-value");
|
|
240
|
+
expect(res.body.echo).toEqual({ test: true });
|
|
241
|
+
} finally {
|
|
242
|
+
await client.close();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should return 404 for unknown routes", async () => {
|
|
247
|
+
const app = createTestApp();
|
|
248
|
+
const client = await createTestClient(app);
|
|
249
|
+
try {
|
|
250
|
+
const res = await client.get("/nonexistent");
|
|
251
|
+
expect(res.status).toBe(404);
|
|
252
|
+
} finally {
|
|
253
|
+
await client.close();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should support contract-typed request method", async () => {
|
|
258
|
+
const app = createTestApp();
|
|
259
|
+
const client = await createTestClient(app);
|
|
260
|
+
try {
|
|
261
|
+
const res = await client.request<
|
|
262
|
+
RouteContract<
|
|
263
|
+
void,
|
|
264
|
+
void,
|
|
265
|
+
void,
|
|
266
|
+
{ data: Array<{ id: string; name: string }> }
|
|
267
|
+
>
|
|
268
|
+
>("GET", "/users");
|
|
269
|
+
expect(res.status).toBe(200);
|
|
270
|
+
expect(res.body.data).toHaveLength(2);
|
|
271
|
+
} finally {
|
|
272
|
+
await client.close();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|