@luckystack/test-runner 0.10.0 → 0.10.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.
- package/CHANGELOG.md +27 -1
- package/CLAUDE.md +20 -7
- package/README.md +5 -3
- package/dist/index.d.ts +219 -23
- package/dist/index.js +569 -303
- package/dist/index.js.map +1 -1
- package/docs/auth-tests.md +2 -2
- package/docs/contract-tests.md +21 -6
- package/docs/extension-hooks.md +12 -5
- package/docs/fuzz-tests.md +3 -3
- package/docs/rate-limit-tests.md +13 -5
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -32,20 +32,21 @@ var walkEndpoints = (apiMethodMap) => {
|
|
|
32
32
|
}
|
|
33
33
|
return endpoints;
|
|
34
34
|
};
|
|
35
|
-
var walkSyncEndpoints = (
|
|
35
|
+
var walkSyncEndpoints = (syncMap) => {
|
|
36
36
|
const endpoints = [];
|
|
37
|
-
for (const [page, nameMap] of Object.entries(
|
|
37
|
+
for (const [page, nameMap] of Object.entries(syncMap)) {
|
|
38
38
|
if (!nameMap) continue;
|
|
39
39
|
for (const [name, versionMap] of Object.entries(nameMap)) {
|
|
40
40
|
if (!versionMap) continue;
|
|
41
|
-
for (const [version,
|
|
42
|
-
if (
|
|
41
|
+
for (const [version, leaf] of Object.entries(versionMap)) {
|
|
42
|
+
if (leaf === void 0) continue;
|
|
43
43
|
endpoints.push({
|
|
44
44
|
page,
|
|
45
45
|
name,
|
|
46
46
|
version,
|
|
47
47
|
method: "POST",
|
|
48
|
-
fullPath: `sync/${page}/${name}/${version}
|
|
48
|
+
fullPath: `sync/${page}/${name}/${version}`,
|
|
49
|
+
kind: "sync"
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -54,13 +55,15 @@ var walkSyncEndpoints = (syncMethodMap) => {
|
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
// src/probeRequest.ts
|
|
57
|
-
import { tryCatch } from "@luckystack/core";
|
|
58
|
+
import { buildRoutedGetUrl, tryCatch } from "@luckystack/core";
|
|
58
59
|
var sendProbe = async (input) => {
|
|
59
60
|
const controller = new AbortController();
|
|
60
61
|
const timeoutHandle = setTimeout(() => {
|
|
61
62
|
controller.abort();
|
|
62
63
|
}, input.requestTimeoutMs);
|
|
63
|
-
const
|
|
64
|
+
const isGet = input.method === "GET";
|
|
65
|
+
const url = isGet && input.body !== void 0 ? buildRoutedGetUrl(input.url, input.body) : input.url;
|
|
66
|
+
const [fetchError, response] = await tryCatch(() => fetch(url, {
|
|
64
67
|
method: input.method,
|
|
65
68
|
headers: {
|
|
66
69
|
"Content-Type": "application/json",
|
|
@@ -69,7 +72,7 @@ var sendProbe = async (input) => {
|
|
|
69
72
|
"Origin": new URL(input.baseUrl).origin,
|
|
70
73
|
...input.headers
|
|
71
74
|
},
|
|
72
|
-
body:
|
|
75
|
+
body: isGet ? void 0 : JSON.stringify(input.body),
|
|
73
76
|
signal: controller.signal
|
|
74
77
|
}));
|
|
75
78
|
clearTimeout(timeoutHandle);
|
|
@@ -79,17 +82,27 @@ var sendProbe = async (input) => {
|
|
|
79
82
|
);
|
|
80
83
|
return { httpStatus: response.status, parsed: parsed ?? null };
|
|
81
84
|
};
|
|
85
|
+
var createHttpProbeTransport = (options = {}) => ({
|
|
86
|
+
name: "http",
|
|
87
|
+
send: (request) => sendProbe({
|
|
88
|
+
url: `${request.baseUrl.replace(/\/$/, "")}/${request.endpoint.fullPath}`,
|
|
89
|
+
method: request.endpoint.method,
|
|
90
|
+
baseUrl: request.baseUrl,
|
|
91
|
+
body: request.endpoint.kind === "sync" ? { data: request.body, receiver: options.syncReceiver } : request.body,
|
|
92
|
+
headers: request.headers,
|
|
93
|
+
requestTimeoutMs: request.requestTimeoutMs
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
var defaultProbeTransport = createHttpProbeTransport();
|
|
82
97
|
|
|
83
98
|
// src/contractCheck.ts
|
|
84
99
|
var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
|
|
85
100
|
var runContractCheck = async (input) => {
|
|
86
101
|
const { endpoint, baseUrl } = input;
|
|
87
|
-
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
88
102
|
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
89
103
|
const started = Date.now();
|
|
90
|
-
const result = await
|
|
91
|
-
|
|
92
|
-
method: endpoint.method,
|
|
104
|
+
const result = await (input.transport ?? defaultProbeTransport).send({
|
|
105
|
+
endpoint,
|
|
93
106
|
baseUrl,
|
|
94
107
|
body,
|
|
95
108
|
headers: input.headers,
|
|
@@ -167,23 +180,31 @@ var LAYER_KEYS = {
|
|
|
167
180
|
custom: "custom"
|
|
168
181
|
};
|
|
169
182
|
var STATE_CHANGING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE"]);
|
|
183
|
+
var routeKey = (endpoint) => `${endpoint.page}/${endpoint.name}/${endpoint.version}`;
|
|
184
|
+
var matchesFilter = (endpoint, filter) => !filter || routeKey(endpoint).includes(filter);
|
|
185
|
+
var selectEndpoints = (input) => [
|
|
186
|
+
...walkEndpoints(input.apiMethodMap),
|
|
187
|
+
...input.syncMetaMap ? walkSyncEndpoints(input.syncMetaMap) : []
|
|
188
|
+
].filter((endpoint) => matchesFilter(endpoint, input.filter));
|
|
189
|
+
var metaMapFor = (endpoint, apiMetaMap, syncMetaMap) => endpoint.kind === "sync" ? syncMetaMap ?? {} : apiMetaMap;
|
|
190
|
+
var transportFor = (endpoint, syncTransport) => endpoint.kind === "sync" && syncTransport ? syncTransport : defaultProbeTransport;
|
|
170
191
|
var shouldSkip = (endpoint, skip) => {
|
|
171
192
|
if (skip.length === 0) return false;
|
|
172
|
-
const versioned =
|
|
193
|
+
const versioned = routeKey(endpoint);
|
|
173
194
|
const versionless = `${endpoint.page}/${endpoint.name}`;
|
|
174
195
|
return skip.includes(versioned) || skip.includes(versionless);
|
|
175
196
|
};
|
|
176
|
-
var requiresLogin = (
|
|
177
|
-
const meta =
|
|
197
|
+
var requiresLogin = (metaMap, endpoint) => {
|
|
198
|
+
const meta = metaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
178
199
|
return meta?.auth?.login ?? false;
|
|
179
200
|
};
|
|
180
|
-
var hasAuthRequirement = (
|
|
181
|
-
const meta =
|
|
201
|
+
var hasAuthRequirement = (metaMap, endpoint) => {
|
|
202
|
+
const meta = metaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
182
203
|
return (meta?.auth?.login ?? false) || (meta?.auth?.hasAdditional ?? false);
|
|
183
204
|
};
|
|
184
|
-
var hasMetaEntry = (
|
|
185
|
-
var getRateLimit = (
|
|
186
|
-
const meta =
|
|
205
|
+
var hasMetaEntry = (metaMap, endpoint) => metaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version] !== void 0;
|
|
206
|
+
var getRateLimit = (metaMap, endpoint) => {
|
|
207
|
+
const meta = metaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
187
208
|
if (!meta || meta.rateLimit === false || meta.rateLimit === void 0) return null;
|
|
188
209
|
return meta.rateLimit;
|
|
189
210
|
};
|
|
@@ -197,7 +218,7 @@ var calculateSummary = (results) => ({
|
|
|
197
218
|
|
|
198
219
|
// src/runContractTests.ts
|
|
199
220
|
var runContractTests = async (input) => {
|
|
200
|
-
const endpoints =
|
|
221
|
+
const endpoints = selectEndpoints(input);
|
|
201
222
|
const skip = input.skip ?? [];
|
|
202
223
|
const results = [];
|
|
203
224
|
for (const endpoint of endpoints) {
|
|
@@ -216,7 +237,8 @@ var runContractTests = async (input) => {
|
|
|
216
237
|
endpoint,
|
|
217
238
|
baseUrl: input.baseUrl,
|
|
218
239
|
inputFor: input.inputFor,
|
|
219
|
-
headers: input.headers
|
|
240
|
+
headers: input.headers,
|
|
241
|
+
transport: transportFor(endpoint, input.syncTransport)
|
|
220
242
|
});
|
|
221
243
|
results.push(result);
|
|
222
244
|
input.onResult?.(result);
|
|
@@ -229,12 +251,10 @@ var DEFAULT_REQUEST_TIMEOUT_MS2 = 5e3;
|
|
|
229
251
|
var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set(["auth.required", "auth.forbidden"]);
|
|
230
252
|
var runAuthEnforcementCheck = async (input) => {
|
|
231
253
|
const { endpoint, baseUrl } = input;
|
|
232
|
-
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
233
254
|
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
234
255
|
const started = Date.now();
|
|
235
|
-
const result = await
|
|
236
|
-
|
|
237
|
-
method: endpoint.method,
|
|
256
|
+
const result = await (input.transport ?? defaultProbeTransport).send({
|
|
257
|
+
endpoint,
|
|
238
258
|
baseUrl,
|
|
239
259
|
body,
|
|
240
260
|
requestTimeoutMs: input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2
|
|
@@ -292,22 +312,23 @@ var runAuthEnforcementCheck = async (input) => {
|
|
|
292
312
|
|
|
293
313
|
// src/runAuthEnforcementTests.ts
|
|
294
314
|
var runAuthEnforcementTests = async (input) => {
|
|
295
|
-
const endpoints =
|
|
315
|
+
const endpoints = selectEndpoints(input);
|
|
296
316
|
const skip = input.skip ?? [];
|
|
297
317
|
const results = [];
|
|
298
318
|
for (const endpoint of endpoints) {
|
|
299
|
-
|
|
319
|
+
const metaMap = metaMapFor(endpoint, input.apiMetaMap, input.syncMetaMap);
|
|
320
|
+
if (!hasMetaEntry(metaMap, endpoint)) {
|
|
300
321
|
const skipped = {
|
|
301
322
|
endpoint,
|
|
302
323
|
status: "skipped",
|
|
303
324
|
durationMs: 0,
|
|
304
|
-
reason:
|
|
325
|
+
reason: `No ${endpoint.kind === "sync" ? "syncMetaMap" : "apiMetaMap"} entry \u2014 auth requirement unverifiable`
|
|
305
326
|
};
|
|
306
327
|
results.push(skipped);
|
|
307
328
|
input.onResult?.(skipped);
|
|
308
329
|
continue;
|
|
309
330
|
}
|
|
310
|
-
if (!hasAuthRequirement(
|
|
331
|
+
if (!hasAuthRequirement(metaMap, endpoint)) {
|
|
311
332
|
continue;
|
|
312
333
|
}
|
|
313
334
|
if (shouldSkip(endpoint, skip)) {
|
|
@@ -324,7 +345,8 @@ var runAuthEnforcementTests = async (input) => {
|
|
|
324
345
|
const result = await runAuthEnforcementCheck({
|
|
325
346
|
endpoint,
|
|
326
347
|
baseUrl: input.baseUrl,
|
|
327
|
-
inputFor: input.inputFor
|
|
348
|
+
inputFor: input.inputFor,
|
|
349
|
+
transport: transportFor(endpoint, input.syncTransport)
|
|
328
350
|
});
|
|
329
351
|
results.push(result);
|
|
330
352
|
input.onResult?.(result);
|
|
@@ -334,21 +356,24 @@ var runAuthEnforcementTests = async (input) => {
|
|
|
334
356
|
|
|
335
357
|
// src/rateLimitCheck.ts
|
|
336
358
|
var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
|
|
337
|
-
var
|
|
359
|
+
var expectedErrorCodeFor = (endpoint) => endpoint.kind === "sync" ? "sync.rateLimitExceeded" : "api.rateLimitExceeded";
|
|
338
360
|
var PRE_LIMITER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
339
361
|
"auth.required",
|
|
340
362
|
"auth.forbidden",
|
|
341
|
-
"auth.csrfMismatch"
|
|
363
|
+
"auth.csrfMismatch",
|
|
364
|
+
"sync.notRoomMember",
|
|
365
|
+
"sync.receiverNotAllowed",
|
|
366
|
+
"sync.missingReceiver"
|
|
342
367
|
]);
|
|
343
368
|
var runRateLimitCheck = async (input) => {
|
|
344
369
|
const { endpoint, baseUrl, rateLimit } = input;
|
|
345
|
-
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
346
370
|
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
347
371
|
const started = Date.now();
|
|
348
372
|
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS3;
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
373
|
+
const expectedErrorCode = expectedErrorCodeFor(endpoint);
|
|
374
|
+
const transport = input.transport ?? defaultProbeTransport;
|
|
375
|
+
const send = () => transport.send({
|
|
376
|
+
endpoint,
|
|
352
377
|
baseUrl,
|
|
353
378
|
body,
|
|
354
379
|
headers: input.headers,
|
|
@@ -378,7 +403,7 @@ var runRateLimitCheck = async (input) => {
|
|
|
378
403
|
};
|
|
379
404
|
}
|
|
380
405
|
const { httpStatus, parsed } = final;
|
|
381
|
-
if (parsed?.status === "error" && parsed.errorCode ===
|
|
406
|
+
if (parsed?.status === "error" && parsed.errorCode === expectedErrorCode) {
|
|
382
407
|
return {
|
|
383
408
|
endpoint,
|
|
384
409
|
status: "pass",
|
|
@@ -395,16 +420,17 @@ var runRateLimitCheck = async (input) => {
|
|
|
395
420
|
httpStatus,
|
|
396
421
|
responseStatus,
|
|
397
422
|
errorCode: parsed?.errorCode,
|
|
398
|
-
reason: `expected '${
|
|
423
|
+
reason: `expected '${expectedErrorCode}' on request ${rateLimit + 1} but got ${parsed?.status ?? "none"}/${parsed?.errorCode ?? "(none)"}`,
|
|
399
424
|
durationMs
|
|
400
425
|
};
|
|
401
426
|
};
|
|
402
427
|
|
|
403
428
|
// src/resetServerState.ts
|
|
404
|
-
import { getProjectConfig, tryCatch as tryCatch2 } from "@luckystack/core";
|
|
429
|
+
import { getProjectConfig, TEST_RESET_ONLY_PARAM, tryCatch as tryCatch2 } from "@luckystack/core";
|
|
405
430
|
var DEFAULT_REQUEST_TIMEOUT_MS4 = 1e4;
|
|
406
|
-
var resetServerState = async ({ baseUrl, token, requestTimeoutMs }) => {
|
|
407
|
-
const
|
|
431
|
+
var resetServerState = async ({ baseUrl, token, requestTimeoutMs, only }) => {
|
|
432
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}${getProjectConfig().http.testResetEndpoint}`;
|
|
433
|
+
const url = only && only.length > 0 ? `${endpoint}?${new URLSearchParams({ [TEST_RESET_ONLY_PARAM]: only.join(",") }).toString()}` : endpoint;
|
|
408
434
|
const headers = { "Content-Type": "application/json" };
|
|
409
435
|
if (token) headers["X-Test-Reset-Token"] = token;
|
|
410
436
|
const controller = new AbortController();
|
|
@@ -419,15 +445,16 @@ var resetServerState = async ({ baseUrl, token, requestTimeoutMs }) => {
|
|
|
419
445
|
|
|
420
446
|
// src/runRateLimitTests.ts
|
|
421
447
|
var runRateLimitTests = async (input) => {
|
|
422
|
-
const endpoints =
|
|
448
|
+
const endpoints = selectEndpoints(input);
|
|
423
449
|
const skip = input.skip ?? [];
|
|
424
450
|
const maxRateLimit = input.maxRateLimitToTest ?? 50;
|
|
425
451
|
const isAuthenticatedSweep = Boolean(input.headers?.Cookie);
|
|
426
452
|
const results = [];
|
|
427
453
|
for (const endpoint of endpoints) {
|
|
428
|
-
const
|
|
454
|
+
const metaMap = metaMapFor(endpoint, input.apiMetaMap, input.syncMetaMap);
|
|
455
|
+
const rateLimit = getRateLimit(metaMap, endpoint);
|
|
429
456
|
if (rateLimit === null) continue;
|
|
430
|
-
if (!isAuthenticatedSweep && requiresLogin(
|
|
457
|
+
if (!isAuthenticatedSweep && requiresLogin(metaMap, endpoint)) {
|
|
431
458
|
const skipped = {
|
|
432
459
|
endpoint,
|
|
433
460
|
status: "skipped",
|
|
@@ -472,14 +499,15 @@ var runRateLimitTests = async (input) => {
|
|
|
472
499
|
continue;
|
|
473
500
|
}
|
|
474
501
|
if (input.resetBetweenEndpoints) {
|
|
475
|
-
await resetServerState({ baseUrl: input.baseUrl, token: input.resetToken });
|
|
502
|
+
await resetServerState({ baseUrl: input.baseUrl, token: input.resetToken, only: ["rateLimits"] });
|
|
476
503
|
}
|
|
477
504
|
const result = await runRateLimitCheck({
|
|
478
505
|
endpoint,
|
|
479
506
|
baseUrl: input.baseUrl,
|
|
480
507
|
rateLimit,
|
|
481
508
|
inputFor: input.inputFor,
|
|
482
|
-
headers: input.headers
|
|
509
|
+
headers: input.headers,
|
|
510
|
+
transport: transportFor(endpoint, input.syncTransport)
|
|
483
511
|
});
|
|
484
512
|
results.push(result);
|
|
485
513
|
input.onResult?.(result);
|
|
@@ -501,14 +529,21 @@ var runCsrfEnforcementCheck = async (input) => {
|
|
|
501
529
|
durationMs: 0
|
|
502
530
|
};
|
|
503
531
|
}
|
|
504
|
-
const
|
|
532
|
+
const transport = input.transport ?? defaultProbeTransport;
|
|
533
|
+
if (transport.name !== "http") {
|
|
534
|
+
return {
|
|
535
|
+
endpoint,
|
|
536
|
+
status: "skipped",
|
|
537
|
+
reason: `CSRF enforcement only applies over HTTP (transport: ${transport.name})`,
|
|
538
|
+
durationMs: 0
|
|
539
|
+
};
|
|
540
|
+
}
|
|
505
541
|
const body = input.inputFor ? input.inputFor(endpoint) : {};
|
|
506
542
|
const started = Date.now();
|
|
507
543
|
const expectedErrorCode = input.expectedErrorCode ?? DEFAULT_EXPECTED_ERROR_CODE;
|
|
508
544
|
const expectedHttpStatus = input.expectedHttpStatus ?? DEFAULT_EXPECTED_HTTP_STATUS;
|
|
509
|
-
const result = await
|
|
510
|
-
|
|
511
|
-
method: endpoint.method,
|
|
545
|
+
const result = await transport.send({
|
|
546
|
+
endpoint,
|
|
512
547
|
baseUrl,
|
|
513
548
|
body,
|
|
514
549
|
headers: { Cookie: input.authCookie },
|
|
@@ -557,12 +592,13 @@ var runCsrfEnforcementCheck = async (input) => {
|
|
|
557
592
|
|
|
558
593
|
// src/runCsrfEnforcementTests.ts
|
|
559
594
|
var runCsrfEnforcementTests = async (input) => {
|
|
560
|
-
const endpoints =
|
|
595
|
+
const endpoints = selectEndpoints(input);
|
|
561
596
|
const skip = input.skip ?? [];
|
|
562
597
|
const results = [];
|
|
563
598
|
for (const endpoint of endpoints) {
|
|
564
|
-
|
|
565
|
-
if (!
|
|
599
|
+
const metaMap = metaMapFor(endpoint, input.apiMetaMap, input.syncMetaMap);
|
|
600
|
+
if (!hasMetaEntry(metaMap, endpoint)) continue;
|
|
601
|
+
if (!requiresLogin(metaMap, endpoint)) continue;
|
|
566
602
|
if (!STATE_CHANGING_METHODS.has(endpoint.method)) continue;
|
|
567
603
|
if (shouldSkip(endpoint, skip)) {
|
|
568
604
|
const skipped = {
|
|
@@ -582,7 +618,8 @@ var runCsrfEnforcementTests = async (input) => {
|
|
|
582
618
|
inputFor: input.inputFor,
|
|
583
619
|
requestTimeoutMs: input.requestTimeoutMs,
|
|
584
620
|
expectedErrorCode: input.expectedErrorCode,
|
|
585
|
-
expectedHttpStatus: input.expectedHttpStatus
|
|
621
|
+
expectedHttpStatus: input.expectedHttpStatus,
|
|
622
|
+
transport: transportFor(endpoint, input.syncTransport)
|
|
586
623
|
});
|
|
587
624
|
results.push(result);
|
|
588
625
|
input.onResult?.(result);
|
|
@@ -609,11 +646,10 @@ var JUNK_PAYLOADS = [
|
|
|
609
646
|
var runFuzzCheck = async (input) => {
|
|
610
647
|
const started = Date.now();
|
|
611
648
|
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS6;
|
|
612
|
-
const
|
|
649
|
+
const transport = input.transport ?? defaultProbeTransport;
|
|
613
650
|
for (const payload of JUNK_PAYLOADS) {
|
|
614
|
-
const result = await
|
|
615
|
-
|
|
616
|
-
method: input.endpoint.method,
|
|
651
|
+
const result = await transport.send({
|
|
652
|
+
endpoint: input.endpoint,
|
|
617
653
|
baseUrl: input.baseUrl,
|
|
618
654
|
body: payload,
|
|
619
655
|
headers: input.headers,
|
|
@@ -656,7 +692,7 @@ var runFuzzCheck = async (input) => {
|
|
|
656
692
|
|
|
657
693
|
// src/runFuzzTests.ts
|
|
658
694
|
var runFuzzTests = async (input) => {
|
|
659
|
-
const endpoints =
|
|
695
|
+
const endpoints = selectEndpoints(input);
|
|
660
696
|
const skip = input.skip ?? [];
|
|
661
697
|
const isAuthenticatedSweep = Boolean(input.headers?.Cookie);
|
|
662
698
|
const results = [];
|
|
@@ -686,7 +722,8 @@ var runFuzzTests = async (input) => {
|
|
|
686
722
|
const result = await runFuzzCheck({
|
|
687
723
|
endpoint,
|
|
688
724
|
baseUrl: input.baseUrl,
|
|
689
|
-
headers: input.headers
|
|
725
|
+
headers: input.headers,
|
|
726
|
+
transport: transportFor(endpoint, input.syncTransport)
|
|
690
727
|
});
|
|
691
728
|
results.push(result);
|
|
692
729
|
input.onResult?.(result);
|
|
@@ -694,81 +731,203 @@ var runFuzzTests = async (input) => {
|
|
|
694
731
|
return calculateSummary(results);
|
|
695
732
|
};
|
|
696
733
|
|
|
697
|
-
// src/
|
|
698
|
-
import
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
734
|
+
// src/socketProbeTransport.ts
|
|
735
|
+
import { tryCatch as tryCatch4 } from "@luckystack/core";
|
|
736
|
+
|
|
737
|
+
// src/testSocket.ts
|
|
738
|
+
import { io as createSocketIoClient } from "socket.io-client";
|
|
739
|
+
import {
|
|
740
|
+
buildJoinRoomResponseEventName,
|
|
741
|
+
buildSyncResponseEventName,
|
|
742
|
+
getProjectConfig as getProjectConfig2,
|
|
743
|
+
socketEventNames,
|
|
744
|
+
tryCatch as tryCatch3
|
|
745
|
+
} from "@luckystack/core";
|
|
746
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 3e3;
|
|
747
|
+
var DEFAULT_JOIN_TIMEOUT_MS = 3e3;
|
|
748
|
+
var responseIndexCounter = 0;
|
|
749
|
+
var nextResponseIndex = () => {
|
|
750
|
+
responseIndexCounter += 1;
|
|
751
|
+
return responseIndexCounter;
|
|
708
752
|
};
|
|
709
|
-
var
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
path.join(options.cwd ?? process.cwd(), "node_modules", ".luckystack", "dev-server.json"),
|
|
715
|
-
"utf8"
|
|
716
|
-
);
|
|
717
|
-
const info = JSON.parse(raw);
|
|
718
|
-
if (typeof info.port === "number" && Number.isInteger(info.port) && info.port > 0 && info.port <= 65535 && isProcessRunning(info.pid)) {
|
|
719
|
-
return `http://localhost:${String(info.port)}`;
|
|
753
|
+
var waitForConnect = (socket, label, timeoutMs) => {
|
|
754
|
+
return new Promise((resolve, reject) => {
|
|
755
|
+
if (socket.connected) {
|
|
756
|
+
resolve();
|
|
757
|
+
return;
|
|
720
758
|
}
|
|
721
|
-
|
|
759
|
+
const timer = setTimeout(() => {
|
|
760
|
+
cleanup();
|
|
761
|
+
reject(new Error(`${label}: socket failed to connect within ${timeoutMs}ms`));
|
|
762
|
+
}, timeoutMs);
|
|
763
|
+
const onConnect = () => {
|
|
764
|
+
cleanup();
|
|
765
|
+
resolve();
|
|
766
|
+
};
|
|
767
|
+
const onConnectError = (err) => {
|
|
768
|
+
cleanup();
|
|
769
|
+
reject(new Error(`${label}: connect_error \u2014 ${err.message}`));
|
|
770
|
+
};
|
|
771
|
+
const cleanup = () => {
|
|
772
|
+
clearTimeout(timer);
|
|
773
|
+
socket.off("connect", onConnect);
|
|
774
|
+
socket.off("connect_error", onConnectError);
|
|
775
|
+
};
|
|
776
|
+
socket.on("connect", onConnect);
|
|
777
|
+
socket.on("connect_error", onConnectError);
|
|
778
|
+
});
|
|
779
|
+
};
|
|
780
|
+
var connectTestSocket = async (input) => {
|
|
781
|
+
const sessionCookieName = input.sessionCookieName ?? getProjectConfig2().http.sessionCookieName;
|
|
782
|
+
const socket = createSocketIoClient(input.baseUrl, {
|
|
783
|
+
transports: ["websocket"],
|
|
784
|
+
forceNew: true,
|
|
785
|
+
reconnection: false,
|
|
786
|
+
auth: input.token ? { token: input.token } : void 0,
|
|
787
|
+
extraHeaders: input.token ? { Cookie: `${sessionCookieName}=${input.token}` } : void 0
|
|
788
|
+
});
|
|
789
|
+
const [connectError] = await tryCatch3(
|
|
790
|
+
() => waitForConnect(socket, input.label, input.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS)
|
|
791
|
+
);
|
|
792
|
+
if (connectError) {
|
|
793
|
+
socket.disconnect();
|
|
794
|
+
throw connectError;
|
|
722
795
|
}
|
|
723
|
-
return
|
|
796
|
+
return socket;
|
|
797
|
+
};
|
|
798
|
+
var RELEASE_TIMEOUT_MS = 1e3;
|
|
799
|
+
var releaseTestSocket = async (socket) => {
|
|
800
|
+
if (!socket.connected) return;
|
|
801
|
+
await new Promise((resolve) => {
|
|
802
|
+
const timer = setTimeout(() => {
|
|
803
|
+
socket.off("disconnect", onDisconnect);
|
|
804
|
+
resolve();
|
|
805
|
+
}, RELEASE_TIMEOUT_MS);
|
|
806
|
+
const onDisconnect = () => {
|
|
807
|
+
clearTimeout(timer);
|
|
808
|
+
resolve();
|
|
809
|
+
};
|
|
810
|
+
socket.once("disconnect", onDisconnect);
|
|
811
|
+
socket.emit(socketEventNames.intentionalDisconnect);
|
|
812
|
+
});
|
|
813
|
+
socket.disconnect();
|
|
814
|
+
};
|
|
815
|
+
var joinTestRoom = (socket, room, options) => {
|
|
816
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_JOIN_TIMEOUT_MS;
|
|
817
|
+
const responseIndex = nextResponseIndex();
|
|
818
|
+
const eventName = buildJoinRoomResponseEventName(responseIndex);
|
|
819
|
+
return new Promise((resolve, reject) => {
|
|
820
|
+
const timer = setTimeout(() => {
|
|
821
|
+
socket.off(eventName, onAck);
|
|
822
|
+
reject(new Error(`${options.label}: joinRoom ack timeout after ${timeoutMs}ms`));
|
|
823
|
+
}, timeoutMs);
|
|
824
|
+
const onAck = (payload) => {
|
|
825
|
+
clearTimeout(timer);
|
|
826
|
+
socket.off(eventName, onAck);
|
|
827
|
+
if (typeof payload === "object" && payload !== null && payload.status === "error") {
|
|
828
|
+
const errorCode = payload.errorCode;
|
|
829
|
+
reject(new Error(`${options.label}: joinRoom rejected \u2014 ${typeof errorCode === "string" ? errorCode : "unknown"}`));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
resolve();
|
|
833
|
+
};
|
|
834
|
+
socket.on(eventName, onAck);
|
|
835
|
+
socket.emit(socketEventNames.joinRoom, { group: room, responseIndex });
|
|
836
|
+
});
|
|
837
|
+
};
|
|
838
|
+
var emitSyncOverSocket = (socket, input) => {
|
|
839
|
+
const responseIndex = nextResponseIndex();
|
|
840
|
+
const eventName = buildSyncResponseEventName(responseIndex);
|
|
841
|
+
return new Promise((resolve) => {
|
|
842
|
+
const timer = setTimeout(() => {
|
|
843
|
+
socket.off(eventName, onResponse);
|
|
844
|
+
resolve({ kind: "timeout" });
|
|
845
|
+
}, input.timeoutMs);
|
|
846
|
+
const onResponse = (payload) => {
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
socket.off(eventName, onResponse);
|
|
849
|
+
resolve({ kind: "response", payload });
|
|
850
|
+
};
|
|
851
|
+
socket.on(eventName, onResponse);
|
|
852
|
+
socket.emit(socketEventNames.sync, {
|
|
853
|
+
name: `sync/${input.routePath}`,
|
|
854
|
+
//? Only `undefined` defaults — a fuzz probe's `null` must reach the server as-is.
|
|
855
|
+
data: input.data === void 0 ? {} : input.data,
|
|
856
|
+
//? Same fallback cancel key the real client sends before the server's
|
|
857
|
+
//? `__cancelId` handshake arrives; the runner never cancels, so it is
|
|
858
|
+
//? only there to keep the request shape identical.
|
|
859
|
+
cb: input.routePath,
|
|
860
|
+
receiver: input.receiver,
|
|
861
|
+
responseIndex,
|
|
862
|
+
ignoreSelf: input.ignoreSelf ?? false
|
|
863
|
+
});
|
|
864
|
+
});
|
|
724
865
|
};
|
|
725
866
|
|
|
726
|
-
// src/
|
|
727
|
-
|
|
728
|
-
var
|
|
729
|
-
if (
|
|
730
|
-
|
|
867
|
+
// src/socketProbeTransport.ts
|
|
868
|
+
var LABEL = "sync probe";
|
|
869
|
+
var tokenFromCookieHeader = (cookieHeader, cookieName) => {
|
|
870
|
+
if (!cookieHeader) return null;
|
|
871
|
+
for (const part of cookieHeader.split(";")) {
|
|
872
|
+
const [rawName, ...rest] = part.trim().split("=");
|
|
873
|
+
if (rawName === cookieName && rest.length > 0) return rest.join("=");
|
|
731
874
|
}
|
|
875
|
+
return null;
|
|
732
876
|
};
|
|
733
|
-
var
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
throw new TypeError("[test-runner] config.secretManager.url must be a string.");
|
|
743
|
-
}
|
|
744
|
-
if (!isToken(candidate.token)) {
|
|
745
|
-
throw new TypeError(
|
|
746
|
-
"[test-runner] config.secretManager.token must be a string or { fromFile: string }."
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
return { ...candidate, url, token: candidate.token };
|
|
877
|
+
var toProbeResponse = (payload) => {
|
|
878
|
+
if (typeof payload !== "object" || payload === null) return { parsed: null };
|
|
879
|
+
const record = payload;
|
|
880
|
+
return {
|
|
881
|
+
parsed: {
|
|
882
|
+
status: typeof record.status === "string" ? record.status : void 0,
|
|
883
|
+
errorCode: typeof record.errorCode === "string" ? record.errorCode : void 0
|
|
884
|
+
}
|
|
885
|
+
};
|
|
750
886
|
};
|
|
751
|
-
var
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
() =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
887
|
+
var createSocketProbeTransport = (options) => {
|
|
888
|
+
const http = createHttpProbeTransport({ syncReceiver: options.receiver });
|
|
889
|
+
const sockets = /* @__PURE__ */ new Map();
|
|
890
|
+
const socketFor = (token) => {
|
|
891
|
+
const key = token ?? "";
|
|
892
|
+
const existing = sockets.get(key);
|
|
893
|
+
if (existing) return existing;
|
|
894
|
+
const opening = (async () => {
|
|
895
|
+
const [connectError, socket] = await tryCatch4(() => connectTestSocket({
|
|
896
|
+
baseUrl: options.baseUrl,
|
|
897
|
+
token,
|
|
898
|
+
label: LABEL,
|
|
899
|
+
sessionCookieName: options.sessionCookieName
|
|
900
|
+
}));
|
|
901
|
+
if (connectError || !socket) return null;
|
|
902
|
+
if (token) await tryCatch4(() => joinTestRoom(socket, options.receiver, { label: LABEL }));
|
|
903
|
+
return socket;
|
|
904
|
+
})();
|
|
905
|
+
sockets.set(key, opening);
|
|
906
|
+
return opening;
|
|
907
|
+
};
|
|
908
|
+
const send = async (request) => {
|
|
909
|
+
if (request.endpoint.kind !== "sync") return http.send(request);
|
|
910
|
+
const socket = await socketFor(tokenFromCookieHeader(request.headers?.Cookie, options.sessionCookieName));
|
|
911
|
+
if (!socket) return null;
|
|
912
|
+
const outcome = await emitSyncOverSocket(socket, {
|
|
913
|
+
routePath: `${request.endpoint.page}/${request.endpoint.name}/${request.endpoint.version}`,
|
|
914
|
+
data: request.body,
|
|
915
|
+
receiver: options.receiver,
|
|
916
|
+
timeoutMs: request.requestTimeoutMs
|
|
917
|
+
});
|
|
918
|
+
return outcome.kind === "response" ? toProbeResponse(outcome.payload) : null;
|
|
919
|
+
};
|
|
920
|
+
const close = async () => {
|
|
921
|
+
const opened = [...sockets.entries()];
|
|
922
|
+
sockets.clear();
|
|
923
|
+
for (const [key, pending] of opened) {
|
|
924
|
+
const socket = await pending;
|
|
925
|
+
if (!socket) continue;
|
|
926
|
+
if (key) await releaseTestSocket(socket);
|
|
927
|
+
else socket.disconnect();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
return { name: "socket", send, close };
|
|
772
931
|
};
|
|
773
932
|
|
|
774
933
|
// src/schemaSampleInput.ts
|
|
@@ -886,6 +1045,90 @@ var sampleSchemaInput = (schema, options) => {
|
|
|
886
1045
|
return sampleForDef(node.def ?? node._def, options);
|
|
887
1046
|
};
|
|
888
1047
|
|
|
1048
|
+
// src/sampleInputFor.ts
|
|
1049
|
+
var createSampleInputFor = (options) => (endpoint) => {
|
|
1050
|
+
const schemas = endpoint.kind === "sync" ? options.syncInputSchemas : options.apiInputSchemas;
|
|
1051
|
+
const schema = schemas?.[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
1052
|
+
return schema ? sampleSchemaInput(schema, { stringPrefix: options.stringPrefix }) : {};
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// src/resolveTestBaseUrl.ts
|
|
1056
|
+
import fs from "fs";
|
|
1057
|
+
import path from "path";
|
|
1058
|
+
var isProcessRunning = (pid) => {
|
|
1059
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
1060
|
+
try {
|
|
1061
|
+
process.kill(pid, 0);
|
|
1062
|
+
return true;
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
var resolveTestBaseUrl = (options = {}) => {
|
|
1068
|
+
const explicit = process.env.TEST_BASE_URL;
|
|
1069
|
+
if (explicit) return explicit;
|
|
1070
|
+
try {
|
|
1071
|
+
const raw = fs.readFileSync(
|
|
1072
|
+
path.join(options.cwd ?? process.cwd(), "node_modules", ".luckystack", "dev-server.json"),
|
|
1073
|
+
"utf8"
|
|
1074
|
+
);
|
|
1075
|
+
const info = JSON.parse(raw);
|
|
1076
|
+
if (typeof info.port === "number" && Number.isInteger(info.port) && info.port > 0 && info.port <= 65535 && isProcessRunning(info.pid)) {
|
|
1077
|
+
return `http://localhost:${String(info.port)}`;
|
|
1078
|
+
}
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
return options.fallbackUrl ?? "http://localhost:80";
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// src/resolveTestEnvironment.ts
|
|
1085
|
+
import { loadEnvFiles, tryCatch as tryCatch5 } from "@luckystack/core";
|
|
1086
|
+
var assertProjectConfigLoader = (input, caller) => {
|
|
1087
|
+
if (typeof input !== "object" || input === null || !("loadProjectConfig" in input) || typeof input.loadProjectConfig !== "function") {
|
|
1088
|
+
throw new TypeError(`[test-runner] ${caller} requires a lazy loadProjectConfig callback`);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
1092
|
+
var isToken = (value) => typeof value === "string" || isRecord(value) && typeof value.fromFile === "string";
|
|
1093
|
+
var readSecretManagerConfig = (projectConfig) => {
|
|
1094
|
+
if (!isRecord(projectConfig)) return null;
|
|
1095
|
+
const candidate = projectConfig.secretManager;
|
|
1096
|
+
if (!isRecord(candidate)) return null;
|
|
1097
|
+
const url = candidate.url;
|
|
1098
|
+
if (url === void 0 || url === "") return null;
|
|
1099
|
+
if (typeof url !== "string") {
|
|
1100
|
+
throw new TypeError("[test-runner] config.secretManager.url must be a string.");
|
|
1101
|
+
}
|
|
1102
|
+
if (!isToken(candidate.token)) {
|
|
1103
|
+
throw new TypeError(
|
|
1104
|
+
"[test-runner] config.secretManager.token must be a string or { fromFile: string }."
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
return { ...candidate, url, token: candidate.token };
|
|
1108
|
+
};
|
|
1109
|
+
var resolveTestEnvironment = async (input = {}) => {
|
|
1110
|
+
loadEnvFiles();
|
|
1111
|
+
if (!input.loadProjectConfig) return;
|
|
1112
|
+
const projectConfig = await input.loadProjectConfig();
|
|
1113
|
+
const secretManagerConfig = readSecretManagerConfig(projectConfig);
|
|
1114
|
+
if (!secretManagerConfig) return;
|
|
1115
|
+
const [importError, secretManager] = await tryCatch5(
|
|
1116
|
+
() => import("@luckystack/secret-manager")
|
|
1117
|
+
);
|
|
1118
|
+
if (importError || !secretManager) {
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
"[test-runner] config.secretManager.url is set, but @luckystack/secret-manager could not be loaded. Install the package or remove the secret-manager config; tests will not continue with unresolved env pointers.",
|
|
1121
|
+
{ cause: importError ?? void 0 }
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
await secretManager.initSecretManager({
|
|
1125
|
+
...secretManagerConfig,
|
|
1126
|
+
//? Match the normal server/ORM bootstrap: configured remote resolution is
|
|
1127
|
+
//? fail-fast. A raw DATABASE_URL_V<n> must never leak into Prisma.
|
|
1128
|
+
source: "remote"
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
|
|
889
1132
|
// src/testDataMarker.ts
|
|
890
1133
|
import { randomUUID } from "crypto";
|
|
891
1134
|
var TEST_DATA_PREFIX = "lstest_";
|
|
@@ -939,84 +1182,26 @@ import crypto from "crypto";
|
|
|
939
1182
|
import fs2 from "fs";
|
|
940
1183
|
import path2 from "path";
|
|
941
1184
|
import { pathToFileURL } from "url";
|
|
942
|
-
import { getCsrfConfig, getPrismaClient, getProjectConfig as getProjectConfig3, getSrcDir, tryCatch as tryCatch5, tryCatchSync } from "@luckystack/core";
|
|
943
|
-
|
|
944
|
-
// src/streamWatcher.ts
|
|
945
|
-
import { io as createSocketIoClient } from "socket.io-client";
|
|
946
1185
|
import {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
getProjectConfig as
|
|
1186
|
+
buildRoutedGetUrl as buildRoutedGetUrl2,
|
|
1187
|
+
getCsrfConfig,
|
|
1188
|
+
getPrismaClient,
|
|
1189
|
+
getProjectConfig as getProjectConfig3,
|
|
1190
|
+
getSrcDir,
|
|
1191
|
+
tryCatch as tryCatch7,
|
|
1192
|
+
tryCatchSync
|
|
951
1193
|
} from "@luckystack/core";
|
|
1194
|
+
|
|
1195
|
+
// src/streamWatcher.ts
|
|
1196
|
+
import { socketEventNames as socketEventNames2, tryCatch as tryCatch6 } from "@luckystack/core";
|
|
952
1197
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
953
|
-
var
|
|
954
|
-
var CONNECT_TIMEOUT_MS = 3e3;
|
|
955
|
-
var isJoinRoomResponse = (value) => typeof value === "object" && value !== null;
|
|
1198
|
+
var LABEL2 = "watchStream";
|
|
956
1199
|
var isStreamChunkFrame = (value) => typeof value === "object" && value !== null;
|
|
957
|
-
var responseIndexCounter = 0;
|
|
958
|
-
var nextResponseIndex = () => {
|
|
959
|
-
responseIndexCounter += 1;
|
|
960
|
-
return responseIndexCounter;
|
|
961
|
-
};
|
|
962
|
-
var waitForConnect = (socket) => {
|
|
963
|
-
return new Promise((resolve, reject) => {
|
|
964
|
-
if (socket.connected) {
|
|
965
|
-
resolve();
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const timer = setTimeout(() => {
|
|
969
|
-
cleanup();
|
|
970
|
-
reject(new Error(`watchStream: socket failed to connect within ${CONNECT_TIMEOUT_MS}ms`));
|
|
971
|
-
}, CONNECT_TIMEOUT_MS);
|
|
972
|
-
const onConnect = () => {
|
|
973
|
-
cleanup();
|
|
974
|
-
resolve();
|
|
975
|
-
};
|
|
976
|
-
const onConnectError = (err) => {
|
|
977
|
-
cleanup();
|
|
978
|
-
reject(new Error(`watchStream: connect_error \u2014 ${err.message}`));
|
|
979
|
-
};
|
|
980
|
-
const cleanup = () => {
|
|
981
|
-
clearTimeout(timer);
|
|
982
|
-
socket.off("connect", onConnect);
|
|
983
|
-
socket.off("connect_error", onConnectError);
|
|
984
|
-
};
|
|
985
|
-
socket.on("connect", onConnect);
|
|
986
|
-
socket.on("connect_error", onConnectError);
|
|
987
|
-
});
|
|
988
|
-
};
|
|
989
|
-
var waitForJoinAck = (socket, responseIndex) => {
|
|
990
|
-
const eventName = buildJoinRoomResponseEventName(responseIndex);
|
|
991
|
-
return new Promise((resolve, reject) => {
|
|
992
|
-
const timer = setTimeout(() => {
|
|
993
|
-
socket.off(eventName, onAck);
|
|
994
|
-
reject(new Error(`watchStream: joinRoom ack timeout after ${JOIN_RESPONSE_TIMEOUT_MS}ms`));
|
|
995
|
-
}, JOIN_RESPONSE_TIMEOUT_MS);
|
|
996
|
-
const onAck = (payload) => {
|
|
997
|
-
clearTimeout(timer);
|
|
998
|
-
socket.off(eventName, onAck);
|
|
999
|
-
if (isJoinRoomResponse(payload) && payload.status === "error") {
|
|
1000
|
-
reject(new Error(`watchStream: joinRoom rejected \u2014 ${payload.errorCode ?? "unknown"}`));
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
resolve();
|
|
1004
|
-
};
|
|
1005
|
-
socket.on(eventName, onAck);
|
|
1006
|
-
});
|
|
1007
|
-
};
|
|
1008
1200
|
var openStreamWatcher = async (input) => {
|
|
1009
1201
|
const defaultTimeoutMs = input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1010
1202
|
const chunks = [];
|
|
1011
1203
|
const waiters = [];
|
|
1012
|
-
const
|
|
1013
|
-
const socket = createSocketIoClient(input.baseUrl, {
|
|
1014
|
-
transports: ["websocket"],
|
|
1015
|
-
forceNew: true,
|
|
1016
|
-
reconnection: false,
|
|
1017
|
-
auth: input.token ? { token: input.token } : void 0,
|
|
1018
|
-
extraHeaders: input.token ? { Cookie: `${sessionCookieName}=${input.token}` } : void 0
|
|
1019
|
-
});
|
|
1204
|
+
const socket = await connectTestSocket({ baseUrl: input.baseUrl, token: input.token, label: LABEL2 });
|
|
1020
1205
|
const handleChunk = (raw) => {
|
|
1021
1206
|
if (!isStreamChunkFrame(raw)) return;
|
|
1022
1207
|
if (raw.status !== "stream") return;
|
|
@@ -1032,21 +1217,10 @@ var openStreamWatcher = async (input) => {
|
|
|
1032
1217
|
}
|
|
1033
1218
|
}
|
|
1034
1219
|
};
|
|
1035
|
-
socket.on(
|
|
1036
|
-
const [
|
|
1037
|
-
if (connectError) {
|
|
1038
|
-
socket.off(socketEventNames.sync, handleChunk);
|
|
1039
|
-
socket.disconnect();
|
|
1040
|
-
throw connectError;
|
|
1041
|
-
}
|
|
1042
|
-
const responseIndex = nextResponseIndex();
|
|
1043
|
-
socket.emit(socketEventNames.joinRoom, {
|
|
1044
|
-
group: input.roomCode,
|
|
1045
|
-
responseIndex
|
|
1046
|
-
});
|
|
1047
|
-
const [joinError] = await tryCatch4(() => waitForJoinAck(socket, responseIndex));
|
|
1220
|
+
socket.on(socketEventNames2.sync, handleChunk);
|
|
1221
|
+
const [joinError] = await tryCatch6(() => joinTestRoom(socket, input.roomCode, { label: LABEL2 }));
|
|
1048
1222
|
if (joinError) {
|
|
1049
|
-
socket.off(
|
|
1223
|
+
socket.off(socketEventNames2.sync, handleChunk);
|
|
1050
1224
|
socket.disconnect();
|
|
1051
1225
|
throw joinError;
|
|
1052
1226
|
}
|
|
@@ -1054,7 +1228,7 @@ var openStreamWatcher = async (input) => {
|
|
|
1054
1228
|
const close = () => {
|
|
1055
1229
|
if (closed) return Promise.resolve();
|
|
1056
1230
|
closed = true;
|
|
1057
|
-
socket.off(
|
|
1231
|
+
socket.off(socketEventNames2.sync, handleChunk);
|
|
1058
1232
|
for (const waiter of waiters) waiter.resolve();
|
|
1059
1233
|
waiters.length = 0;
|
|
1060
1234
|
socket.disconnect();
|
|
@@ -1158,7 +1332,7 @@ var buildExpect = () => ({
|
|
|
1158
1332
|
if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);
|
|
1159
1333
|
},
|
|
1160
1334
|
throws: async (fn, message) => {
|
|
1161
|
-
const [err] = await
|
|
1335
|
+
const [err] = await tryCatch7(fn);
|
|
1162
1336
|
if (!err) throw new AssertionError(message ?? "expected fn to throw, but it did not");
|
|
1163
1337
|
return err;
|
|
1164
1338
|
},
|
|
@@ -1210,7 +1384,7 @@ var buildSessionHelpers = (state) => {
|
|
|
1210
1384
|
};
|
|
1211
1385
|
};
|
|
1212
1386
|
var parseResponse = async (response, state) => {
|
|
1213
|
-
const [textError, text] = await
|
|
1387
|
+
const [textError, text] = await tryCatch7(() => response.text());
|
|
1214
1388
|
if (textError || text === null) {
|
|
1215
1389
|
state.lastResponse = null;
|
|
1216
1390
|
throw new Error(`failed to read response body (HTTP ${response.status}): ${textError?.message ?? "unknown"}`);
|
|
@@ -1226,7 +1400,7 @@ var parseResponse = async (response, state) => {
|
|
|
1226
1400
|
};
|
|
1227
1401
|
var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
1228
1402
|
return async (input) => {
|
|
1229
|
-
const
|
|
1403
|
+
const path4 = `${baseUrl.replace(/\/$/, "")}/api/${routePath}`;
|
|
1230
1404
|
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
1231
1405
|
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
1232
1406
|
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
@@ -1235,8 +1409,10 @@ var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
|
1235
1409
|
controller.abort();
|
|
1236
1410
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1237
1411
|
const init = { method, headers, signal: controller.signal };
|
|
1238
|
-
|
|
1239
|
-
const
|
|
1412
|
+
const bodyless = method === "GET" || method === "HEAD";
|
|
1413
|
+
const url = bodyless ? buildRoutedGetUrl2(path4, input ?? {}) : path4;
|
|
1414
|
+
if (!bodyless) init.body = JSON.stringify(input ?? {});
|
|
1415
|
+
const [fetchError, response] = await tryCatch7(() => fetch(url, init));
|
|
1240
1416
|
clearTimeout(timeoutHandle);
|
|
1241
1417
|
if (fetchError || !response) {
|
|
1242
1418
|
state.lastResponse = null;
|
|
@@ -1245,18 +1421,57 @@ var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
|
1245
1421
|
return await parseResponse(response, state);
|
|
1246
1422
|
};
|
|
1247
1423
|
};
|
|
1248
|
-
var
|
|
1424
|
+
var createCaseSyncSocket = (baseUrl, cookieName) => {
|
|
1425
|
+
let current = null;
|
|
1426
|
+
const close = () => {
|
|
1427
|
+
current?.socket.disconnect();
|
|
1428
|
+
current = null;
|
|
1429
|
+
};
|
|
1430
|
+
return {
|
|
1431
|
+
get: async (token) => {
|
|
1432
|
+
if (current?.token === token && current.socket.connected) return current;
|
|
1433
|
+
close();
|
|
1434
|
+
const socket = await connectTestSocket({ baseUrl, token, label: "callSync", sessionCookieName: cookieName });
|
|
1435
|
+
current = { socket, token, joinedRooms: /* @__PURE__ */ new Set() };
|
|
1436
|
+
return current;
|
|
1437
|
+
},
|
|
1438
|
+
close
|
|
1439
|
+
};
|
|
1440
|
+
};
|
|
1441
|
+
var resolveSyncTransport = (opts) => opts?.transport ?? (getProjectConfig3().transport.invocation === "routed-http" ? "http" : "socket");
|
|
1442
|
+
var buildCallSync = (baseUrl, routePath, state, cookieName, syncSocket) => {
|
|
1249
1443
|
return async (input, opts) => {
|
|
1444
|
+
if (resolveSyncTransport(opts) === "socket") {
|
|
1445
|
+
const { socket, joinedRooms } = await syncSocket.get(state.token);
|
|
1446
|
+
const { receiver, joinRoom = true } = opts ?? {};
|
|
1447
|
+
if (receiver && receiver !== "all" && joinRoom && !joinedRooms.has(receiver)) {
|
|
1448
|
+
const [joinError] = await tryCatch7(() => joinTestRoom(socket, receiver, { label: "callSync" }));
|
|
1449
|
+
if (!joinError) joinedRooms.add(receiver);
|
|
1450
|
+
}
|
|
1451
|
+
const outcome = await emitSyncOverSocket(socket, {
|
|
1452
|
+
routePath,
|
|
1453
|
+
data: input,
|
|
1454
|
+
receiver,
|
|
1455
|
+
ignoreSelf: opts?.ignoreSelf,
|
|
1456
|
+
timeoutMs: DEFAULT_CALL_TIMEOUT_MS
|
|
1457
|
+
});
|
|
1458
|
+
if (outcome.kind === "timeout") {
|
|
1459
|
+
state.lastResponse = null;
|
|
1460
|
+
throw new Error(`callSync ${routePath} failed: no socket response within ${DEFAULT_CALL_TIMEOUT_MS}ms`);
|
|
1461
|
+
}
|
|
1462
|
+
state.lastResponse = outcome.payload;
|
|
1463
|
+
return outcome.payload;
|
|
1464
|
+
}
|
|
1250
1465
|
const url = `${baseUrl.replace(/\/$/, "")}/sync/${routePath}`;
|
|
1251
1466
|
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
1252
1467
|
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
1253
1468
|
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
1254
|
-
const body = { data: input ?? {}, receiver: opts?.receiver };
|
|
1469
|
+
const body = { data: input ?? {}, receiver: opts?.receiver, ignoreSelf: opts?.ignoreSelf };
|
|
1255
1470
|
const controller = new AbortController();
|
|
1256
1471
|
const timeoutHandle = setTimeout(() => {
|
|
1257
1472
|
controller.abort();
|
|
1258
1473
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1259
|
-
const [fetchError, response] = await
|
|
1474
|
+
const [fetchError, response] = await tryCatch7(() => fetch(url, {
|
|
1260
1475
|
method: "POST",
|
|
1261
1476
|
headers,
|
|
1262
1477
|
body: JSON.stringify(body),
|
|
@@ -1287,18 +1502,20 @@ var buildContext = (discovery, input) => {
|
|
|
1287
1502
|
watchers.push(watcher);
|
|
1288
1503
|
return watcher;
|
|
1289
1504
|
};
|
|
1505
|
+
const syncSocket = createCaseSyncSocket(input.baseUrl, cookieName);
|
|
1290
1506
|
const closeAllWatchers = async () => {
|
|
1507
|
+
syncSocket.close();
|
|
1291
1508
|
while (watchers.length > 0) {
|
|
1292
1509
|
const watcher = watchers.pop();
|
|
1293
1510
|
if (!watcher) continue;
|
|
1294
|
-
const [closeError] = await
|
|
1511
|
+
const [closeError] = await tryCatch7(() => watcher.close());
|
|
1295
1512
|
if (closeError) {
|
|
1296
1513
|
}
|
|
1297
1514
|
}
|
|
1298
1515
|
};
|
|
1299
1516
|
const ctx = {
|
|
1300
1517
|
callApi: buildCallApi(input.baseUrl, routePath, state, cookieName, apiMethod),
|
|
1301
|
-
callSync: buildCallSync(input.baseUrl, routePath, state, cookieName),
|
|
1518
|
+
callSync: buildCallSync(input.baseUrl, routePath, state, cookieName, syncSocket),
|
|
1302
1519
|
watchStream,
|
|
1303
1520
|
session: buildSessionHelpers(state),
|
|
1304
1521
|
//? Lazy: only resolves when a case actually touches `ctx.prisma`, so an
|
|
@@ -1333,7 +1550,7 @@ var runCustomTestsAfterEnvironmentPrepared = async (input) => {
|
|
|
1333
1550
|
input.onResult?.(r);
|
|
1334
1551
|
continue;
|
|
1335
1552
|
}
|
|
1336
|
-
const [importError, mod] = await
|
|
1553
|
+
const [importError, mod] = await tryCatch7(
|
|
1337
1554
|
() => import(pathToFileURL(resolvedPath).href)
|
|
1338
1555
|
);
|
|
1339
1556
|
if (importError || !mod) {
|
|
@@ -1358,11 +1575,11 @@ var runCustomTestsAfterEnvironmentPrepared = async (input) => {
|
|
|
1358
1575
|
const expectedToFail = typeof c.expectedToFail === "string" ? c.expectedToFail : void 0;
|
|
1359
1576
|
const built = buildContext(discovery, input);
|
|
1360
1577
|
const started = Date.now();
|
|
1361
|
-
const [runError] = await
|
|
1362
|
-
await
|
|
1578
|
+
const [runError] = await tryCatch7(async () => c.run(built.ctx));
|
|
1579
|
+
await tryCatch7(() => built.closeAllWatchers());
|
|
1363
1580
|
const residualToken = built.state.token;
|
|
1364
1581
|
if (residualToken) {
|
|
1365
|
-
await
|
|
1582
|
+
await tryCatch7(async () => {
|
|
1366
1583
|
const { deleteSession } = await import("@luckystack/login");
|
|
1367
1584
|
await deleteSession(residualToken);
|
|
1368
1585
|
});
|
|
@@ -1403,11 +1620,10 @@ var runCustomTests = async (input) => {
|
|
|
1403
1620
|
};
|
|
1404
1621
|
|
|
1405
1622
|
// src/runAllTests.ts
|
|
1406
|
-
import
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
};
|
|
1623
|
+
import crypto2 from "crypto";
|
|
1624
|
+
import fs3 from "fs";
|
|
1625
|
+
import path3 from "path";
|
|
1626
|
+
import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as getProjectConfig4, tryCatch as tryCatch8, tryCatchSync as tryCatchSync2 } from "@luckystack/core";
|
|
1411
1627
|
var resolveResetBookendToken = (input) => {
|
|
1412
1628
|
if (input.resetBookends === false) return null;
|
|
1413
1629
|
const token = input.resetToken ?? process.env.TEST_RESET_TOKEN;
|
|
@@ -1419,15 +1635,6 @@ var resolveResetBookendToken = (input) => {
|
|
|
1419
1635
|
}
|
|
1420
1636
|
return token;
|
|
1421
1637
|
};
|
|
1422
|
-
var matchesFilter = (endpoint, filter) => {
|
|
1423
|
-
if (!filter) return true;
|
|
1424
|
-
return `${endpoint.page}/${endpoint.name}/${endpoint.version}`.includes(filter);
|
|
1425
|
-
};
|
|
1426
|
-
var filterResults = (results, filter) => filter ? results.filter((r) => matchesFilter(r.endpoint, filter)) : results;
|
|
1427
|
-
var cloneSummary = (summary, filter) => {
|
|
1428
|
-
if (!filter) return summary;
|
|
1429
|
-
return calculateSummary(filterResults(summary.results, filter));
|
|
1430
|
-
};
|
|
1431
1638
|
var buildAuthHeaders = async (input) => {
|
|
1432
1639
|
const headers = {};
|
|
1433
1640
|
if (!input.authToken) return headers;
|
|
@@ -1448,70 +1655,92 @@ var buildAuthHeaders = async (input) => {
|
|
|
1448
1655
|
}
|
|
1449
1656
|
return headers;
|
|
1450
1657
|
};
|
|
1658
|
+
var prepareSyncSweep = async (input) => {
|
|
1659
|
+
const receiver = `lstest-room-${crypto2.randomUUID()}`;
|
|
1660
|
+
const sessionCookieName = input.sessionCookieName ?? getProjectConfig4().http.sessionCookieName;
|
|
1661
|
+
const mode = input.syncTransport ?? (getProjectConfig4().transport.invocation === "routed-http" ? "http" : "socket");
|
|
1662
|
+
const http = createHttpProbeTransport({ syncReceiver: receiver });
|
|
1663
|
+
let memberSocket = null;
|
|
1664
|
+
if (input.authToken) {
|
|
1665
|
+
const authToken = input.authToken;
|
|
1666
|
+
const [connectError, socket] = await tryCatch8(() => connectTestSocket({
|
|
1667
|
+
baseUrl: input.baseUrl,
|
|
1668
|
+
token: authToken,
|
|
1669
|
+
label: "sync sweep",
|
|
1670
|
+
sessionCookieName
|
|
1671
|
+
}));
|
|
1672
|
+
if (connectError || !socket) {
|
|
1673
|
+
console.warn(`[test-runner] runAllTests: could not open a socket for the sync sweep \u2014 ${connectError?.message ?? "no socket"}; sync probes will be rejected as non-members.`);
|
|
1674
|
+
} else {
|
|
1675
|
+
memberSocket = socket;
|
|
1676
|
+
const [joinError] = await tryCatch8(() => joinTestRoom(socket, receiver, { label: "sync sweep" }));
|
|
1677
|
+
if (joinError) console.warn(`[test-runner] runAllTests: ${joinError.message}; sync probes will be rejected as non-members.`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const sweep = mode === "socket" ? createSocketProbeTransport({ baseUrl: input.baseUrl, receiver, sessionCookieName }) : http;
|
|
1681
|
+
return {
|
|
1682
|
+
sweep,
|
|
1683
|
+
csrf: http,
|
|
1684
|
+
close: async () => {
|
|
1685
|
+
await sweep.close?.();
|
|
1686
|
+
if (memberSocket) await releaseTestSocket(memberSocket);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
};
|
|
1451
1690
|
var runSweepLayers = async (input, summary, headers, inputFor) => {
|
|
1691
|
+
const resetToken = input.resetToken ?? process.env.TEST_RESET_TOKEN;
|
|
1452
1692
|
const resetBookendToken = resolveResetBookendToken(input);
|
|
1453
1693
|
if (resetBookendToken) {
|
|
1454
|
-
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1694
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken, only: ["rateLimits"] });
|
|
1455
1695
|
}
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
skip: input.skip,
|
|
1460
|
-
headers,
|
|
1461
|
-
inputFor
|
|
1462
|
-
});
|
|
1463
|
-
summary.contract = cloneSummary(contract, input.filter);
|
|
1464
|
-
const auth = await runAuthEnforcementTests({
|
|
1696
|
+
const hasSyncRoutes = Object.keys(input.syncMetaMap ?? {}).length > 0;
|
|
1697
|
+
const sync = hasSyncRoutes ? await prepareSyncSweep(input) : null;
|
|
1698
|
+
const shared = {
|
|
1465
1699
|
apiMethodMap: input.apiMethodMap,
|
|
1466
|
-
|
|
1700
|
+
syncMetaMap: input.syncMetaMap,
|
|
1467
1701
|
baseUrl: input.baseUrl,
|
|
1468
1702
|
skip: input.skip,
|
|
1469
|
-
|
|
1703
|
+
filter: input.filter,
|
|
1704
|
+
syncTransport: sync?.sweep
|
|
1705
|
+
};
|
|
1706
|
+
const [layerError] = await tryCatch8(async () => {
|
|
1707
|
+
summary.contract = await runContractTests({ ...shared, headers, inputFor });
|
|
1708
|
+
summary.auth = await runAuthEnforcementTests({ ...shared, apiMetaMap: input.apiMetaMap, inputFor });
|
|
1709
|
+
if (!input.noRateLimit) {
|
|
1710
|
+
summary.rateLimit = await runRateLimitTests({
|
|
1711
|
+
...shared,
|
|
1712
|
+
apiMetaMap: input.apiMetaMap,
|
|
1713
|
+
headers,
|
|
1714
|
+
inputFor,
|
|
1715
|
+
//? Reset the shared per-IP rate-limit bucket between endpoints so a
|
|
1716
|
+
//? neighbour saturating the window doesn't cause false pass/fail on the
|
|
1717
|
+
//? next endpoint. Requires the server to expose /_test/reset (NODE_ENV in
|
|
1718
|
+
//? { 'development', 'test' } AND TEST_RESET_TOKEN set + matching header).
|
|
1719
|
+
resetBetweenEndpoints: true,
|
|
1720
|
+
//? Thread the reset token (explicit input, else the TEST_RESET_TOKEN env the
|
|
1721
|
+
//? server validates against) — without it `/_test/reset` returns 403 and the
|
|
1722
|
+
//? between-endpoint reset silently never happens.
|
|
1723
|
+
resetToken
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
if (!input.noCsrf && headers.Cookie) {
|
|
1727
|
+
summary.csrf = await runCsrfEnforcementTests({
|
|
1728
|
+
...shared,
|
|
1729
|
+
apiMetaMap: input.apiMetaMap,
|
|
1730
|
+
authCookie: headers.Cookie,
|
|
1731
|
+
inputFor,
|
|
1732
|
+
syncTransport: sync?.csrf
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
if (!input.noFuzz) {
|
|
1736
|
+
summary.fuzz = await runFuzzTests({ ...shared, headers });
|
|
1737
|
+
}
|
|
1470
1738
|
});
|
|
1471
|
-
|
|
1472
|
-
if (!input.noRateLimit) {
|
|
1473
|
-
const rateLimit = await runRateLimitTests({
|
|
1474
|
-
apiMethodMap: input.apiMethodMap,
|
|
1475
|
-
apiMetaMap: input.apiMetaMap,
|
|
1476
|
-
baseUrl: input.baseUrl,
|
|
1477
|
-
skip: input.skip,
|
|
1478
|
-
headers,
|
|
1479
|
-
inputFor,
|
|
1480
|
-
//? Reset the shared per-IP rate-limit bucket between endpoints so a
|
|
1481
|
-
//? neighbour saturating the window doesn't cause false pass/fail on the
|
|
1482
|
-
//? next endpoint. Requires the server to expose /_test/reset (NODE_ENV in
|
|
1483
|
-
//? { 'development', 'test' } AND TEST_RESET_TOKEN set + matching header).
|
|
1484
|
-
resetBetweenEndpoints: true,
|
|
1485
|
-
//? Thread the reset token (explicit input, else the TEST_RESET_TOKEN env the
|
|
1486
|
-
//? server validates against) — without it `/_test/reset` returns 403 and the
|
|
1487
|
-
//? between-endpoint reset silently never happens.
|
|
1488
|
-
resetToken: input.resetToken ?? process.env.TEST_RESET_TOKEN
|
|
1489
|
-
});
|
|
1490
|
-
summary.rateLimit = cloneSummary(rateLimit, input.filter);
|
|
1491
|
-
}
|
|
1492
|
-
if (!input.noCsrf && headers.Cookie) {
|
|
1493
|
-
const csrf = await runCsrfEnforcementTests({
|
|
1494
|
-
apiMethodMap: input.apiMethodMap,
|
|
1495
|
-
apiMetaMap: input.apiMetaMap,
|
|
1496
|
-
baseUrl: input.baseUrl,
|
|
1497
|
-
authCookie: headers.Cookie,
|
|
1498
|
-
skip: input.skip,
|
|
1499
|
-
inputFor
|
|
1500
|
-
});
|
|
1501
|
-
summary.csrf = cloneSummary(csrf, input.filter);
|
|
1502
|
-
}
|
|
1503
|
-
if (!input.noFuzz) {
|
|
1504
|
-
const fuzz = await runFuzzTests({
|
|
1505
|
-
apiMethodMap: input.apiMethodMap,
|
|
1506
|
-
baseUrl: input.baseUrl,
|
|
1507
|
-
skip: input.skip,
|
|
1508
|
-
headers
|
|
1509
|
-
});
|
|
1510
|
-
summary.fuzz = cloneSummary(fuzz, input.filter);
|
|
1511
|
-
}
|
|
1739
|
+
await sync?.close();
|
|
1512
1740
|
if (resetBookendToken) {
|
|
1513
|
-
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1741
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken, only: ["rateLimits"] });
|
|
1514
1742
|
}
|
|
1743
|
+
if (layerError) throw layerError;
|
|
1515
1744
|
};
|
|
1516
1745
|
var runCustomLayer = async (input, summary) => {
|
|
1517
1746
|
try {
|
|
@@ -1565,9 +1794,18 @@ var runAllTests = async (input) => {
|
|
|
1565
1794
|
"[test-runner] runAllTests: apiMetaMap is empty but apiMethodMap has routes. The auth-enforcement and rate-limit layers will skip every route. Pass the generated apiMetaMap to enable those checks."
|
|
1566
1795
|
);
|
|
1567
1796
|
}
|
|
1797
|
+
if (!input.syncMetaMap) {
|
|
1798
|
+
console.warn(
|
|
1799
|
+
"[test-runner] runAllTests: no syncMetaMap passed \u2014 sync routes are NOT swept. Pass the generated syncMetaMap (+ syncInputSchemas) to cover them."
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1568
1802
|
}
|
|
1569
1803
|
const headers = await buildAuthHeaders(input);
|
|
1570
|
-
const inputFor =
|
|
1804
|
+
const inputFor = createSampleInputFor({
|
|
1805
|
+
apiInputSchemas: input.apiInputSchemas,
|
|
1806
|
+
syncInputSchemas: input.syncInputSchemas,
|
|
1807
|
+
stringPrefix: createTestDataMarker()
|
|
1808
|
+
});
|
|
1571
1809
|
if (!input.noSweep) {
|
|
1572
1810
|
await runSweepLayers(input, summary, headers, inputFor);
|
|
1573
1811
|
}
|
|
@@ -1575,8 +1813,21 @@ var runAllTests = async (input) => {
|
|
|
1575
1813
|
await runCustomLayer(input, summary);
|
|
1576
1814
|
}
|
|
1577
1815
|
computeTotals(summary);
|
|
1816
|
+
writeSummaryFile(summary, input.outputFile);
|
|
1578
1817
|
return summary;
|
|
1579
1818
|
};
|
|
1819
|
+
var writeSummaryFile = (summary, outputFile) => {
|
|
1820
|
+
if (outputFile === false) return;
|
|
1821
|
+
const target = path3.resolve(outputFile ?? process.env.TEST_OUTPUT_FILE ?? "test-results.json");
|
|
1822
|
+
const [writeError] = tryCatchSync2(() => {
|
|
1823
|
+
fs3.writeFileSync(target, JSON.stringify(summary, null, 2), "utf8");
|
|
1824
|
+
});
|
|
1825
|
+
if (writeError) {
|
|
1826
|
+
console.warn(`[test-runner] runAllTests: could not write ${target} \u2014 ${writeError.message}`);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
console.log(`[test-runner] full results written to ${target}`);
|
|
1830
|
+
};
|
|
1580
1831
|
var useColor = (() => {
|
|
1581
1832
|
if (process.env.NO_COLOR) return false;
|
|
1582
1833
|
if (process.env.FORCE_COLOR) return true;
|
|
@@ -1691,13 +1942,13 @@ var resetTestExtensionsForTests = () => {
|
|
|
1691
1942
|
};
|
|
1692
1943
|
|
|
1693
1944
|
// src/runRegisteredLayers.ts
|
|
1694
|
-
import { tryCatch as
|
|
1945
|
+
import { tryCatch as tryCatch9, tryCatchSync as tryCatchSync3 } from "@luckystack/core";
|
|
1695
1946
|
var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
1696
1947
|
var postWebhook = async (summary) => {
|
|
1697
1948
|
const reporter2 = getTestReporter();
|
|
1698
1949
|
const webhookUrl = reporter2?.webhookUrl;
|
|
1699
1950
|
if (!reporter2 || !webhookUrl) return;
|
|
1700
|
-
const [, parsedUrl] =
|
|
1951
|
+
const [, parsedUrl] = tryCatchSync3(() => new URL(webhookUrl));
|
|
1701
1952
|
if (parsedUrl?.protocol === "http:" && !isLoopbackHost(parsedUrl.hostname)) {
|
|
1702
1953
|
console.warn(
|
|
1703
1954
|
`[test-runner] reporter.webhookUrl is plaintext http to a non-loopback host (${parsedUrl.hostname}); the summary (endpoint paths, error codes, metadata) and any bearer token are sent unencrypted.`
|
|
@@ -1709,7 +1960,7 @@ var postWebhook = async (summary) => {
|
|
|
1709
1960
|
const timeoutHandle = setTimeout(() => {
|
|
1710
1961
|
controller.abort();
|
|
1711
1962
|
}, 1e4);
|
|
1712
|
-
await
|
|
1963
|
+
await tryCatch9(() => fetch(webhookUrl, {
|
|
1713
1964
|
method: "POST",
|
|
1714
1965
|
headers,
|
|
1715
1966
|
body: JSON.stringify(summary),
|
|
@@ -1719,15 +1970,25 @@ var postWebhook = async (summary) => {
|
|
|
1719
1970
|
};
|
|
1720
1971
|
var runRegisteredLayers = async (input) => {
|
|
1721
1972
|
const layers2 = listTestLayers();
|
|
1722
|
-
const endpoints =
|
|
1973
|
+
const endpoints = selectEndpoints(input);
|
|
1723
1974
|
const reporter2 = getTestReporter();
|
|
1724
1975
|
const skip = input.skip ?? [];
|
|
1725
1976
|
const results = [];
|
|
1977
|
+
const inputFor = input.inputFor ?? createSampleInputFor({
|
|
1978
|
+
apiInputSchemas: input.apiInputSchemas,
|
|
1979
|
+
syncInputSchemas: input.syncInputSchemas,
|
|
1980
|
+
stringPrefix: createTestDataMarker()
|
|
1981
|
+
});
|
|
1726
1982
|
for (const layer of layers2) {
|
|
1727
1983
|
for (const endpoint of endpoints) {
|
|
1728
1984
|
if (shouldSkip(endpoint, skip)) continue;
|
|
1729
1985
|
const started = Date.now();
|
|
1730
|
-
const [runError, layerResult] = await
|
|
1986
|
+
const [runError, layerResult] = await tryCatch9(async () => layer.run({
|
|
1987
|
+
endpoint: endpoint.fullPath,
|
|
1988
|
+
method: endpoint.kind === "sync" ? void 0 : endpoint.method,
|
|
1989
|
+
authToken: input.authToken,
|
|
1990
|
+
sampleInput: inputFor(endpoint)
|
|
1991
|
+
}));
|
|
1731
1992
|
const durationMs = Date.now() - started;
|
|
1732
1993
|
const result = runError || !layerResult ? {
|
|
1733
1994
|
layer: layer.name,
|
|
@@ -1744,7 +2005,7 @@ var runRegisteredLayers = async (input) => {
|
|
|
1744
2005
|
metadata: layerResult.metadata
|
|
1745
2006
|
};
|
|
1746
2007
|
results.push(result);
|
|
1747
|
-
await
|
|
2008
|
+
await tryCatch9(async () => reporter2?.onResult?.(result));
|
|
1748
2009
|
}
|
|
1749
2010
|
}
|
|
1750
2011
|
const summary = {
|
|
@@ -1754,14 +2015,18 @@ var runRegisteredLayers = async (input) => {
|
|
|
1754
2015
|
failed: results.filter((r) => !r.passed).length,
|
|
1755
2016
|
results
|
|
1756
2017
|
};
|
|
1757
|
-
await
|
|
2018
|
+
await tryCatch9(async () => reporter2?.onSummary?.(summary));
|
|
1758
2019
|
await postWebhook(summary);
|
|
1759
2020
|
return summary;
|
|
1760
2021
|
};
|
|
1761
2022
|
export {
|
|
1762
2023
|
LAYER_KEYS,
|
|
1763
2024
|
TEST_DATA_PREFIX,
|
|
2025
|
+
createHttpProbeTransport,
|
|
2026
|
+
createSampleInputFor,
|
|
2027
|
+
createSocketProbeTransport,
|
|
1764
2028
|
createTestDataMarker,
|
|
2029
|
+
defaultProbeTransport,
|
|
1765
2030
|
discoverCustomTestFiles,
|
|
1766
2031
|
getTestFixture,
|
|
1767
2032
|
getTestReporter,
|
|
@@ -1791,6 +2056,7 @@ export {
|
|
|
1791
2056
|
runRateLimitTests,
|
|
1792
2057
|
runRegisteredLayers,
|
|
1793
2058
|
sampleSchemaInput,
|
|
2059
|
+
sendProbe,
|
|
1794
2060
|
walkEndpoints,
|
|
1795
2061
|
walkSyncEndpoints
|
|
1796
2062
|
};
|