@luckystack/test-runner 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 +14 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.js +1270 -0
- package/dist/index.js.map +1 -0
- package/docs/auth-tests.md +226 -0
- package/docs/contract-tests.md +259 -0
- package/docs/extension-hooks.md +438 -0
- package/docs/fuzz-tests.md +249 -0
- package/docs/rate-limit-tests.md +256 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
// src/walkEndpoints.ts
|
|
2
|
+
var walkEndpoints = (apiMethodMap) => {
|
|
3
|
+
const endpoints = [];
|
|
4
|
+
for (const [page, nameMap] of Object.entries(apiMethodMap)) {
|
|
5
|
+
if (!nameMap) continue;
|
|
6
|
+
for (const [name, versionMap] of Object.entries(nameMap)) {
|
|
7
|
+
if (!versionMap) continue;
|
|
8
|
+
for (const [version, method] of Object.entries(versionMap)) {
|
|
9
|
+
if (method === void 0) continue;
|
|
10
|
+
endpoints.push({
|
|
11
|
+
page,
|
|
12
|
+
name,
|
|
13
|
+
version,
|
|
14
|
+
method,
|
|
15
|
+
fullPath: `api/${page}/${name}/${version}`
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return endpoints;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/contractCheck.ts
|
|
24
|
+
import { tryCatch } from "@luckystack/core";
|
|
25
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
|
|
26
|
+
var runContractCheck = async (input) => {
|
|
27
|
+
const { endpoint, baseUrl } = input;
|
|
28
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
29
|
+
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeoutHandle = setTimeout(() => {
|
|
34
|
+
controller.abort();
|
|
35
|
+
}, requestTimeoutMs);
|
|
36
|
+
const [fetchError, response] = await tryCatch(() => fetch(url, {
|
|
37
|
+
method: endpoint.method,
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
//? Browsers attach Origin on state-changing requests; the server's origin
|
|
41
|
+
//? policy fail-closes POST/PUT/DELETE without one. Send the same-origin
|
|
42
|
+
//? value so the request reaches the API pipeline (consumers can override).
|
|
43
|
+
"Origin": new URL(baseUrl).origin,
|
|
44
|
+
...input.headers
|
|
45
|
+
},
|
|
46
|
+
body: endpoint.method === "GET" ? void 0 : JSON.stringify(body),
|
|
47
|
+
signal: controller.signal
|
|
48
|
+
}));
|
|
49
|
+
clearTimeout(timeoutHandle);
|
|
50
|
+
if (fetchError || !response) {
|
|
51
|
+
return {
|
|
52
|
+
endpoint,
|
|
53
|
+
status: "fail",
|
|
54
|
+
reason: fetchError?.message ?? "fetch returned no response",
|
|
55
|
+
durationMs: Date.now() - started
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const [parseError, parsed] = await tryCatch(
|
|
59
|
+
async () => await response.json()
|
|
60
|
+
);
|
|
61
|
+
const durationMs = Date.now() - started;
|
|
62
|
+
if (parseError) {
|
|
63
|
+
return {
|
|
64
|
+
endpoint,
|
|
65
|
+
status: "fail",
|
|
66
|
+
httpStatus: response.status,
|
|
67
|
+
responseStatus: "unknown",
|
|
68
|
+
reason: `JSON parse failed: ${parseError.message}`,
|
|
69
|
+
durationMs
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || parsed.status !== "success" && parsed.status !== "error") {
|
|
73
|
+
return {
|
|
74
|
+
endpoint,
|
|
75
|
+
status: "fail",
|
|
76
|
+
httpStatus: response.status,
|
|
77
|
+
responseStatus: "unknown",
|
|
78
|
+
reason: "Response missing standard `status` envelope",
|
|
79
|
+
durationMs
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (parsed.status === "error" && !parsed.errorCode) {
|
|
83
|
+
return {
|
|
84
|
+
endpoint,
|
|
85
|
+
status: "fail",
|
|
86
|
+
httpStatus: response.status,
|
|
87
|
+
responseStatus: "error",
|
|
88
|
+
reason: "Error response missing `errorCode`",
|
|
89
|
+
durationMs
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
endpoint,
|
|
94
|
+
status: "pass",
|
|
95
|
+
httpStatus: response.status,
|
|
96
|
+
responseStatus: parsed.status,
|
|
97
|
+
errorCode: parsed.errorCode,
|
|
98
|
+
durationMs
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/runContractTests.ts
|
|
103
|
+
var shouldSkip = (endpoint, skip) => {
|
|
104
|
+
if (skip.length === 0) return false;
|
|
105
|
+
const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;
|
|
106
|
+
const versionless = `${endpoint.page}/${endpoint.name}`;
|
|
107
|
+
return skip.includes(versioned) || skip.includes(versionless);
|
|
108
|
+
};
|
|
109
|
+
var runContractTests = async (input) => {
|
|
110
|
+
const endpoints = walkEndpoints(input.apiMethodMap);
|
|
111
|
+
const skip = input.skip ?? [];
|
|
112
|
+
const results = [];
|
|
113
|
+
for (const endpoint of endpoints) {
|
|
114
|
+
if (shouldSkip(endpoint, skip)) {
|
|
115
|
+
const skipped = {
|
|
116
|
+
endpoint,
|
|
117
|
+
status: "skipped",
|
|
118
|
+
durationMs: 0,
|
|
119
|
+
reason: "Explicitly skipped"
|
|
120
|
+
};
|
|
121
|
+
results.push(skipped);
|
|
122
|
+
input.onResult?.(skipped);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const result = await runContractCheck({
|
|
126
|
+
endpoint,
|
|
127
|
+
baseUrl: input.baseUrl,
|
|
128
|
+
inputFor: input.inputFor,
|
|
129
|
+
headers: input.headers
|
|
130
|
+
});
|
|
131
|
+
results.push(result);
|
|
132
|
+
input.onResult?.(result);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
total: results.length,
|
|
136
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
137
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
138
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
139
|
+
results
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/authEnforcementCheck.ts
|
|
144
|
+
import { tryCatch as tryCatch2 } from "@luckystack/core";
|
|
145
|
+
var DEFAULT_REQUEST_TIMEOUT_MS2 = 5e3;
|
|
146
|
+
var EXPECTED_ERROR_CODE = "auth.required";
|
|
147
|
+
var runAuthEnforcementCheck = async (input) => {
|
|
148
|
+
const { endpoint, baseUrl } = input;
|
|
149
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
150
|
+
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timeoutHandle = setTimeout(() => {
|
|
155
|
+
controller.abort();
|
|
156
|
+
}, requestTimeoutMs);
|
|
157
|
+
const [fetchError, response] = await tryCatch2(() => fetch(url, {
|
|
158
|
+
method: endpoint.method,
|
|
159
|
+
//? No session cookie, no auth header — deliberately. Origin IS sent so the
|
|
160
|
+
//? server's origin policy doesn't 403 before the auth check can run.
|
|
161
|
+
headers: { "Content-Type": "application/json", "Origin": new URL(baseUrl).origin },
|
|
162
|
+
body: endpoint.method === "GET" ? void 0 : JSON.stringify(body),
|
|
163
|
+
signal: controller.signal
|
|
164
|
+
}));
|
|
165
|
+
clearTimeout(timeoutHandle);
|
|
166
|
+
if (fetchError || !response) {
|
|
167
|
+
return {
|
|
168
|
+
endpoint,
|
|
169
|
+
status: "fail",
|
|
170
|
+
reason: fetchError?.message ?? "fetch returned no response",
|
|
171
|
+
durationMs: Date.now() - started
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const [parseError, parsed] = await tryCatch2(
|
|
175
|
+
async () => await response.json()
|
|
176
|
+
);
|
|
177
|
+
const durationMs = Date.now() - started;
|
|
178
|
+
if (parseError) {
|
|
179
|
+
return {
|
|
180
|
+
endpoint,
|
|
181
|
+
status: "fail",
|
|
182
|
+
httpStatus: response.status,
|
|
183
|
+
responseStatus: "unknown",
|
|
184
|
+
reason: `JSON parse failed: ${parseError.message}`,
|
|
185
|
+
durationMs
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (parsed?.status === "success") {
|
|
189
|
+
return {
|
|
190
|
+
endpoint,
|
|
191
|
+
status: "fail",
|
|
192
|
+
httpStatus: response.status,
|
|
193
|
+
responseStatus: "success",
|
|
194
|
+
reason: `auth.login endpoint returned success without a session`,
|
|
195
|
+
durationMs
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (parsed?.status !== "error") {
|
|
199
|
+
return {
|
|
200
|
+
endpoint,
|
|
201
|
+
status: "fail",
|
|
202
|
+
httpStatus: response.status,
|
|
203
|
+
responseStatus: "unknown",
|
|
204
|
+
reason: "Response missing standard `status` envelope",
|
|
205
|
+
durationMs
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (parsed.errorCode !== EXPECTED_ERROR_CODE) {
|
|
209
|
+
return {
|
|
210
|
+
endpoint,
|
|
211
|
+
status: "fail",
|
|
212
|
+
httpStatus: response.status,
|
|
213
|
+
responseStatus: "error",
|
|
214
|
+
errorCode: parsed.errorCode,
|
|
215
|
+
reason: `expected errorCode '${EXPECTED_ERROR_CODE}' but got '${parsed.errorCode ?? "(missing)"}'`,
|
|
216
|
+
durationMs
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
endpoint,
|
|
221
|
+
status: "pass",
|
|
222
|
+
httpStatus: response.status,
|
|
223
|
+
responseStatus: "error",
|
|
224
|
+
errorCode: parsed.errorCode,
|
|
225
|
+
durationMs
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/runAuthEnforcementTests.ts
|
|
230
|
+
var shouldSkip2 = (endpoint, skip) => {
|
|
231
|
+
if (skip.length === 0) return false;
|
|
232
|
+
const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;
|
|
233
|
+
const versionless = `${endpoint.page}/${endpoint.name}`;
|
|
234
|
+
return skip.includes(versioned) || skip.includes(versionless);
|
|
235
|
+
};
|
|
236
|
+
var requiresLogin = (apiMetaMap, endpoint) => {
|
|
237
|
+
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
238
|
+
return meta?.auth.login ?? false;
|
|
239
|
+
};
|
|
240
|
+
var runAuthEnforcementTests = async (input) => {
|
|
241
|
+
const endpoints = walkEndpoints(input.apiMethodMap);
|
|
242
|
+
const skip = input.skip ?? [];
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const endpoint of endpoints) {
|
|
245
|
+
if (!requiresLogin(input.apiMetaMap, endpoint)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (shouldSkip2(endpoint, skip)) {
|
|
249
|
+
const skipped = {
|
|
250
|
+
endpoint,
|
|
251
|
+
status: "skipped",
|
|
252
|
+
durationMs: 0,
|
|
253
|
+
reason: "Explicitly skipped"
|
|
254
|
+
};
|
|
255
|
+
results.push(skipped);
|
|
256
|
+
input.onResult?.(skipped);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const result = await runAuthEnforcementCheck({
|
|
260
|
+
endpoint,
|
|
261
|
+
baseUrl: input.baseUrl,
|
|
262
|
+
inputFor: input.inputFor
|
|
263
|
+
});
|
|
264
|
+
results.push(result);
|
|
265
|
+
input.onResult?.(result);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
total: results.length,
|
|
269
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
270
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
271
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
272
|
+
results
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/rateLimitCheck.ts
|
|
277
|
+
import { tryCatch as tryCatch3 } from "@luckystack/core";
|
|
278
|
+
var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
|
|
279
|
+
var EXPECTED_ERROR_CODE2 = "api.rateLimitExceeded";
|
|
280
|
+
var runRateLimitCheck = async (input) => {
|
|
281
|
+
const { endpoint, baseUrl, rateLimit } = input;
|
|
282
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
283
|
+
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
284
|
+
const started = Date.now();
|
|
285
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS3;
|
|
286
|
+
const send = async () => {
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
const timeoutHandle = setTimeout(() => {
|
|
289
|
+
controller.abort();
|
|
290
|
+
}, requestTimeoutMs);
|
|
291
|
+
const [error, response] = await tryCatch3(() => fetch(url, {
|
|
292
|
+
method: endpoint.method,
|
|
293
|
+
headers: {
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
"Origin": new URL(baseUrl).origin,
|
|
296
|
+
...input.headers
|
|
297
|
+
},
|
|
298
|
+
body: endpoint.method === "GET" ? void 0 : JSON.stringify(body),
|
|
299
|
+
signal: controller.signal
|
|
300
|
+
}));
|
|
301
|
+
clearTimeout(timeoutHandle);
|
|
302
|
+
if (error || !response) return null;
|
|
303
|
+
return response;
|
|
304
|
+
};
|
|
305
|
+
for (let i = 0; i < rateLimit; i += 1) {
|
|
306
|
+
await send();
|
|
307
|
+
}
|
|
308
|
+
const final = await send();
|
|
309
|
+
const durationMs = Date.now() - started;
|
|
310
|
+
if (!final) {
|
|
311
|
+
return {
|
|
312
|
+
endpoint,
|
|
313
|
+
status: "fail",
|
|
314
|
+
reason: "Final rate-limit probe request failed to return a response",
|
|
315
|
+
durationMs
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const [, parsed] = await tryCatch3(
|
|
319
|
+
async () => await final.json()
|
|
320
|
+
);
|
|
321
|
+
if (parsed?.status === "error" && parsed.errorCode === EXPECTED_ERROR_CODE2) {
|
|
322
|
+
return {
|
|
323
|
+
endpoint,
|
|
324
|
+
status: "pass",
|
|
325
|
+
httpStatus: final.status,
|
|
326
|
+
responseStatus: "error",
|
|
327
|
+
errorCode: parsed.errorCode,
|
|
328
|
+
durationMs
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
endpoint,
|
|
333
|
+
status: "fail",
|
|
334
|
+
httpStatus: final.status,
|
|
335
|
+
responseStatus: parsed?.status ?? "unknown",
|
|
336
|
+
errorCode: parsed?.errorCode,
|
|
337
|
+
reason: `expected '${EXPECTED_ERROR_CODE2}' on request ${rateLimit + 1} but got ${parsed?.status ?? "none"}/${parsed?.errorCode ?? "(none)"}`,
|
|
338
|
+
durationMs
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/resetServerState.ts
|
|
343
|
+
import { tryCatch as tryCatch4 } from "@luckystack/core";
|
|
344
|
+
var resetServerState = async ({ baseUrl, token }) => {
|
|
345
|
+
const url = `${baseUrl.replace(/\/$/, "")}/_test/reset`;
|
|
346
|
+
const headers = { "Content-Type": "application/json" };
|
|
347
|
+
if (token) headers["X-Test-Reset-Token"] = token;
|
|
348
|
+
const [error, response] = await tryCatch4(() => fetch(url, { method: "POST", headers }));
|
|
349
|
+
if (error || !response) return false;
|
|
350
|
+
return response.ok;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/runRateLimitTests.ts
|
|
354
|
+
var getRateLimit = (apiMetaMap, endpoint) => {
|
|
355
|
+
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
356
|
+
if (!meta || meta.rateLimit === false || meta.rateLimit === void 0) return null;
|
|
357
|
+
return meta.rateLimit;
|
|
358
|
+
};
|
|
359
|
+
var requiresLogin2 = (apiMetaMap, endpoint) => {
|
|
360
|
+
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
361
|
+
return meta?.auth.login ?? false;
|
|
362
|
+
};
|
|
363
|
+
var shouldSkip3 = (endpoint, skip) => {
|
|
364
|
+
if (skip.length === 0) return false;
|
|
365
|
+
const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;
|
|
366
|
+
const versionless = `${endpoint.page}/${endpoint.name}`;
|
|
367
|
+
return skip.includes(versioned) || skip.includes(versionless);
|
|
368
|
+
};
|
|
369
|
+
var runRateLimitTests = async (input) => {
|
|
370
|
+
const endpoints = walkEndpoints(input.apiMethodMap);
|
|
371
|
+
const skip = input.skip ?? [];
|
|
372
|
+
const maxRateLimit = input.maxRateLimitToTest ?? 50;
|
|
373
|
+
const isAuthenticatedSweep = Boolean(input.headers?.Cookie);
|
|
374
|
+
const results = [];
|
|
375
|
+
for (const endpoint of endpoints) {
|
|
376
|
+
const rateLimit = getRateLimit(input.apiMetaMap, endpoint);
|
|
377
|
+
if (rateLimit === null) continue;
|
|
378
|
+
if (!isAuthenticatedSweep && requiresLogin2(input.apiMetaMap, endpoint)) {
|
|
379
|
+
const skipped = {
|
|
380
|
+
endpoint,
|
|
381
|
+
status: "skipped",
|
|
382
|
+
durationMs: 0,
|
|
383
|
+
reason: "login-required route \u2014 set TEST_AUTH_TOKEN to rate-limit-test it (unauthenticated calls hit auth.required before the limiter)"
|
|
384
|
+
};
|
|
385
|
+
results.push(skipped);
|
|
386
|
+
input.onResult?.(skipped);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (rateLimit > maxRateLimit) {
|
|
390
|
+
const skipped = {
|
|
391
|
+
endpoint,
|
|
392
|
+
status: "skipped",
|
|
393
|
+
durationMs: 0,
|
|
394
|
+
reason: `rateLimit ${rateLimit} exceeds maxRateLimitToTest=${maxRateLimit}`
|
|
395
|
+
};
|
|
396
|
+
results.push(skipped);
|
|
397
|
+
input.onResult?.(skipped);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (shouldSkip3(endpoint, skip)) {
|
|
401
|
+
const skipped = {
|
|
402
|
+
endpoint,
|
|
403
|
+
status: "skipped",
|
|
404
|
+
durationMs: 0,
|
|
405
|
+
reason: "Explicitly skipped"
|
|
406
|
+
};
|
|
407
|
+
results.push(skipped);
|
|
408
|
+
input.onResult?.(skipped);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (input.resetBetweenEndpoints) {
|
|
412
|
+
await resetServerState({ baseUrl: input.baseUrl, token: input.resetToken });
|
|
413
|
+
}
|
|
414
|
+
const result = await runRateLimitCheck({
|
|
415
|
+
endpoint,
|
|
416
|
+
baseUrl: input.baseUrl,
|
|
417
|
+
rateLimit,
|
|
418
|
+
inputFor: input.inputFor,
|
|
419
|
+
headers: input.headers
|
|
420
|
+
});
|
|
421
|
+
results.push(result);
|
|
422
|
+
input.onResult?.(result);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
total: results.length,
|
|
426
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
427
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
428
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
429
|
+
results
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/fuzzCheck.ts
|
|
434
|
+
import { tryCatch as tryCatch5 } from "@luckystack/core";
|
|
435
|
+
var DEFAULT_REQUEST_TIMEOUT_MS4 = 5e3;
|
|
436
|
+
var JUNK_PAYLOADS = [
|
|
437
|
+
null,
|
|
438
|
+
[],
|
|
439
|
+
[1, 2, 3],
|
|
440
|
+
"string-instead-of-object",
|
|
441
|
+
1234567890,
|
|
442
|
+
true,
|
|
443
|
+
{ nested: { deeply: { nested: { value: "x".repeat(1e4) } } } },
|
|
444
|
+
{ __proto__: { polluted: true } },
|
|
445
|
+
{ key: null, other: void 0, third: Number.NaN }
|
|
446
|
+
];
|
|
447
|
+
var probe = async (endpoint, baseUrl, body, headers, requestTimeoutMs) => {
|
|
448
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
449
|
+
const controller = new AbortController();
|
|
450
|
+
const timeoutHandle = setTimeout(() => {
|
|
451
|
+
controller.abort();
|
|
452
|
+
}, requestTimeoutMs);
|
|
453
|
+
const [fetchError, response] = await tryCatch5(() => fetch(url, {
|
|
454
|
+
method: endpoint.method,
|
|
455
|
+
headers: { "Content-Type": "application/json", "Origin": new URL(baseUrl).origin, ...headers },
|
|
456
|
+
body: endpoint.method === "GET" ? void 0 : JSON.stringify(body),
|
|
457
|
+
signal: controller.signal
|
|
458
|
+
}));
|
|
459
|
+
clearTimeout(timeoutHandle);
|
|
460
|
+
if (fetchError || !response) return null;
|
|
461
|
+
const [, parsed] = await tryCatch5(
|
|
462
|
+
async () => await response.json()
|
|
463
|
+
);
|
|
464
|
+
return { httpStatus: response.status, parsed: parsed ?? null };
|
|
465
|
+
};
|
|
466
|
+
var runFuzzCheck = async (input) => {
|
|
467
|
+
const started = Date.now();
|
|
468
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS4;
|
|
469
|
+
for (const payload of JUNK_PAYLOADS) {
|
|
470
|
+
const result = await probe(input.endpoint, input.baseUrl, payload, input.headers, requestTimeoutMs);
|
|
471
|
+
const durationMs = Date.now() - started;
|
|
472
|
+
if (!result) {
|
|
473
|
+
return {
|
|
474
|
+
endpoint: input.endpoint,
|
|
475
|
+
status: "fail",
|
|
476
|
+
reason: `fuzz probe crashed (no response) with payload: ${JSON.stringify(payload).slice(0, 80)}`,
|
|
477
|
+
durationMs
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (!result.parsed || result.parsed.status !== "success" && result.parsed.status !== "error") {
|
|
481
|
+
return {
|
|
482
|
+
endpoint: input.endpoint,
|
|
483
|
+
status: "fail",
|
|
484
|
+
httpStatus: result.httpStatus,
|
|
485
|
+
reason: `fuzz payload produced non-envelope response: ${JSON.stringify(payload).slice(0, 80)}`,
|
|
486
|
+
durationMs
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (result.parsed.status === "error" && typeof result.parsed.errorCode !== "string") {
|
|
490
|
+
return {
|
|
491
|
+
endpoint: input.endpoint,
|
|
492
|
+
status: "fail",
|
|
493
|
+
httpStatus: result.httpStatus,
|
|
494
|
+
reason: `fuzz error response missing errorCode for payload: ${JSON.stringify(payload).slice(0, 80)}`,
|
|
495
|
+
durationMs
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
endpoint: input.endpoint,
|
|
501
|
+
status: "pass",
|
|
502
|
+
durationMs: Date.now() - started
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/runFuzzTests.ts
|
|
507
|
+
var shouldSkip4 = (endpoint, skip) => {
|
|
508
|
+
if (skip.length === 0) return false;
|
|
509
|
+
const versioned = `${endpoint.page}/${endpoint.name}/${endpoint.version}`;
|
|
510
|
+
const versionless = `${endpoint.page}/${endpoint.name}`;
|
|
511
|
+
return skip.includes(versioned) || skip.includes(versionless);
|
|
512
|
+
};
|
|
513
|
+
var runFuzzTests = async (input) => {
|
|
514
|
+
const endpoints = walkEndpoints(input.apiMethodMap);
|
|
515
|
+
const skip = input.skip ?? [];
|
|
516
|
+
const results = [];
|
|
517
|
+
for (const endpoint of endpoints) {
|
|
518
|
+
if (shouldSkip4(endpoint, skip)) {
|
|
519
|
+
const skipped = {
|
|
520
|
+
endpoint,
|
|
521
|
+
status: "skipped",
|
|
522
|
+
durationMs: 0,
|
|
523
|
+
reason: "Explicitly skipped"
|
|
524
|
+
};
|
|
525
|
+
results.push(skipped);
|
|
526
|
+
input.onResult?.(skipped);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const result = await runFuzzCheck({
|
|
530
|
+
endpoint,
|
|
531
|
+
baseUrl: input.baseUrl,
|
|
532
|
+
headers: input.headers
|
|
533
|
+
});
|
|
534
|
+
results.push(result);
|
|
535
|
+
input.onResult?.(result);
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
total: results.length,
|
|
539
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
540
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
541
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
542
|
+
results
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// src/schemaSampleInput.ts
|
|
547
|
+
var sampleForDef = (def) => {
|
|
548
|
+
const d = def;
|
|
549
|
+
const name = d.typeName ?? d.type;
|
|
550
|
+
switch (name) {
|
|
551
|
+
case "ZodString":
|
|
552
|
+
case "string": {
|
|
553
|
+
return "test";
|
|
554
|
+
}
|
|
555
|
+
case "ZodNumber":
|
|
556
|
+
case "number": {
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
case "ZodBoolean":
|
|
560
|
+
case "boolean": {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
case "ZodNull":
|
|
564
|
+
case "null": {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
case "ZodUndefined":
|
|
568
|
+
case "undefined": {
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
571
|
+
case "ZodAny":
|
|
572
|
+
case "any":
|
|
573
|
+
case "ZodUnknown":
|
|
574
|
+
case "unknown": {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
case "ZodLiteral":
|
|
578
|
+
case "literal": {
|
|
579
|
+
return d.value ?? d.values?.[0];
|
|
580
|
+
}
|
|
581
|
+
case "ZodOptional":
|
|
582
|
+
case "optional":
|
|
583
|
+
case "ZodNullable":
|
|
584
|
+
case "nullable": {
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
case "ZodUnion":
|
|
588
|
+
case "union": {
|
|
589
|
+
return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(d.options[0]._def) : null;
|
|
590
|
+
}
|
|
591
|
+
case "ZodArray":
|
|
592
|
+
case "array": {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
case "ZodObject":
|
|
596
|
+
case "object": {
|
|
597
|
+
const out = {};
|
|
598
|
+
const shape = typeof d.shape === "function" ? d.shape() : d.shape ?? {};
|
|
599
|
+
for (const [key, valueSchema] of Object.entries(shape)) {
|
|
600
|
+
const sample = sampleForDef(valueSchema._def);
|
|
601
|
+
if (sample !== void 0) out[key] = sample;
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
case "ZodRecord":
|
|
606
|
+
case "record": {
|
|
607
|
+
return {};
|
|
608
|
+
}
|
|
609
|
+
case "ZodDate":
|
|
610
|
+
case "date": {
|
|
611
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
612
|
+
}
|
|
613
|
+
default: {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
var sampleSchemaInput = (schema) => {
|
|
619
|
+
return sampleForDef(schema.def);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/reporter.ts
|
|
623
|
+
var statusIcon = (status) => {
|
|
624
|
+
switch (status) {
|
|
625
|
+
case "pass": {
|
|
626
|
+
return "[PASS]";
|
|
627
|
+
}
|
|
628
|
+
case "fail": {
|
|
629
|
+
return "[FAIL]";
|
|
630
|
+
}
|
|
631
|
+
case "skipped": {
|
|
632
|
+
return "[SKIP]";
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var logContractResult = (result) => {
|
|
637
|
+
const { endpoint, durationMs, httpStatus, responseStatus, errorCode, reason } = result;
|
|
638
|
+
const icon = statusIcon(result.status);
|
|
639
|
+
const core = `${icon} ${endpoint.method} /${endpoint.fullPath} ${durationMs}ms`;
|
|
640
|
+
if (result.status === "pass") {
|
|
641
|
+
const tail = responseStatus === "error" ? ` (error: ${errorCode ?? "unknown"})` : "";
|
|
642
|
+
console.log(`${core} http=${httpStatus ?? "-"}${tail}`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (result.status === "skipped") {
|
|
646
|
+
console.log(`${core} ${reason ?? ""}`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(`${core} http=${httpStatus ?? "-"} reason=${reason ?? "(none)"}`);
|
|
650
|
+
};
|
|
651
|
+
var logContractSummary = (summary) => {
|
|
652
|
+
const { total, passed, failed, skipped } = summary;
|
|
653
|
+
console.log("");
|
|
654
|
+
console.log(`Contract tests: ${passed}/${total} passed, ${failed} failed, ${skipped} skipped`);
|
|
655
|
+
if (failed > 0) {
|
|
656
|
+
console.log("");
|
|
657
|
+
console.log("Failures:");
|
|
658
|
+
for (const result of summary.results) {
|
|
659
|
+
if (result.status !== "fail") continue;
|
|
660
|
+
console.log(` - /${result.endpoint.fullPath}: ${result.reason ?? "(no reason)"}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/customTests.ts
|
|
666
|
+
import fs from "fs";
|
|
667
|
+
import path from "path";
|
|
668
|
+
import { pathToFileURL } from "url";
|
|
669
|
+
import { getCsrfConfig, getPrismaClient, getProjectConfig, getSrcDir, tryCatch as tryCatch7 } from "@luckystack/core";
|
|
670
|
+
|
|
671
|
+
// src/streamWatcher.ts
|
|
672
|
+
import { io as createSocketIoClient } from "socket.io-client";
|
|
673
|
+
import {
|
|
674
|
+
buildJoinRoomResponseEventName,
|
|
675
|
+
socketEventNames,
|
|
676
|
+
tryCatch as tryCatch6
|
|
677
|
+
} from "@luckystack/core";
|
|
678
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
679
|
+
var JOIN_RESPONSE_TIMEOUT_MS = 3e3;
|
|
680
|
+
var CONNECT_TIMEOUT_MS = 3e3;
|
|
681
|
+
var isJoinRoomResponse = (value) => typeof value === "object" && value !== null;
|
|
682
|
+
var isStreamChunkFrame = (value) => typeof value === "object" && value !== null;
|
|
683
|
+
var responseIndexCounter = 0;
|
|
684
|
+
var nextResponseIndex = () => {
|
|
685
|
+
responseIndexCounter += 1;
|
|
686
|
+
return responseIndexCounter;
|
|
687
|
+
};
|
|
688
|
+
var waitForConnect = (socket) => {
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
if (socket.connected) {
|
|
691
|
+
resolve();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const timer = setTimeout(() => {
|
|
695
|
+
cleanup();
|
|
696
|
+
reject(new Error(`watchStream: socket failed to connect within ${CONNECT_TIMEOUT_MS}ms`));
|
|
697
|
+
}, CONNECT_TIMEOUT_MS);
|
|
698
|
+
const onConnect = () => {
|
|
699
|
+
cleanup();
|
|
700
|
+
resolve();
|
|
701
|
+
};
|
|
702
|
+
const onConnectError = (err) => {
|
|
703
|
+
cleanup();
|
|
704
|
+
reject(new Error(`watchStream: connect_error \u2014 ${err.message}`));
|
|
705
|
+
};
|
|
706
|
+
const cleanup = () => {
|
|
707
|
+
clearTimeout(timer);
|
|
708
|
+
socket.off("connect", onConnect);
|
|
709
|
+
socket.off("connect_error", onConnectError);
|
|
710
|
+
};
|
|
711
|
+
socket.on("connect", onConnect);
|
|
712
|
+
socket.on("connect_error", onConnectError);
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
var waitForJoinAck = (socket, responseIndex) => {
|
|
716
|
+
const eventName = buildJoinRoomResponseEventName(responseIndex);
|
|
717
|
+
return new Promise((resolve, reject) => {
|
|
718
|
+
const timer = setTimeout(() => {
|
|
719
|
+
socket.off(eventName, onAck);
|
|
720
|
+
reject(new Error(`watchStream: joinRoom ack timeout after ${JOIN_RESPONSE_TIMEOUT_MS}ms`));
|
|
721
|
+
}, JOIN_RESPONSE_TIMEOUT_MS);
|
|
722
|
+
const onAck = (payload) => {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
socket.off(eventName, onAck);
|
|
725
|
+
if (isJoinRoomResponse(payload) && payload.status === "error") {
|
|
726
|
+
reject(new Error(`watchStream: joinRoom rejected \u2014 ${payload.errorCode ?? "unknown"}`));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
resolve();
|
|
730
|
+
};
|
|
731
|
+
socket.on(eventName, onAck);
|
|
732
|
+
});
|
|
733
|
+
};
|
|
734
|
+
var openStreamWatcher = async (input) => {
|
|
735
|
+
const defaultTimeoutMs = input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
736
|
+
const chunks = [];
|
|
737
|
+
const waiters = [];
|
|
738
|
+
const socket = createSocketIoClient(input.baseUrl, {
|
|
739
|
+
transports: ["websocket"],
|
|
740
|
+
forceNew: true,
|
|
741
|
+
reconnection: false,
|
|
742
|
+
auth: input.token ? { token: input.token } : void 0
|
|
743
|
+
});
|
|
744
|
+
const handleChunk = (raw) => {
|
|
745
|
+
if (!isStreamChunkFrame(raw)) return;
|
|
746
|
+
if (raw.status !== "stream") return;
|
|
747
|
+
if (raw.fullName !== input.routeFullName) return;
|
|
748
|
+
const chunk = raw;
|
|
749
|
+
chunks.push(chunk);
|
|
750
|
+
for (let i = waiters.length - 1; i >= 0; i--) {
|
|
751
|
+
const waiter = waiters[i];
|
|
752
|
+
if (!waiter) continue;
|
|
753
|
+
if (waiter.predicate(chunks)) {
|
|
754
|
+
waiters.splice(i, 1);
|
|
755
|
+
waiter.resolve();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
socket.on(socketEventNames.sync, handleChunk);
|
|
760
|
+
await waitForConnect(socket);
|
|
761
|
+
const responseIndex = nextResponseIndex();
|
|
762
|
+
socket.emit(socketEventNames.joinRoom, {
|
|
763
|
+
group: input.roomCode,
|
|
764
|
+
responseIndex
|
|
765
|
+
});
|
|
766
|
+
const [joinError] = await tryCatch6(() => waitForJoinAck(socket, responseIndex));
|
|
767
|
+
if (joinError) {
|
|
768
|
+
socket.off(socketEventNames.sync, handleChunk);
|
|
769
|
+
socket.disconnect();
|
|
770
|
+
throw joinError;
|
|
771
|
+
}
|
|
772
|
+
let closed = false;
|
|
773
|
+
const close = () => {
|
|
774
|
+
if (closed) return Promise.resolve();
|
|
775
|
+
closed = true;
|
|
776
|
+
socket.off(socketEventNames.sync, handleChunk);
|
|
777
|
+
for (const waiter of waiters) waiter.resolve();
|
|
778
|
+
waiters.length = 0;
|
|
779
|
+
socket.disconnect();
|
|
780
|
+
return Promise.resolve();
|
|
781
|
+
};
|
|
782
|
+
const stopAt = (predicate, timeoutMs = defaultTimeoutMs) => {
|
|
783
|
+
if (predicate(chunks)) return Promise.resolve();
|
|
784
|
+
return new Promise((resolve, reject) => {
|
|
785
|
+
let settled = false;
|
|
786
|
+
const waiter = {
|
|
787
|
+
predicate,
|
|
788
|
+
resolve: () => {
|
|
789
|
+
if (settled) return;
|
|
790
|
+
settled = true;
|
|
791
|
+
clearTimeout(timer);
|
|
792
|
+
resolve();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const timer = setTimeout(() => {
|
|
796
|
+
if (settled) return;
|
|
797
|
+
settled = true;
|
|
798
|
+
const idx = waiters.indexOf(waiter);
|
|
799
|
+
if (idx !== -1) waiters.splice(idx, 1);
|
|
800
|
+
reject(new Error(
|
|
801
|
+
`watchStream.stopAt: predicate not satisfied within ${timeoutMs}ms (received ${chunks.length} chunks)`
|
|
802
|
+
));
|
|
803
|
+
}, timeoutMs);
|
|
804
|
+
waiters.push(waiter);
|
|
805
|
+
});
|
|
806
|
+
};
|
|
807
|
+
const waitForCount = (n, timeoutMs = defaultTimeoutMs) => {
|
|
808
|
+
return stopAt((current) => current.length >= n, timeoutMs);
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
chunks,
|
|
812
|
+
stopAt,
|
|
813
|
+
waitForCount,
|
|
814
|
+
close
|
|
815
|
+
};
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/customTests.ts
|
|
819
|
+
var API_TEST_FILE_PATTERN = /_v(\d+)\.tests\.ts$/;
|
|
820
|
+
var SYNC_SERVER_TEST_FILE_PATTERN = /_server_v(\d+)\.tests\.ts$/;
|
|
821
|
+
var discoverCustomTestFiles = (srcDir = getSrcDir()) => {
|
|
822
|
+
if (!fs.existsSync(srcDir)) return [];
|
|
823
|
+
const found = [];
|
|
824
|
+
const stack = [srcDir];
|
|
825
|
+
while (stack.length > 0) {
|
|
826
|
+
const current = stack.pop();
|
|
827
|
+
if (current === void 0) break;
|
|
828
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
829
|
+
for (const entry of entries) {
|
|
830
|
+
const fullPath = path.join(current, entry.name);
|
|
831
|
+
if (entry.isDirectory()) {
|
|
832
|
+
stack.push(fullPath);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (!entry.isFile() || !entry.name.endsWith(".tests.ts")) continue;
|
|
836
|
+
const parentDir = path.basename(current);
|
|
837
|
+
if (parentDir !== "_api" && parentDir !== "_sync") continue;
|
|
838
|
+
const pattern = parentDir === "_api" ? API_TEST_FILE_PATTERN : SYNC_SERVER_TEST_FILE_PATTERN;
|
|
839
|
+
const versionMatch = pattern.exec(entry.name);
|
|
840
|
+
if (!versionMatch) continue;
|
|
841
|
+
const version = `v${versionMatch[1]}`;
|
|
842
|
+
const namePart = entry.name.replace(parentDir === "_api" ? /_v\d+\.tests\.ts$/ : /_server_v\d+\.tests\.ts$/, "");
|
|
843
|
+
const pageDir = path.dirname(current);
|
|
844
|
+
const relativePage = path.relative(srcDir, pageDir).replaceAll("\\", "/") || "system";
|
|
845
|
+
found.push({
|
|
846
|
+
filePath: fullPath,
|
|
847
|
+
kind: parentDir === "_api" ? "api" : "sync",
|
|
848
|
+
route: { page: relativePage, name: namePart, version }
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return found.toSorted((a, b) => a.filePath.localeCompare(b.filePath));
|
|
853
|
+
};
|
|
854
|
+
var AssertionError = class extends Error {
|
|
855
|
+
constructor(message) {
|
|
856
|
+
super(message);
|
|
857
|
+
this.name = "AssertionError";
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var buildExpect = () => ({
|
|
861
|
+
eq: (actual, expected, message) => {
|
|
862
|
+
const ok = Object.is(actual, expected) || typeof actual === "object" && typeof expected === "object" && JSON.stringify(actual) === JSON.stringify(expected);
|
|
863
|
+
if (!ok) {
|
|
864
|
+
const prefix = message ? `${message}: ` : "";
|
|
865
|
+
throw new AssertionError(`${prefix}expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
ok: (value, message) => {
|
|
869
|
+
if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);
|
|
870
|
+
},
|
|
871
|
+
throws: async (fn, message) => {
|
|
872
|
+
const [err] = await tryCatch7(fn);
|
|
873
|
+
if (!err) throw new AssertionError(message ?? "expected fn to throw, but it did not");
|
|
874
|
+
return err;
|
|
875
|
+
},
|
|
876
|
+
matches: (value, pattern, message) => {
|
|
877
|
+
if (!pattern.test(value)) {
|
|
878
|
+
const prefix = message ? `${message}: ` : "";
|
|
879
|
+
throw new AssertionError(`${prefix}"${value}" does not match ${pattern}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
var extractErrorCode = (response) => {
|
|
884
|
+
if (!response || typeof response !== "object") return void 0;
|
|
885
|
+
const r = response;
|
|
886
|
+
if (typeof r.errorCode === "string" && r.errorCode.length > 0) return r.errorCode;
|
|
887
|
+
if (typeof r.status === "string" && r.status !== "success") return r.status;
|
|
888
|
+
return void 0;
|
|
889
|
+
};
|
|
890
|
+
var buildSessionHelpers = (state) => {
|
|
891
|
+
return {
|
|
892
|
+
login: async (user) => {
|
|
893
|
+
const { saveSession, getSession } = await import("@luckystack/login");
|
|
894
|
+
const userId = user?.id ?? `test-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
895
|
+
const email = user?.email ?? `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
896
|
+
const name = user?.name ?? "Test User";
|
|
897
|
+
const token = `test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
898
|
+
const sessionData = {
|
|
899
|
+
id: userId,
|
|
900
|
+
email,
|
|
901
|
+
name,
|
|
902
|
+
token,
|
|
903
|
+
provider: "credentials"
|
|
904
|
+
};
|
|
905
|
+
await saveSession(token, sessionData, true);
|
|
906
|
+
state.token = token;
|
|
907
|
+
state.userId = userId;
|
|
908
|
+
const persisted = await getSession(token);
|
|
909
|
+
state.csrfToken = persisted?.csrfToken ?? null;
|
|
910
|
+
return { token, userId };
|
|
911
|
+
},
|
|
912
|
+
logout: async () => {
|
|
913
|
+
if (!state.token) return;
|
|
914
|
+
const { deleteSession } = await import("@luckystack/login");
|
|
915
|
+
await deleteSession(state.token);
|
|
916
|
+
state.token = null;
|
|
917
|
+
state.userId = null;
|
|
918
|
+
state.csrfToken = null;
|
|
919
|
+
},
|
|
920
|
+
current: () => ({ token: state.token, userId: state.userId })
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
924
|
+
return async (input) => {
|
|
925
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/${routePath}`;
|
|
926
|
+
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
927
|
+
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
928
|
+
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
929
|
+
const init = { method, headers };
|
|
930
|
+
if (method !== "GET" && method !== "HEAD") init.body = JSON.stringify(input ?? {});
|
|
931
|
+
const response = await fetch(url, init);
|
|
932
|
+
const json = await response.json();
|
|
933
|
+
state.lastResponse = json;
|
|
934
|
+
return json;
|
|
935
|
+
};
|
|
936
|
+
};
|
|
937
|
+
var buildCallSync = (baseUrl, routePath, state, cookieName) => {
|
|
938
|
+
return async (input, opts) => {
|
|
939
|
+
const url = `${baseUrl.replace(/\/$/, "")}/sync/${routePath}`;
|
|
940
|
+
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
941
|
+
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
942
|
+
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
943
|
+
const body = { data: input ?? {}, receiver: opts?.receiver };
|
|
944
|
+
const response = await fetch(url, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers,
|
|
947
|
+
body: JSON.stringify(body)
|
|
948
|
+
});
|
|
949
|
+
const json = await response.json();
|
|
950
|
+
state.lastResponse = json;
|
|
951
|
+
return json;
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
var buildContext = (discovery, input) => {
|
|
955
|
+
const cookieName = input.sessionCookieName ?? getProjectConfig().http.sessionCookieName;
|
|
956
|
+
const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
|
|
957
|
+
const routeFullName = `sync/${routePath}`;
|
|
958
|
+
const apiMethod = input.apiMethodMap?.[discovery.route.page]?.[discovery.route.name]?.[discovery.route.version] ?? "POST";
|
|
959
|
+
const state = { token: null, userId: null, csrfToken: null, lastResponse: null };
|
|
960
|
+
const prisma = getPrismaClient();
|
|
961
|
+
const watchers = [];
|
|
962
|
+
const watchStream = async (roomCode) => {
|
|
963
|
+
const watcher = await openStreamWatcher({
|
|
964
|
+
baseUrl: input.baseUrl,
|
|
965
|
+
roomCode,
|
|
966
|
+
token: state.token,
|
|
967
|
+
routeFullName
|
|
968
|
+
});
|
|
969
|
+
watchers.push(watcher);
|
|
970
|
+
return watcher;
|
|
971
|
+
};
|
|
972
|
+
const closeAllWatchers = async () => {
|
|
973
|
+
while (watchers.length > 0) {
|
|
974
|
+
const watcher = watchers.pop();
|
|
975
|
+
if (!watcher) continue;
|
|
976
|
+
const [closeError] = await tryCatch7(() => watcher.close());
|
|
977
|
+
if (closeError) {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
const ctx = {
|
|
982
|
+
callApi: buildCallApi(input.baseUrl, routePath, state, cookieName, apiMethod),
|
|
983
|
+
callSync: buildCallSync(input.baseUrl, routePath, state, cookieName),
|
|
984
|
+
watchStream,
|
|
985
|
+
session: buildSessionHelpers(state),
|
|
986
|
+
prisma,
|
|
987
|
+
expect: buildExpect()
|
|
988
|
+
};
|
|
989
|
+
return { ctx, closeAllWatchers, state };
|
|
990
|
+
};
|
|
991
|
+
var runCustomTests = async (input) => {
|
|
992
|
+
const files = discoverCustomTestFiles();
|
|
993
|
+
const results = [];
|
|
994
|
+
for (const discovery of files) {
|
|
995
|
+
const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
|
|
996
|
+
if (input.filter && !routePath.includes(input.filter)) continue;
|
|
997
|
+
const [importError, mod] = await tryCatch7(
|
|
998
|
+
() => import(pathToFileURL(discovery.filePath).href)
|
|
999
|
+
);
|
|
1000
|
+
if (importError || !mod) {
|
|
1001
|
+
const r = {
|
|
1002
|
+
routePath,
|
|
1003
|
+
caseName: "(import)",
|
|
1004
|
+
status: "fail",
|
|
1005
|
+
durationMs: 0,
|
|
1006
|
+
reason: `failed to import ${discovery.filePath}: ${importError?.message ?? "unknown"}`
|
|
1007
|
+
};
|
|
1008
|
+
results.push(r);
|
|
1009
|
+
input.onResult?.(r);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const exported = mod.customTests;
|
|
1013
|
+
if (!Array.isArray(exported) || exported.length === 0) {
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
for (const rawCase of exported) {
|
|
1017
|
+
const c = rawCase;
|
|
1018
|
+
if (typeof c.name !== "string" || typeof c.run !== "function") continue;
|
|
1019
|
+
const expectedToFail = typeof c.expectedToFail === "string" ? c.expectedToFail : void 0;
|
|
1020
|
+
const built = buildContext(discovery, input);
|
|
1021
|
+
const started = Date.now();
|
|
1022
|
+
const [runError] = await tryCatch7(async () => c.run(built.ctx));
|
|
1023
|
+
await tryCatch7(() => built.closeAllWatchers());
|
|
1024
|
+
const durationMs = Date.now() - started;
|
|
1025
|
+
let r;
|
|
1026
|
+
if (runError) {
|
|
1027
|
+
const errorCode = extractErrorCode(built.state.lastResponse);
|
|
1028
|
+
const reason = errorCode ? `${runError.message} (server: ${errorCode})` : runError.message;
|
|
1029
|
+
r = expectedToFail ? { routePath, caseName: c.name, status: "xfail", durationMs, reason, expectedToFail } : { routePath, caseName: c.name, status: "fail", durationMs, reason };
|
|
1030
|
+
} else {
|
|
1031
|
+
r = expectedToFail ? {
|
|
1032
|
+
routePath,
|
|
1033
|
+
caseName: c.name,
|
|
1034
|
+
status: "xpass",
|
|
1035
|
+
durationMs,
|
|
1036
|
+
reason: `marked expectedToFail ("${expectedToFail}") but passed \u2014 remove the marker`,
|
|
1037
|
+
expectedToFail
|
|
1038
|
+
} : { routePath, caseName: c.name, status: "pass", durationMs };
|
|
1039
|
+
}
|
|
1040
|
+
results.push(r);
|
|
1041
|
+
input.onResult?.(r);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
total: results.length,
|
|
1046
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
1047
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
1048
|
+
xfailed: results.filter((r) => r.status === "xfail").length,
|
|
1049
|
+
xpassed: results.filter((r) => r.status === "xpass").length,
|
|
1050
|
+
results
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// src/runAllTests.ts
|
|
1055
|
+
import { clearAllRateLimits, getProjectConfig as getProjectConfig2 } from "@luckystack/core";
|
|
1056
|
+
var inputForEndpoint = (apiInputSchemas) => (endpoint) => {
|
|
1057
|
+
const schema = apiInputSchemas[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
1058
|
+
return schema ? sampleSchemaInput(schema) : {};
|
|
1059
|
+
};
|
|
1060
|
+
var matchesFilter = (endpoint, filter) => {
|
|
1061
|
+
if (!filter) return true;
|
|
1062
|
+
return `${endpoint.page}/${endpoint.name}/${endpoint.version}`.includes(filter);
|
|
1063
|
+
};
|
|
1064
|
+
var filterResults = (results, filter) => filter ? results.filter((r) => matchesFilter(r.endpoint, filter)) : results;
|
|
1065
|
+
var cloneSummary = (summary, filter) => {
|
|
1066
|
+
if (!filter) return summary;
|
|
1067
|
+
const results = filterResults(summary.results, filter);
|
|
1068
|
+
return {
|
|
1069
|
+
total: results.length,
|
|
1070
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
1071
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
1072
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
1073
|
+
results
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
var runAllTests = async (input) => {
|
|
1077
|
+
const summary = { totalPassed: 0, totalFailed: 0 };
|
|
1078
|
+
const headers = {};
|
|
1079
|
+
if (input.authToken) {
|
|
1080
|
+
const cookieName = input.sessionCookieName ?? getProjectConfig2().http.sessionCookieName;
|
|
1081
|
+
headers.Cookie = `${cookieName}=${input.authToken}`;
|
|
1082
|
+
}
|
|
1083
|
+
const inputFor = inputForEndpoint(input.apiInputSchemas);
|
|
1084
|
+
if (!input.noSweep) {
|
|
1085
|
+
const contract = await runContractTests({
|
|
1086
|
+
apiMethodMap: input.apiMethodMap,
|
|
1087
|
+
baseUrl: input.baseUrl,
|
|
1088
|
+
skip: input.skip,
|
|
1089
|
+
headers,
|
|
1090
|
+
inputFor
|
|
1091
|
+
});
|
|
1092
|
+
summary.contract = cloneSummary(contract, input.filter);
|
|
1093
|
+
const auth = await runAuthEnforcementTests({
|
|
1094
|
+
apiMethodMap: input.apiMethodMap,
|
|
1095
|
+
apiMetaMap: input.apiMetaMap,
|
|
1096
|
+
baseUrl: input.baseUrl,
|
|
1097
|
+
skip: input.skip,
|
|
1098
|
+
inputFor
|
|
1099
|
+
});
|
|
1100
|
+
summary.auth = cloneSummary(auth, input.filter);
|
|
1101
|
+
if (!input.noRateLimit) {
|
|
1102
|
+
const rateLimit = await runRateLimitTests({
|
|
1103
|
+
apiMethodMap: input.apiMethodMap,
|
|
1104
|
+
apiMetaMap: input.apiMetaMap,
|
|
1105
|
+
baseUrl: input.baseUrl,
|
|
1106
|
+
skip: input.skip,
|
|
1107
|
+
headers,
|
|
1108
|
+
inputFor
|
|
1109
|
+
});
|
|
1110
|
+
summary.rateLimit = cloneSummary(rateLimit, input.filter);
|
|
1111
|
+
}
|
|
1112
|
+
if (!input.noFuzz) {
|
|
1113
|
+
const fuzz = await runFuzzTests({
|
|
1114
|
+
apiMethodMap: input.apiMethodMap,
|
|
1115
|
+
baseUrl: input.baseUrl,
|
|
1116
|
+
skip: input.skip,
|
|
1117
|
+
headers
|
|
1118
|
+
});
|
|
1119
|
+
summary.fuzz = cloneSummary(fuzz, input.filter);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (!input.noCustom) {
|
|
1123
|
+
try {
|
|
1124
|
+
await clearAllRateLimits();
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
const custom = await runCustomTests({
|
|
1128
|
+
baseUrl: input.baseUrl,
|
|
1129
|
+
sessionCookieName: input.sessionCookieName,
|
|
1130
|
+
filter: input.filter,
|
|
1131
|
+
//? Pass the generated method map so the harness invokes each API route
|
|
1132
|
+
//? with its declared HTTP method (e.g. logout is DELETE, not POST).
|
|
1133
|
+
apiMethodMap: input.apiMethodMap
|
|
1134
|
+
});
|
|
1135
|
+
summary.custom = custom;
|
|
1136
|
+
}
|
|
1137
|
+
summary.totalPassed = (summary.contract?.passed ?? 0) + (summary.auth?.passed ?? 0) + (summary.rateLimit?.passed ?? 0) + (summary.fuzz?.passed ?? 0) + (summary.custom?.passed ?? 0);
|
|
1138
|
+
summary.totalFailed = (summary.contract?.failed ?? 0) + (summary.auth?.failed ?? 0) + (summary.rateLimit?.failed ?? 0) + (summary.fuzz?.failed ?? 0) + (summary.custom?.failed ?? 0);
|
|
1139
|
+
return summary;
|
|
1140
|
+
};
|
|
1141
|
+
var useColor = (() => {
|
|
1142
|
+
if (process.env.NO_COLOR) return false;
|
|
1143
|
+
if (process.env.FORCE_COLOR) return true;
|
|
1144
|
+
return process.stdout.isTTY;
|
|
1145
|
+
})();
|
|
1146
|
+
var paint = (code, text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
1147
|
+
var green = (t) => paint("32", t);
|
|
1148
|
+
var red = (t) => paint("31", t);
|
|
1149
|
+
var yellow = (t) => paint("33", t);
|
|
1150
|
+
var dim = (t) => paint("2", t);
|
|
1151
|
+
var bold = (t) => paint("1", t);
|
|
1152
|
+
var xfailOf = (s) => s && "xfailed" in s ? s.xfailed : 0;
|
|
1153
|
+
var skippedOf = (s) => s && "skipped" in s ? s.skipped : 0;
|
|
1154
|
+
var formatLayerLine = (label, s) => {
|
|
1155
|
+
if (!s) return ` ${dim("\u2013")} ${label.padEnd(18)} ${dim("(layer skipped)")}`;
|
|
1156
|
+
const clean = s.failed === 0;
|
|
1157
|
+
const mark = clean ? green("\u2713") : red("\u2717");
|
|
1158
|
+
const head = clean ? green(`${s.passed}/${s.total} passed`) : `${green(`${s.passed}/${s.total} passed`)} ${red(`${s.failed}/${s.total} failed`)}`;
|
|
1159
|
+
const extras = [];
|
|
1160
|
+
const xf = xfailOf(s);
|
|
1161
|
+
const sk = skippedOf(s);
|
|
1162
|
+
if (xf) extras.push(yellow(`${xf} expected-fail`));
|
|
1163
|
+
if (sk) extras.push(dim(`${sk} skipped`));
|
|
1164
|
+
const extra = extras.length > 0 ? ` ${dim("\xB7")} ${extras.join(dim(", "))}` : "";
|
|
1165
|
+
return ` ${mark} ${label.padEnd(18)} ${head}${extra}`;
|
|
1166
|
+
};
|
|
1167
|
+
var sweepFailRows = (s) => (s?.results ?? []).filter((r) => r.status === "fail").map((r) => ({
|
|
1168
|
+
label: `${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,
|
|
1169
|
+
reason: r.errorCode ? `${r.reason ?? "failed"} [${r.errorCode}]` : r.reason
|
|
1170
|
+
}));
|
|
1171
|
+
var sweepSkipRows = (layer, s) => (s?.results ?? []).filter((r) => r.status === "skipped").map((r) => ({
|
|
1172
|
+
label: `${layer}: ${r.endpoint.page}/${r.endpoint.name}/${r.endpoint.version}`,
|
|
1173
|
+
reason: r.reason
|
|
1174
|
+
}));
|
|
1175
|
+
var customRows = (s, status) => (s?.results ?? []).filter((r) => r.status === status).map((r) => ({ label: `${r.routePath} :: ${r.caseName}`, reason: r.reason }));
|
|
1176
|
+
var printSection = (title, heading, rows, bullet) => {
|
|
1177
|
+
if (rows.length === 0) return;
|
|
1178
|
+
console.log("");
|
|
1179
|
+
console.log(title(bold(`${heading} (${rows.length})`)));
|
|
1180
|
+
for (const row of rows) {
|
|
1181
|
+
console.log(` ${title(bullet)} ${row.label}`);
|
|
1182
|
+
if (row.reason) console.log(` ${dim(row.reason)}`);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
var logRunAllSummary = (summary) => {
|
|
1186
|
+
console.log("");
|
|
1187
|
+
console.log(formatLayerLine("contract", summary.contract));
|
|
1188
|
+
console.log(formatLayerLine("auth-enforcement", summary.auth));
|
|
1189
|
+
console.log(formatLayerLine("rate-limit", summary.rateLimit));
|
|
1190
|
+
console.log(formatLayerLine("fuzz", summary.fuzz));
|
|
1191
|
+
console.log(formatLayerLine("custom", summary.custom));
|
|
1192
|
+
const failed = [
|
|
1193
|
+
...sweepFailRows(summary.contract),
|
|
1194
|
+
...sweepFailRows(summary.auth),
|
|
1195
|
+
...sweepFailRows(summary.rateLimit),
|
|
1196
|
+
...sweepFailRows(summary.fuzz),
|
|
1197
|
+
...customRows(summary.custom, "fail")
|
|
1198
|
+
];
|
|
1199
|
+
printSection(red, "Failed \u2014 must be fixed (real bugs / wrong tests)", failed, "\u2717");
|
|
1200
|
+
printSection(yellow, "Expected failures \u2014 known issues, allowed to fail", customRows(summary.custom, "xfail"), "\u26A0");
|
|
1201
|
+
printSection(yellow, "Unexpectedly passed \u2014 remove the expectedToFail marker", customRows(summary.custom, "xpass"), "?");
|
|
1202
|
+
const skipped = [
|
|
1203
|
+
...sweepSkipRows("contract", summary.contract),
|
|
1204
|
+
...sweepSkipRows("auth", summary.auth),
|
|
1205
|
+
...sweepSkipRows("rate-limit", summary.rateLimit),
|
|
1206
|
+
...sweepSkipRows("fuzz", summary.fuzz)
|
|
1207
|
+
];
|
|
1208
|
+
printSection(dim, "Skipped \u2014 not run (with reason)", skipped, "\u2013");
|
|
1209
|
+
const totalXfail = xfailOf(summary.contract) + xfailOf(summary.auth) + xfailOf(summary.rateLimit) + xfailOf(summary.fuzz) + xfailOf(summary.custom);
|
|
1210
|
+
const totalSkipped = skippedOf(summary.contract) + skippedOf(summary.auth) + skippedOf(summary.rateLimit) + skippedOf(summary.fuzz) + skippedOf(summary.custom);
|
|
1211
|
+
console.log("");
|
|
1212
|
+
const parts = [
|
|
1213
|
+
green(`${summary.totalPassed} passed`),
|
|
1214
|
+
summary.totalFailed > 0 ? red(`${summary.totalFailed} failed`) : dim("0 failed"),
|
|
1215
|
+
totalXfail > 0 ? yellow(`${totalXfail} expected-fail`) : dim("0 expected-fail"),
|
|
1216
|
+
totalSkipped > 0 ? yellow(`${totalSkipped} skipped`) : dim("0 skipped")
|
|
1217
|
+
];
|
|
1218
|
+
console.log(`Summary: ${parts.join(dim(" \xB7 "))}`);
|
|
1219
|
+
console.log(dim(" legend: \u2717 red = must fix \xB7 \u26A0 yellow = known/allowed \xB7 \u2013 skipped (not run)"));
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/extensionRegistry.ts
|
|
1223
|
+
var layers = /* @__PURE__ */ new Map();
|
|
1224
|
+
var fixtures = /* @__PURE__ */ new Map();
|
|
1225
|
+
var reporter = null;
|
|
1226
|
+
var registerTestLayer = (layer) => {
|
|
1227
|
+
layers.set(layer.name, layer);
|
|
1228
|
+
};
|
|
1229
|
+
var listTestLayers = () => [...layers.values()];
|
|
1230
|
+
var registerTestFixture = (typeKey, fixture) => {
|
|
1231
|
+
fixtures.set(typeKey, fixture);
|
|
1232
|
+
};
|
|
1233
|
+
var getTestFixture = (typeKey) => fixtures.get(typeKey);
|
|
1234
|
+
var registerTestReporter = (newReporter) => {
|
|
1235
|
+
reporter = newReporter;
|
|
1236
|
+
};
|
|
1237
|
+
var getTestReporter = () => reporter;
|
|
1238
|
+
var resetTestExtensionsForTests = () => {
|
|
1239
|
+
layers.clear();
|
|
1240
|
+
fixtures.clear();
|
|
1241
|
+
reporter = null;
|
|
1242
|
+
};
|
|
1243
|
+
export {
|
|
1244
|
+
discoverCustomTestFiles,
|
|
1245
|
+
getTestFixture,
|
|
1246
|
+
getTestReporter,
|
|
1247
|
+
listTestLayers,
|
|
1248
|
+
logContractResult,
|
|
1249
|
+
logContractSummary,
|
|
1250
|
+
logRunAllSummary,
|
|
1251
|
+
openStreamWatcher,
|
|
1252
|
+
registerTestFixture,
|
|
1253
|
+
registerTestLayer,
|
|
1254
|
+
registerTestReporter,
|
|
1255
|
+
resetServerState,
|
|
1256
|
+
resetTestExtensionsForTests,
|
|
1257
|
+
runAllTests,
|
|
1258
|
+
runAuthEnforcementCheck,
|
|
1259
|
+
runAuthEnforcementTests,
|
|
1260
|
+
runContractCheck,
|
|
1261
|
+
runContractTests,
|
|
1262
|
+
runCustomTests,
|
|
1263
|
+
runFuzzCheck,
|
|
1264
|
+
runFuzzTests,
|
|
1265
|
+
runRateLimitCheck,
|
|
1266
|
+
runRateLimitTests,
|
|
1267
|
+
sampleSchemaInput,
|
|
1268
|
+
walkEndpoints
|
|
1269
|
+
};
|
|
1270
|
+
//# sourceMappingURL=index.js.map
|