@luckystack/test-runner 0.10.1 → 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 +21 -1
- package/CLAUDE.md +20 -7
- package/README.md +5 -3
- package/dist/index.d.ts +210 -22
- package/dist/index.js +560 -304
- package/dist/index.js.map +1 -1
- package/docs/auth-tests.md +2 -2
- package/docs/contract-tests.md +21 -7
- 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_";
|
|
@@ -940,91 +1183,25 @@ import fs2 from "fs";
|
|
|
940
1183
|
import path2 from "path";
|
|
941
1184
|
import { pathToFileURL } from "url";
|
|
942
1185
|
import {
|
|
943
|
-
buildRoutedGetUrl,
|
|
1186
|
+
buildRoutedGetUrl as buildRoutedGetUrl2,
|
|
944
1187
|
getCsrfConfig,
|
|
945
1188
|
getPrismaClient,
|
|
946
1189
|
getProjectConfig as getProjectConfig3,
|
|
947
1190
|
getSrcDir,
|
|
948
|
-
tryCatch as
|
|
1191
|
+
tryCatch as tryCatch7,
|
|
949
1192
|
tryCatchSync
|
|
950
1193
|
} from "@luckystack/core";
|
|
951
1194
|
|
|
952
1195
|
// src/streamWatcher.ts
|
|
953
|
-
import {
|
|
954
|
-
import {
|
|
955
|
-
buildJoinRoomResponseEventName,
|
|
956
|
-
socketEventNames,
|
|
957
|
-
tryCatch as tryCatch4,
|
|
958
|
-
getProjectConfig as getProjectConfig2
|
|
959
|
-
} from "@luckystack/core";
|
|
1196
|
+
import { socketEventNames as socketEventNames2, tryCatch as tryCatch6 } from "@luckystack/core";
|
|
960
1197
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
961
|
-
var
|
|
962
|
-
var CONNECT_TIMEOUT_MS = 3e3;
|
|
963
|
-
var isJoinRoomResponse = (value) => typeof value === "object" && value !== null;
|
|
1198
|
+
var LABEL2 = "watchStream";
|
|
964
1199
|
var isStreamChunkFrame = (value) => typeof value === "object" && value !== null;
|
|
965
|
-
var responseIndexCounter = 0;
|
|
966
|
-
var nextResponseIndex = () => {
|
|
967
|
-
responseIndexCounter += 1;
|
|
968
|
-
return responseIndexCounter;
|
|
969
|
-
};
|
|
970
|
-
var waitForConnect = (socket) => {
|
|
971
|
-
return new Promise((resolve, reject) => {
|
|
972
|
-
if (socket.connected) {
|
|
973
|
-
resolve();
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
const timer = setTimeout(() => {
|
|
977
|
-
cleanup();
|
|
978
|
-
reject(new Error(`watchStream: socket failed to connect within ${CONNECT_TIMEOUT_MS}ms`));
|
|
979
|
-
}, CONNECT_TIMEOUT_MS);
|
|
980
|
-
const onConnect = () => {
|
|
981
|
-
cleanup();
|
|
982
|
-
resolve();
|
|
983
|
-
};
|
|
984
|
-
const onConnectError = (err) => {
|
|
985
|
-
cleanup();
|
|
986
|
-
reject(new Error(`watchStream: connect_error \u2014 ${err.message}`));
|
|
987
|
-
};
|
|
988
|
-
const cleanup = () => {
|
|
989
|
-
clearTimeout(timer);
|
|
990
|
-
socket.off("connect", onConnect);
|
|
991
|
-
socket.off("connect_error", onConnectError);
|
|
992
|
-
};
|
|
993
|
-
socket.on("connect", onConnect);
|
|
994
|
-
socket.on("connect_error", onConnectError);
|
|
995
|
-
});
|
|
996
|
-
};
|
|
997
|
-
var waitForJoinAck = (socket, responseIndex) => {
|
|
998
|
-
const eventName = buildJoinRoomResponseEventName(responseIndex);
|
|
999
|
-
return new Promise((resolve, reject) => {
|
|
1000
|
-
const timer = setTimeout(() => {
|
|
1001
|
-
socket.off(eventName, onAck);
|
|
1002
|
-
reject(new Error(`watchStream: joinRoom ack timeout after ${JOIN_RESPONSE_TIMEOUT_MS}ms`));
|
|
1003
|
-
}, JOIN_RESPONSE_TIMEOUT_MS);
|
|
1004
|
-
const onAck = (payload) => {
|
|
1005
|
-
clearTimeout(timer);
|
|
1006
|
-
socket.off(eventName, onAck);
|
|
1007
|
-
if (isJoinRoomResponse(payload) && payload.status === "error") {
|
|
1008
|
-
reject(new Error(`watchStream: joinRoom rejected \u2014 ${payload.errorCode ?? "unknown"}`));
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
resolve();
|
|
1012
|
-
};
|
|
1013
|
-
socket.on(eventName, onAck);
|
|
1014
|
-
});
|
|
1015
|
-
};
|
|
1016
1200
|
var openStreamWatcher = async (input) => {
|
|
1017
1201
|
const defaultTimeoutMs = input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1018
1202
|
const chunks = [];
|
|
1019
1203
|
const waiters = [];
|
|
1020
|
-
const
|
|
1021
|
-
const socket = createSocketIoClient(input.baseUrl, {
|
|
1022
|
-
transports: ["websocket"],
|
|
1023
|
-
forceNew: true,
|
|
1024
|
-
reconnection: false,
|
|
1025
|
-
auth: input.token ? { token: input.token } : void 0,
|
|
1026
|
-
extraHeaders: input.token ? { Cookie: `${sessionCookieName}=${input.token}` } : void 0
|
|
1027
|
-
});
|
|
1204
|
+
const socket = await connectTestSocket({ baseUrl: input.baseUrl, token: input.token, label: LABEL2 });
|
|
1028
1205
|
const handleChunk = (raw) => {
|
|
1029
1206
|
if (!isStreamChunkFrame(raw)) return;
|
|
1030
1207
|
if (raw.status !== "stream") return;
|
|
@@ -1040,21 +1217,10 @@ var openStreamWatcher = async (input) => {
|
|
|
1040
1217
|
}
|
|
1041
1218
|
}
|
|
1042
1219
|
};
|
|
1043
|
-
socket.on(
|
|
1044
|
-
const [
|
|
1045
|
-
if (connectError) {
|
|
1046
|
-
socket.off(socketEventNames.sync, handleChunk);
|
|
1047
|
-
socket.disconnect();
|
|
1048
|
-
throw connectError;
|
|
1049
|
-
}
|
|
1050
|
-
const responseIndex = nextResponseIndex();
|
|
1051
|
-
socket.emit(socketEventNames.joinRoom, {
|
|
1052
|
-
group: input.roomCode,
|
|
1053
|
-
responseIndex
|
|
1054
|
-
});
|
|
1055
|
-
const [joinError] = await tryCatch4(() => waitForJoinAck(socket, responseIndex));
|
|
1220
|
+
socket.on(socketEventNames2.sync, handleChunk);
|
|
1221
|
+
const [joinError] = await tryCatch6(() => joinTestRoom(socket, input.roomCode, { label: LABEL2 }));
|
|
1056
1222
|
if (joinError) {
|
|
1057
|
-
socket.off(
|
|
1223
|
+
socket.off(socketEventNames2.sync, handleChunk);
|
|
1058
1224
|
socket.disconnect();
|
|
1059
1225
|
throw joinError;
|
|
1060
1226
|
}
|
|
@@ -1062,7 +1228,7 @@ var openStreamWatcher = async (input) => {
|
|
|
1062
1228
|
const close = () => {
|
|
1063
1229
|
if (closed) return Promise.resolve();
|
|
1064
1230
|
closed = true;
|
|
1065
|
-
socket.off(
|
|
1231
|
+
socket.off(socketEventNames2.sync, handleChunk);
|
|
1066
1232
|
for (const waiter of waiters) waiter.resolve();
|
|
1067
1233
|
waiters.length = 0;
|
|
1068
1234
|
socket.disconnect();
|
|
@@ -1166,7 +1332,7 @@ var buildExpect = () => ({
|
|
|
1166
1332
|
if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);
|
|
1167
1333
|
},
|
|
1168
1334
|
throws: async (fn, message) => {
|
|
1169
|
-
const [err] = await
|
|
1335
|
+
const [err] = await tryCatch7(fn);
|
|
1170
1336
|
if (!err) throw new AssertionError(message ?? "expected fn to throw, but it did not");
|
|
1171
1337
|
return err;
|
|
1172
1338
|
},
|
|
@@ -1218,7 +1384,7 @@ var buildSessionHelpers = (state) => {
|
|
|
1218
1384
|
};
|
|
1219
1385
|
};
|
|
1220
1386
|
var parseResponse = async (response, state) => {
|
|
1221
|
-
const [textError, text] = await
|
|
1387
|
+
const [textError, text] = await tryCatch7(() => response.text());
|
|
1222
1388
|
if (textError || text === null) {
|
|
1223
1389
|
state.lastResponse = null;
|
|
1224
1390
|
throw new Error(`failed to read response body (HTTP ${response.status}): ${textError?.message ?? "unknown"}`);
|
|
@@ -1234,7 +1400,7 @@ var parseResponse = async (response, state) => {
|
|
|
1234
1400
|
};
|
|
1235
1401
|
var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
1236
1402
|
return async (input) => {
|
|
1237
|
-
const
|
|
1403
|
+
const path4 = `${baseUrl.replace(/\/$/, "")}/api/${routePath}`;
|
|
1238
1404
|
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
1239
1405
|
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
1240
1406
|
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
@@ -1244,9 +1410,9 @@ var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
|
1244
1410
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1245
1411
|
const init = { method, headers, signal: controller.signal };
|
|
1246
1412
|
const bodyless = method === "GET" || method === "HEAD";
|
|
1247
|
-
const url = bodyless ?
|
|
1413
|
+
const url = bodyless ? buildRoutedGetUrl2(path4, input ?? {}) : path4;
|
|
1248
1414
|
if (!bodyless) init.body = JSON.stringify(input ?? {});
|
|
1249
|
-
const [fetchError, response] = await
|
|
1415
|
+
const [fetchError, response] = await tryCatch7(() => fetch(url, init));
|
|
1250
1416
|
clearTimeout(timeoutHandle);
|
|
1251
1417
|
if (fetchError || !response) {
|
|
1252
1418
|
state.lastResponse = null;
|
|
@@ -1255,18 +1421,57 @@ var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
|
1255
1421
|
return await parseResponse(response, state);
|
|
1256
1422
|
};
|
|
1257
1423
|
};
|
|
1258
|
-
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) => {
|
|
1259
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
|
+
}
|
|
1260
1465
|
const url = `${baseUrl.replace(/\/$/, "")}/sync/${routePath}`;
|
|
1261
1466
|
const headers = { "Content-Type": "application/json", Origin: new URL(baseUrl).origin };
|
|
1262
1467
|
if (state.token) headers.Cookie = `${cookieName}=${state.token}`;
|
|
1263
1468
|
if (state.csrfToken) headers[getCsrfConfig().headerName] = state.csrfToken;
|
|
1264
|
-
const body = { data: input ?? {}, receiver: opts?.receiver };
|
|
1469
|
+
const body = { data: input ?? {}, receiver: opts?.receiver, ignoreSelf: opts?.ignoreSelf };
|
|
1265
1470
|
const controller = new AbortController();
|
|
1266
1471
|
const timeoutHandle = setTimeout(() => {
|
|
1267
1472
|
controller.abort();
|
|
1268
1473
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1269
|
-
const [fetchError, response] = await
|
|
1474
|
+
const [fetchError, response] = await tryCatch7(() => fetch(url, {
|
|
1270
1475
|
method: "POST",
|
|
1271
1476
|
headers,
|
|
1272
1477
|
body: JSON.stringify(body),
|
|
@@ -1297,18 +1502,20 @@ var buildContext = (discovery, input) => {
|
|
|
1297
1502
|
watchers.push(watcher);
|
|
1298
1503
|
return watcher;
|
|
1299
1504
|
};
|
|
1505
|
+
const syncSocket = createCaseSyncSocket(input.baseUrl, cookieName);
|
|
1300
1506
|
const closeAllWatchers = async () => {
|
|
1507
|
+
syncSocket.close();
|
|
1301
1508
|
while (watchers.length > 0) {
|
|
1302
1509
|
const watcher = watchers.pop();
|
|
1303
1510
|
if (!watcher) continue;
|
|
1304
|
-
const [closeError] = await
|
|
1511
|
+
const [closeError] = await tryCatch7(() => watcher.close());
|
|
1305
1512
|
if (closeError) {
|
|
1306
1513
|
}
|
|
1307
1514
|
}
|
|
1308
1515
|
};
|
|
1309
1516
|
const ctx = {
|
|
1310
1517
|
callApi: buildCallApi(input.baseUrl, routePath, state, cookieName, apiMethod),
|
|
1311
|
-
callSync: buildCallSync(input.baseUrl, routePath, state, cookieName),
|
|
1518
|
+
callSync: buildCallSync(input.baseUrl, routePath, state, cookieName, syncSocket),
|
|
1312
1519
|
watchStream,
|
|
1313
1520
|
session: buildSessionHelpers(state),
|
|
1314
1521
|
//? Lazy: only resolves when a case actually touches `ctx.prisma`, so an
|
|
@@ -1343,7 +1550,7 @@ var runCustomTestsAfterEnvironmentPrepared = async (input) => {
|
|
|
1343
1550
|
input.onResult?.(r);
|
|
1344
1551
|
continue;
|
|
1345
1552
|
}
|
|
1346
|
-
const [importError, mod] = await
|
|
1553
|
+
const [importError, mod] = await tryCatch7(
|
|
1347
1554
|
() => import(pathToFileURL(resolvedPath).href)
|
|
1348
1555
|
);
|
|
1349
1556
|
if (importError || !mod) {
|
|
@@ -1368,11 +1575,11 @@ var runCustomTestsAfterEnvironmentPrepared = async (input) => {
|
|
|
1368
1575
|
const expectedToFail = typeof c.expectedToFail === "string" ? c.expectedToFail : void 0;
|
|
1369
1576
|
const built = buildContext(discovery, input);
|
|
1370
1577
|
const started = Date.now();
|
|
1371
|
-
const [runError] = await
|
|
1372
|
-
await
|
|
1578
|
+
const [runError] = await tryCatch7(async () => c.run(built.ctx));
|
|
1579
|
+
await tryCatch7(() => built.closeAllWatchers());
|
|
1373
1580
|
const residualToken = built.state.token;
|
|
1374
1581
|
if (residualToken) {
|
|
1375
|
-
await
|
|
1582
|
+
await tryCatch7(async () => {
|
|
1376
1583
|
const { deleteSession } = await import("@luckystack/login");
|
|
1377
1584
|
await deleteSession(residualToken);
|
|
1378
1585
|
});
|
|
@@ -1413,11 +1620,10 @@ var runCustomTests = async (input) => {
|
|
|
1413
1620
|
};
|
|
1414
1621
|
|
|
1415
1622
|
// src/runAllTests.ts
|
|
1416
|
-
import
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
};
|
|
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";
|
|
1421
1627
|
var resolveResetBookendToken = (input) => {
|
|
1422
1628
|
if (input.resetBookends === false) return null;
|
|
1423
1629
|
const token = input.resetToken ?? process.env.TEST_RESET_TOKEN;
|
|
@@ -1429,15 +1635,6 @@ var resolveResetBookendToken = (input) => {
|
|
|
1429
1635
|
}
|
|
1430
1636
|
return token;
|
|
1431
1637
|
};
|
|
1432
|
-
var matchesFilter = (endpoint, filter) => {
|
|
1433
|
-
if (!filter) return true;
|
|
1434
|
-
return `${endpoint.page}/${endpoint.name}/${endpoint.version}`.includes(filter);
|
|
1435
|
-
};
|
|
1436
|
-
var filterResults = (results, filter) => filter ? results.filter((r) => matchesFilter(r.endpoint, filter)) : results;
|
|
1437
|
-
var cloneSummary = (summary, filter) => {
|
|
1438
|
-
if (!filter) return summary;
|
|
1439
|
-
return calculateSummary(filterResults(summary.results, filter));
|
|
1440
|
-
};
|
|
1441
1638
|
var buildAuthHeaders = async (input) => {
|
|
1442
1639
|
const headers = {};
|
|
1443
1640
|
if (!input.authToken) return headers;
|
|
@@ -1458,70 +1655,92 @@ var buildAuthHeaders = async (input) => {
|
|
|
1458
1655
|
}
|
|
1459
1656
|
return headers;
|
|
1460
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
|
+
};
|
|
1461
1690
|
var runSweepLayers = async (input, summary, headers, inputFor) => {
|
|
1691
|
+
const resetToken = input.resetToken ?? process.env.TEST_RESET_TOKEN;
|
|
1462
1692
|
const resetBookendToken = resolveResetBookendToken(input);
|
|
1463
1693
|
if (resetBookendToken) {
|
|
1464
|
-
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1694
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken, only: ["rateLimits"] });
|
|
1465
1695
|
}
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
skip: input.skip,
|
|
1470
|
-
headers,
|
|
1471
|
-
inputFor
|
|
1472
|
-
});
|
|
1473
|
-
summary.contract = cloneSummary(contract, input.filter);
|
|
1474
|
-
const auth = await runAuthEnforcementTests({
|
|
1696
|
+
const hasSyncRoutes = Object.keys(input.syncMetaMap ?? {}).length > 0;
|
|
1697
|
+
const sync = hasSyncRoutes ? await prepareSyncSweep(input) : null;
|
|
1698
|
+
const shared = {
|
|
1475
1699
|
apiMethodMap: input.apiMethodMap,
|
|
1476
|
-
|
|
1700
|
+
syncMetaMap: input.syncMetaMap,
|
|
1477
1701
|
baseUrl: input.baseUrl,
|
|
1478
1702
|
skip: input.skip,
|
|
1479
|
-
|
|
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
|
+
}
|
|
1480
1738
|
});
|
|
1481
|
-
|
|
1482
|
-
if (!input.noRateLimit) {
|
|
1483
|
-
const rateLimit = await runRateLimitTests({
|
|
1484
|
-
apiMethodMap: input.apiMethodMap,
|
|
1485
|
-
apiMetaMap: input.apiMetaMap,
|
|
1486
|
-
baseUrl: input.baseUrl,
|
|
1487
|
-
skip: input.skip,
|
|
1488
|
-
headers,
|
|
1489
|
-
inputFor,
|
|
1490
|
-
//? Reset the shared per-IP rate-limit bucket between endpoints so a
|
|
1491
|
-
//? neighbour saturating the window doesn't cause false pass/fail on the
|
|
1492
|
-
//? next endpoint. Requires the server to expose /_test/reset (NODE_ENV in
|
|
1493
|
-
//? { 'development', 'test' } AND TEST_RESET_TOKEN set + matching header).
|
|
1494
|
-
resetBetweenEndpoints: true,
|
|
1495
|
-
//? Thread the reset token (explicit input, else the TEST_RESET_TOKEN env the
|
|
1496
|
-
//? server validates against) — without it `/_test/reset` returns 403 and the
|
|
1497
|
-
//? between-endpoint reset silently never happens.
|
|
1498
|
-
resetToken: input.resetToken ?? process.env.TEST_RESET_TOKEN
|
|
1499
|
-
});
|
|
1500
|
-
summary.rateLimit = cloneSummary(rateLimit, input.filter);
|
|
1501
|
-
}
|
|
1502
|
-
if (!input.noCsrf && headers.Cookie) {
|
|
1503
|
-
const csrf = await runCsrfEnforcementTests({
|
|
1504
|
-
apiMethodMap: input.apiMethodMap,
|
|
1505
|
-
apiMetaMap: input.apiMetaMap,
|
|
1506
|
-
baseUrl: input.baseUrl,
|
|
1507
|
-
authCookie: headers.Cookie,
|
|
1508
|
-
skip: input.skip,
|
|
1509
|
-
inputFor
|
|
1510
|
-
});
|
|
1511
|
-
summary.csrf = cloneSummary(csrf, input.filter);
|
|
1512
|
-
}
|
|
1513
|
-
if (!input.noFuzz) {
|
|
1514
|
-
const fuzz = await runFuzzTests({
|
|
1515
|
-
apiMethodMap: input.apiMethodMap,
|
|
1516
|
-
baseUrl: input.baseUrl,
|
|
1517
|
-
skip: input.skip,
|
|
1518
|
-
headers
|
|
1519
|
-
});
|
|
1520
|
-
summary.fuzz = cloneSummary(fuzz, input.filter);
|
|
1521
|
-
}
|
|
1739
|
+
await sync?.close();
|
|
1522
1740
|
if (resetBookendToken) {
|
|
1523
|
-
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1741
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken, only: ["rateLimits"] });
|
|
1524
1742
|
}
|
|
1743
|
+
if (layerError) throw layerError;
|
|
1525
1744
|
};
|
|
1526
1745
|
var runCustomLayer = async (input, summary) => {
|
|
1527
1746
|
try {
|
|
@@ -1575,9 +1794,18 @@ var runAllTests = async (input) => {
|
|
|
1575
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."
|
|
1576
1795
|
);
|
|
1577
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
|
+
}
|
|
1578
1802
|
}
|
|
1579
1803
|
const headers = await buildAuthHeaders(input);
|
|
1580
|
-
const inputFor =
|
|
1804
|
+
const inputFor = createSampleInputFor({
|
|
1805
|
+
apiInputSchemas: input.apiInputSchemas,
|
|
1806
|
+
syncInputSchemas: input.syncInputSchemas,
|
|
1807
|
+
stringPrefix: createTestDataMarker()
|
|
1808
|
+
});
|
|
1581
1809
|
if (!input.noSweep) {
|
|
1582
1810
|
await runSweepLayers(input, summary, headers, inputFor);
|
|
1583
1811
|
}
|
|
@@ -1585,8 +1813,21 @@ var runAllTests = async (input) => {
|
|
|
1585
1813
|
await runCustomLayer(input, summary);
|
|
1586
1814
|
}
|
|
1587
1815
|
computeTotals(summary);
|
|
1816
|
+
writeSummaryFile(summary, input.outputFile);
|
|
1588
1817
|
return summary;
|
|
1589
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
|
+
};
|
|
1590
1831
|
var useColor = (() => {
|
|
1591
1832
|
if (process.env.NO_COLOR) return false;
|
|
1592
1833
|
if (process.env.FORCE_COLOR) return true;
|
|
@@ -1701,13 +1942,13 @@ var resetTestExtensionsForTests = () => {
|
|
|
1701
1942
|
};
|
|
1702
1943
|
|
|
1703
1944
|
// src/runRegisteredLayers.ts
|
|
1704
|
-
import { tryCatch as
|
|
1945
|
+
import { tryCatch as tryCatch9, tryCatchSync as tryCatchSync3 } from "@luckystack/core";
|
|
1705
1946
|
var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
1706
1947
|
var postWebhook = async (summary) => {
|
|
1707
1948
|
const reporter2 = getTestReporter();
|
|
1708
1949
|
const webhookUrl = reporter2?.webhookUrl;
|
|
1709
1950
|
if (!reporter2 || !webhookUrl) return;
|
|
1710
|
-
const [, parsedUrl] =
|
|
1951
|
+
const [, parsedUrl] = tryCatchSync3(() => new URL(webhookUrl));
|
|
1711
1952
|
if (parsedUrl?.protocol === "http:" && !isLoopbackHost(parsedUrl.hostname)) {
|
|
1712
1953
|
console.warn(
|
|
1713
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.`
|
|
@@ -1719,7 +1960,7 @@ var postWebhook = async (summary) => {
|
|
|
1719
1960
|
const timeoutHandle = setTimeout(() => {
|
|
1720
1961
|
controller.abort();
|
|
1721
1962
|
}, 1e4);
|
|
1722
|
-
await
|
|
1963
|
+
await tryCatch9(() => fetch(webhookUrl, {
|
|
1723
1964
|
method: "POST",
|
|
1724
1965
|
headers,
|
|
1725
1966
|
body: JSON.stringify(summary),
|
|
@@ -1729,15 +1970,25 @@ var postWebhook = async (summary) => {
|
|
|
1729
1970
|
};
|
|
1730
1971
|
var runRegisteredLayers = async (input) => {
|
|
1731
1972
|
const layers2 = listTestLayers();
|
|
1732
|
-
const endpoints =
|
|
1973
|
+
const endpoints = selectEndpoints(input);
|
|
1733
1974
|
const reporter2 = getTestReporter();
|
|
1734
1975
|
const skip = input.skip ?? [];
|
|
1735
1976
|
const results = [];
|
|
1977
|
+
const inputFor = input.inputFor ?? createSampleInputFor({
|
|
1978
|
+
apiInputSchemas: input.apiInputSchemas,
|
|
1979
|
+
syncInputSchemas: input.syncInputSchemas,
|
|
1980
|
+
stringPrefix: createTestDataMarker()
|
|
1981
|
+
});
|
|
1736
1982
|
for (const layer of layers2) {
|
|
1737
1983
|
for (const endpoint of endpoints) {
|
|
1738
1984
|
if (shouldSkip(endpoint, skip)) continue;
|
|
1739
1985
|
const started = Date.now();
|
|
1740
|
-
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
|
+
}));
|
|
1741
1992
|
const durationMs = Date.now() - started;
|
|
1742
1993
|
const result = runError || !layerResult ? {
|
|
1743
1994
|
layer: layer.name,
|
|
@@ -1754,7 +2005,7 @@ var runRegisteredLayers = async (input) => {
|
|
|
1754
2005
|
metadata: layerResult.metadata
|
|
1755
2006
|
};
|
|
1756
2007
|
results.push(result);
|
|
1757
|
-
await
|
|
2008
|
+
await tryCatch9(async () => reporter2?.onResult?.(result));
|
|
1758
2009
|
}
|
|
1759
2010
|
}
|
|
1760
2011
|
const summary = {
|
|
@@ -1764,14 +2015,18 @@ var runRegisteredLayers = async (input) => {
|
|
|
1764
2015
|
failed: results.filter((r) => !r.passed).length,
|
|
1765
2016
|
results
|
|
1766
2017
|
};
|
|
1767
|
-
await
|
|
2018
|
+
await tryCatch9(async () => reporter2?.onSummary?.(summary));
|
|
1768
2019
|
await postWebhook(summary);
|
|
1769
2020
|
return summary;
|
|
1770
2021
|
};
|
|
1771
2022
|
export {
|
|
1772
2023
|
LAYER_KEYS,
|
|
1773
2024
|
TEST_DATA_PREFIX,
|
|
2025
|
+
createHttpProbeTransport,
|
|
2026
|
+
createSampleInputFor,
|
|
2027
|
+
createSocketProbeTransport,
|
|
1774
2028
|
createTestDataMarker,
|
|
2029
|
+
defaultProbeTransport,
|
|
1775
2030
|
discoverCustomTestFiles,
|
|
1776
2031
|
getTestFixture,
|
|
1777
2032
|
getTestReporter,
|
|
@@ -1801,6 +2056,7 @@ export {
|
|
|
1801
2056
|
runRateLimitTests,
|
|
1802
2057
|
runRegisteredLayers,
|
|
1803
2058
|
sampleSchemaInput,
|
|
2059
|
+
sendProbe,
|
|
1804
2060
|
walkEndpoints,
|
|
1805
2061
|
walkSyncEndpoints
|
|
1806
2062
|
};
|