@luckystack/test-runner 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -2
- package/dist/index.d.ts +51 -6
- package/dist/index.js +134 -34
- package/dist/index.js.map +1 -1
- package/docs/auth-tests.md +12 -12
- package/docs/extension-hooks.md +32 -7
- package/docs/fuzz-tests.md +5 -6
- package/docs/rate-limit-tests.md +3 -2
- package/package.json +3 -3
package/CLAUDE.md
CHANGED
|
@@ -40,7 +40,8 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
|
|
|
40
40
|
| `runFuzzCheck({ endpoint, baseUrl, headers? })` | Single-endpoint crash-resistance fuzz | → docs/fuzz-tests.md |
|
|
41
41
|
| `runFuzzTests({ apiMethodMap, baseUrl, skip?, headers?, onResult? })` | Sweep fuzz layer (nightly CI) | → docs/fuzz-tests.md |
|
|
42
42
|
| `resetServerState({ baseUrl, token? })` | POST naar `/_test/reset` om DB+Redis schoon te maken | → docs/rate-limit-tests.md |
|
|
43
|
-
| `sampleSchemaInput(schema)` | Genereert Zod-valid sample payload uit een schema | → docs/contract-tests.md |
|
|
43
|
+
| `sampleSchemaInput(schema, options?)` | Genereert Zod-valid sample payload uit een schema. `options.stringPrefix` tagt UNCONSTRAINED strings (finding #98) — format/checked strings blijven ongeprefixt zodat ze valid blijven. | → docs/contract-tests.md |
|
|
44
|
+
| `createTestDataMarker()` / `TEST_DATA_PREFIX` | Run-unique `lstest_<uuid>_` marker (+ vaste `lstest_` prefix-constant) waarmee `runAllTests` gegenereerde test-strings identificeerbaar maakt. | → docs/contract-tests.md |
|
|
44
45
|
| `logContractResult(result)` | Pretty-print één `ContractCheckResult` | → docs/extension-hooks.md |
|
|
45
46
|
| `logContractSummary(summary)` | Pretty-print aggregate `RunContractSummary` | → docs/extension-hooks.md |
|
|
46
47
|
| `registerTestLayer(layer)` | Plug-in custom test-layer (CORS, business rules, multi-tenant) | → docs/extension-hooks.md |
|
|
@@ -79,13 +80,14 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
|
|
|
79
80
|
- `TEST_RESET_TOKEN` (env, optional, server-side) — wanneer gezet moet `resetServerState({ token })` deze meesturen als `X-Test-Reset-Token`. Verplicht voor staging/preview deploys die het endpoint over het netwerk exposen.
|
|
80
81
|
- `RunRateLimitTestsInput.maxRateLimitToTest` (default `50`) — endpoints met hogere `rateLimit` worden geskipt om duizenden requests in CI te vermijden.
|
|
81
82
|
- `RunRateLimitTestsInput.resetBetweenEndpoints` (default `false`) — hit `/_test/reset` voor elke endpoint zodat het shared IP-bucket schoon is.
|
|
83
|
+
- `RunAllTestsInput.resetBookends` (default `true` wanneer een reset-token beschikbaar is) — POST `/_test/reset` één keer vóór en één keer ná de auto-sweep (finding #98: clean slate + cleanup zodat geen echte rows worden geëdit en geen `lstest_*` testdata achterblijft). Zonder token (`resetToken`/`TEST_RESET_TOKEN`) worden de bookends met een logregel geskipt, niet gefaald.
|
|
82
84
|
- `registerTestLayer({ name, run })` — custom check per endpoint (CORS, business rules, custom auth-schemes, multi-tenant isolation).
|
|
83
85
|
- `registerTestFixture(typeKey, { valid, invalid })` — realistic payloads die de fuzz layer prefereert boven schema-random.
|
|
84
86
|
- `registerTestReporter({ onResult?, onSummary?, webhookUrl?, webhookAuth? })` — wire per-result/per-summary callbacks of POST de summary naar een webhook.
|
|
85
87
|
|
|
86
88
|
## Peer dependencies
|
|
87
89
|
|
|
88
|
-
- **Required**: `zod@^4.0.0` (peer), `socket.io-client@^4.8.0` (peer — `streamWatcher.ts` opent een tweede socket voor `ctx.watchStream`), `@luckystack/core@^0.1.0` (dependency —
|
|
90
|
+
- **Required**: `zod@^4.0.0` (peer), `socket.io-client@^4.8.0` (peer — `streamWatcher.ts` opent een tweede socket voor `ctx.watchStream`), `@luckystack/core@^0.1.0` (dependency — levert de helpers die de runner consumeert: `tryCatch`/`tryCatchSync`, `getProjectConfig`/`getCsrfConfig`, `getSrcDir` (default src-dir voor custom-test discovery in `customTests.ts`), `getPrismaClient` (Layer-5 `ctx.prisma`) en `clearAllRateLimits`. De `apiMethodMap` zelf wordt door de consumer als argument doorgegeven, niet uit een pad gelezen).
|
|
89
91
|
- **Required server-side**: `@luckystack/server` met `/_test/reset` endpoint gemount (default ingebouwd, gated op `NODE_ENV !== 'production'`).
|
|
90
92
|
- **Required generated artefact**: `apiMethodMap.generated.ts` (en `apiMetaMap.generated.ts` voor de auth/rate-limit layers), geproduceerd door `@luckystack/devkit`.
|
|
91
93
|
- **Optional**: geen — fixtures, reporters en custom layers zijn pure-JS opt-in.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface ApiMetaEntry {
|
|
|
9
9
|
auth: {
|
|
10
10
|
login: boolean;
|
|
11
11
|
additional?: Record<string, unknown>[];
|
|
12
|
+
hasAdditional?: boolean;
|
|
12
13
|
};
|
|
13
14
|
rateLimit?: number | false;
|
|
14
15
|
}
|
|
@@ -122,9 +123,10 @@ interface RunRateLimitTestsInput {
|
|
|
122
123
|
maxRateLimitToTest?: number;
|
|
123
124
|
/**
|
|
124
125
|
* When true, hit `/_test/reset` before each endpoint so limiter state is
|
|
125
|
-
* clean. Requires the server to allow the endpoint
|
|
126
|
-
*
|
|
127
|
-
*
|
|
126
|
+
* clean. Requires the server to allow the endpoint: `NODE_ENV` in
|
|
127
|
+
* { 'development', 'test' } AND `TEST_RESET_TOKEN` set on the server with a
|
|
128
|
+
* matching `resetToken` here (an unset token is 403, never open). Without
|
|
129
|
+
* resetting, the shared IP bucket leaks state between endpoints.
|
|
128
130
|
*/
|
|
129
131
|
resetBetweenEndpoints?: boolean;
|
|
130
132
|
/**
|
|
@@ -197,7 +199,20 @@ interface ResetServerStateInput {
|
|
|
197
199
|
}
|
|
198
200
|
declare const resetServerState: ({ baseUrl, token, requestTimeoutMs }: ResetServerStateInput) => Promise<boolean>;
|
|
199
201
|
|
|
200
|
-
|
|
202
|
+
interface SampleSchemaOptions {
|
|
203
|
+
/**
|
|
204
|
+
* Marker prefix applied to generated UNCONSTRAINED string values so any rows
|
|
205
|
+
* a sweep creates are identifiable + collision-free (finding #98). Strings
|
|
206
|
+
* that carry a format (email/url/uuid/datetime/…) or a check (min/max/
|
|
207
|
+
* length/regex) are left as-is, since prefixing could break the route's own
|
|
208
|
+
* validation and defeat the contract layer's "valid input" guarantee.
|
|
209
|
+
*/
|
|
210
|
+
stringPrefix?: string;
|
|
211
|
+
}
|
|
212
|
+
declare const sampleSchemaInput: (schema: z.ZodType, options?: SampleSchemaOptions) => unknown;
|
|
213
|
+
|
|
214
|
+
declare const TEST_DATA_PREFIX = "lstest_";
|
|
215
|
+
declare const createTestDataMarker: () => string;
|
|
201
216
|
|
|
202
217
|
declare const logContractResult: (result: ContractCheckResult) => void;
|
|
203
218
|
declare const logContractSummary: (summary: RunContractSummary) => void;
|
|
@@ -346,7 +361,7 @@ interface RunAllTestsInput {
|
|
|
346
361
|
apiMetaMap: ApiMetaMap;
|
|
347
362
|
apiInputSchemas: ApiInputSchemas;
|
|
348
363
|
baseUrl: string;
|
|
349
|
-
/** Cookie name for the session token. Defaults to `
|
|
364
|
+
/** Cookie name for the session token. Defaults to the project's configured session cookie name (`projectConfig.http.sessionCookieName`). */
|
|
350
365
|
sessionCookieName?: string;
|
|
351
366
|
/** Auth token applied as a `Cookie` header to sweep-layer requests that need a session. */
|
|
352
367
|
authToken?: string;
|
|
@@ -365,6 +380,24 @@ interface RunAllTestsInput {
|
|
|
365
380
|
*/
|
|
366
381
|
noCsrf?: boolean;
|
|
367
382
|
noCustom?: boolean;
|
|
383
|
+
/**
|
|
384
|
+
* Token sent as `X-Test-Reset-Token` when the rate-limit layer resets the
|
|
385
|
+
* shared bucket between endpoints. The server's `/_test/reset` requires
|
|
386
|
+
* `TEST_RESET_TOKEN` to be set and matching — an unset/wrong token is 403, so
|
|
387
|
+
* `resetBetweenEndpoints` would silently no-op. Defaults to
|
|
388
|
+
* `process.env.TEST_RESET_TOKEN` so a CI run with the env set works with no
|
|
389
|
+
* extra wiring; pass explicitly to override.
|
|
390
|
+
*/
|
|
391
|
+
resetToken?: string;
|
|
392
|
+
/**
|
|
393
|
+
* Contract-test mutation safety (finding #98). When a reset token is
|
|
394
|
+
* available, the sweep POSTs `/_test/reset` ONCE before the first layer
|
|
395
|
+
* (clean slate — no real row the generated payloads could clobber) and ONCE
|
|
396
|
+
* after the last (drop whatever the sweep created, so no test data persists).
|
|
397
|
+
* Defaults to ON; set `false` to opt out. With no token the bookends are
|
|
398
|
+
* silently skipped with a logged note (they can't run — `/_test/reset` 403s).
|
|
399
|
+
*/
|
|
400
|
+
resetBookends?: boolean;
|
|
368
401
|
}
|
|
369
402
|
interface RunAllTestsSummary {
|
|
370
403
|
contract?: RunContractSummary;
|
|
@@ -456,6 +489,18 @@ declare const registerTestReporter: (newReporter: TestReporter | null) => void;
|
|
|
456
489
|
declare const getTestReporter: () => TestReporter | null;
|
|
457
490
|
declare const resetTestExtensionsForTests: () => void;
|
|
458
491
|
|
|
492
|
+
interface RunRegisteredLayersInput {
|
|
493
|
+
apiMethodMap: ApiMethodMap$1;
|
|
494
|
+
/** Session/bearer token forwarded to each layer's `run({ authToken })`. */
|
|
495
|
+
authToken?: string;
|
|
496
|
+
/**
|
|
497
|
+
* Endpoints to skip — matched against `<page>/<name>` (version-agnostic) and
|
|
498
|
+
* `<page>/<name>/<version>`, same convention as the built-in sweeps.
|
|
499
|
+
*/
|
|
500
|
+
skip?: string[];
|
|
501
|
+
}
|
|
502
|
+
declare const runRegisteredLayers: (input: RunRegisteredLayersInput) => Promise<TestSummary>;
|
|
503
|
+
|
|
459
504
|
declare const LAYER_KEYS: {
|
|
460
505
|
readonly contract: "contract";
|
|
461
506
|
readonly auth: "auth-enforcement";
|
|
@@ -465,4 +510,4 @@ declare const LAYER_KEYS: {
|
|
|
465
510
|
readonly custom: "custom";
|
|
466
511
|
};
|
|
467
512
|
|
|
468
|
-
export { type ApiMetaEntry, type ApiMetaMap, type ApiMethodMap$1 as ApiMethodMap, type AuthEnforcementCheckInput, type ContractCheckInput, type ContractCheckResult, type CsrfEnforcementCheckInput, type CustomTestCase, type CustomTestResult, type EndpointDescriptor, type FuzzCheckInput, type HttpMethod, LAYER_KEYS, type OpenStreamWatcherInput, type RateLimitCheckInput, type ResetServerStateInput, type RunAllTestsInput, type RunAllTestsSummary, type RunAuthEnforcementTestsInput, type RunContractSummary, type RunContractTestsInput, type RunCsrfEnforcementTestsInput, type RunCustomTestsInput, type RunCustomTestsSummary, type RunFuzzTestsInput, type RunRateLimitTestsInput, type StreamChunkFrame, type StreamWatcher, type SyncMethodMap, type TestContext, type TestExpect, type TestFixture, type TestLayer, type TestLayerInput, type TestLayerResult, type TestReporter, type TestResult, type TestSummary, discoverCustomTestFiles, getTestFixture, getTestReporter, listTestLayers, logContractResult, logContractSummary, logRunAllSummary, openStreamWatcher, registerTestFixture, registerTestLayer, registerTestReporter, resetServerState, resetTestExtensionsForTests, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCsrfEnforcementCheck, runCsrfEnforcementTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, sampleSchemaInput, walkEndpoints, walkSyncEndpoints };
|
|
513
|
+
export { type ApiMetaEntry, type ApiMetaMap, type ApiMethodMap$1 as ApiMethodMap, type AuthEnforcementCheckInput, type ContractCheckInput, type ContractCheckResult, type CsrfEnforcementCheckInput, type CustomTestCase, type CustomTestResult, type EndpointDescriptor, type FuzzCheckInput, type HttpMethod, LAYER_KEYS, type OpenStreamWatcherInput, type RateLimitCheckInput, type ResetServerStateInput, type RunAllTestsInput, type RunAllTestsSummary, type RunAuthEnforcementTestsInput, type RunContractSummary, type RunContractTestsInput, type RunCsrfEnforcementTestsInput, type RunCustomTestsInput, type RunCustomTestsSummary, type RunFuzzTestsInput, type RunRateLimitTestsInput, type RunRegisteredLayersInput, type SampleSchemaOptions, type StreamChunkFrame, type StreamWatcher, type SyncMethodMap, TEST_DATA_PREFIX, type TestContext, type TestExpect, type TestFixture, type TestLayer, type TestLayerInput, type TestLayerResult, type TestReporter, type TestResult, type TestSummary, createTestDataMarker, discoverCustomTestFiles, getTestFixture, getTestReporter, listTestLayers, logContractResult, logContractSummary, logRunAllSummary, openStreamWatcher, registerTestFixture, registerTestLayer, registerTestReporter, resetServerState, resetTestExtensionsForTests, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCsrfEnforcementCheck, runCsrfEnforcementTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, runRegisteredLayers, sampleSchemaInput, walkEndpoints, walkSyncEndpoints };
|
package/dist/index.js
CHANGED
|
@@ -177,6 +177,10 @@ var requiresLogin = (apiMetaMap, endpoint) => {
|
|
|
177
177
|
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
178
178
|
return meta?.auth?.login ?? false;
|
|
179
179
|
};
|
|
180
|
+
var hasAuthRequirement = (apiMetaMap, endpoint) => {
|
|
181
|
+
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
182
|
+
return (meta?.auth?.login ?? false) || (meta?.auth?.hasAdditional ?? false);
|
|
183
|
+
};
|
|
180
184
|
var hasMetaEntry = (apiMetaMap, endpoint) => apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version] !== void 0;
|
|
181
185
|
var getRateLimit = (apiMetaMap, endpoint) => {
|
|
182
186
|
const meta = apiMetaMap[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
@@ -222,7 +226,7 @@ var runContractTests = async (input) => {
|
|
|
222
226
|
|
|
223
227
|
// src/authEnforcementCheck.ts
|
|
224
228
|
var DEFAULT_REQUEST_TIMEOUT_MS2 = 5e3;
|
|
225
|
-
var
|
|
229
|
+
var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set(["auth.required", "auth.forbidden"]);
|
|
226
230
|
var runAuthEnforcementCheck = async (input) => {
|
|
227
231
|
const { endpoint, baseUrl } = input;
|
|
228
232
|
const url = `${baseUrl.replace(/\/$/, "")}/${endpoint.fullPath}`;
|
|
@@ -265,14 +269,14 @@ var runAuthEnforcementCheck = async (input) => {
|
|
|
265
269
|
durationMs
|
|
266
270
|
};
|
|
267
271
|
}
|
|
268
|
-
if (parsed.errorCode
|
|
272
|
+
if (parsed.errorCode === void 0 || !EXPECTED_ERROR_CODES.has(parsed.errorCode)) {
|
|
269
273
|
return {
|
|
270
274
|
endpoint,
|
|
271
275
|
status: "fail",
|
|
272
276
|
httpStatus,
|
|
273
277
|
responseStatus: "error",
|
|
274
278
|
errorCode: parsed.errorCode,
|
|
275
|
-
reason: `expected errorCode
|
|
279
|
+
reason: `expected an auth-rejection errorCode (${[...EXPECTED_ERROR_CODES].join(" | ")}) but got '${parsed.errorCode ?? "(missing)"}'`,
|
|
276
280
|
durationMs
|
|
277
281
|
};
|
|
278
282
|
}
|
|
@@ -292,7 +296,7 @@ var runAuthEnforcementTests = async (input) => {
|
|
|
292
296
|
const skip = input.skip ?? [];
|
|
293
297
|
const results = [];
|
|
294
298
|
for (const endpoint of endpoints) {
|
|
295
|
-
if (!
|
|
299
|
+
if (!hasAuthRequirement(input.apiMetaMap, endpoint)) {
|
|
296
300
|
continue;
|
|
297
301
|
}
|
|
298
302
|
if (shouldSkip(endpoint, skip)) {
|
|
@@ -319,16 +323,11 @@ var runAuthEnforcementTests = async (input) => {
|
|
|
319
323
|
|
|
320
324
|
// src/rateLimitCheck.ts
|
|
321
325
|
var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
|
|
322
|
-
var
|
|
326
|
+
var EXPECTED_ERROR_CODE = "api.rateLimitExceeded";
|
|
323
327
|
var PRE_LIMITER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
324
328
|
"auth.required",
|
|
325
|
-
"auth.
|
|
326
|
-
"auth.
|
|
327
|
-
"auth.unauthorized",
|
|
328
|
-
"api.csrfTokenMissing",
|
|
329
|
-
"api.csrfTokenInvalid",
|
|
330
|
-
"api.unauthorized",
|
|
331
|
-
"api.validationError"
|
|
329
|
+
"auth.forbidden",
|
|
330
|
+
"auth.csrfMismatch"
|
|
332
331
|
]);
|
|
333
332
|
var runRateLimitCheck = async (input) => {
|
|
334
333
|
const { endpoint, baseUrl, rateLimit } = input;
|
|
@@ -368,7 +367,7 @@ var runRateLimitCheck = async (input) => {
|
|
|
368
367
|
};
|
|
369
368
|
}
|
|
370
369
|
const { httpStatus, parsed } = final;
|
|
371
|
-
if (parsed?.status === "error" && parsed.errorCode ===
|
|
370
|
+
if (parsed?.status === "error" && parsed.errorCode === EXPECTED_ERROR_CODE) {
|
|
372
371
|
return {
|
|
373
372
|
endpoint,
|
|
374
373
|
status: "pass",
|
|
@@ -385,16 +384,16 @@ var runRateLimitCheck = async (input) => {
|
|
|
385
384
|
httpStatus,
|
|
386
385
|
responseStatus,
|
|
387
386
|
errorCode: parsed?.errorCode,
|
|
388
|
-
reason: `expected '${
|
|
387
|
+
reason: `expected '${EXPECTED_ERROR_CODE}' on request ${rateLimit + 1} but got ${parsed?.status ?? "none"}/${parsed?.errorCode ?? "(none)"}`,
|
|
389
388
|
durationMs
|
|
390
389
|
};
|
|
391
390
|
};
|
|
392
391
|
|
|
393
392
|
// src/resetServerState.ts
|
|
394
|
-
import { tryCatch as tryCatch2 } from "@luckystack/core";
|
|
393
|
+
import { getProjectConfig, tryCatch as tryCatch2 } from "@luckystack/core";
|
|
395
394
|
var DEFAULT_REQUEST_TIMEOUT_MS4 = 1e4;
|
|
396
395
|
var resetServerState = async ({ baseUrl, token, requestTimeoutMs }) => {
|
|
397
|
-
const url = `${baseUrl.replace(/\/$/, "")}
|
|
396
|
+
const url = `${baseUrl.replace(/\/$/, "")}${getProjectConfig().http.testResetEndpoint}`;
|
|
398
397
|
const headers = { "Content-Type": "application/json" };
|
|
399
398
|
if (token) headers["X-Test-Reset-Token"] = token;
|
|
400
399
|
const controller = new AbortController();
|
|
@@ -686,12 +685,14 @@ var runFuzzTests = async (input) => {
|
|
|
686
685
|
|
|
687
686
|
// src/schemaSampleInput.ts
|
|
688
687
|
var innerDef = (node) => node ? node.def ?? node._def : void 0;
|
|
689
|
-
var sampleForDef = (def) => {
|
|
688
|
+
var sampleForDef = (def, options) => {
|
|
690
689
|
const d = def;
|
|
691
690
|
const name = d.typeName ?? d.type;
|
|
692
691
|
switch (name) {
|
|
693
692
|
case "ZodString":
|
|
694
693
|
case "string": {
|
|
694
|
+
const isConstrained = Boolean(d.format) || d.check === "string_format" || Array.isArray(d.checks) && d.checks.length > 0;
|
|
695
|
+
if (options?.stringPrefix && !isConstrained) return `${options.stringPrefix}test`;
|
|
695
696
|
return "test";
|
|
696
697
|
}
|
|
697
698
|
case "ZodNumber":
|
|
@@ -742,7 +743,7 @@ var sampleForDef = (def) => {
|
|
|
742
743
|
case "default":
|
|
743
744
|
case "ZodCatch":
|
|
744
745
|
case "catch": {
|
|
745
|
-
return sampleForDef(innerDef(d.innerType));
|
|
746
|
+
return sampleForDef(innerDef(d.innerType), options);
|
|
746
747
|
}
|
|
747
748
|
case "ZodEffects":
|
|
748
749
|
case "effects":
|
|
@@ -752,18 +753,18 @@ var sampleForDef = (def) => {
|
|
|
752
753
|
case "branded":
|
|
753
754
|
case "ZodReadonly":
|
|
754
755
|
case "readonly": {
|
|
755
|
-
return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType));
|
|
756
|
+
return sampleForDef(innerDef(d.schema ?? d.in ?? d.innerType), options);
|
|
756
757
|
}
|
|
757
758
|
case "ZodTuple":
|
|
758
759
|
case "tuple": {
|
|
759
760
|
const items = d.items ?? [];
|
|
760
|
-
return items.map((item) => sampleForDef(innerDef(item)));
|
|
761
|
+
return items.map((item) => sampleForDef(innerDef(item), options));
|
|
761
762
|
}
|
|
762
763
|
case "ZodUnion":
|
|
763
764
|
case "union":
|
|
764
765
|
case "ZodDiscriminatedUnion":
|
|
765
766
|
case "discriminatedUnion": {
|
|
766
|
-
return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0])) : void 0;
|
|
767
|
+
return d.options && d.options.length > 0 && d.options[0] ? sampleForDef(innerDef(d.options[0]), options) : void 0;
|
|
767
768
|
}
|
|
768
769
|
case "ZodArray":
|
|
769
770
|
case "array": {
|
|
@@ -774,7 +775,7 @@ var sampleForDef = (def) => {
|
|
|
774
775
|
const out = {};
|
|
775
776
|
const shape = typeof d.shape === "function" ? d.shape() : d.shape ?? {};
|
|
776
777
|
for (const [key, valueSchema] of Object.entries(shape)) {
|
|
777
|
-
const sample = sampleForDef(innerDef(valueSchema));
|
|
778
|
+
const sample = sampleForDef(innerDef(valueSchema), options);
|
|
778
779
|
if (sample !== void 0) out[key] = sample;
|
|
779
780
|
}
|
|
780
781
|
return out;
|
|
@@ -792,11 +793,16 @@ var sampleForDef = (def) => {
|
|
|
792
793
|
}
|
|
793
794
|
}
|
|
794
795
|
};
|
|
795
|
-
var sampleSchemaInput = (schema) => {
|
|
796
|
+
var sampleSchemaInput = (schema, options) => {
|
|
796
797
|
const node = schema;
|
|
797
|
-
return sampleForDef(node.def ?? node._def);
|
|
798
|
+
return sampleForDef(node.def ?? node._def, options);
|
|
798
799
|
};
|
|
799
800
|
|
|
801
|
+
// src/testDataMarker.ts
|
|
802
|
+
import { randomUUID } from "crypto";
|
|
803
|
+
var TEST_DATA_PREFIX = "lstest_";
|
|
804
|
+
var createTestDataMarker = () => `${TEST_DATA_PREFIX}${randomUUID().slice(0, 8)}_`;
|
|
805
|
+
|
|
800
806
|
// src/reporter.ts
|
|
801
807
|
var statusIcon = (status) => {
|
|
802
808
|
switch (status) {
|
|
@@ -845,7 +851,7 @@ import crypto from "crypto";
|
|
|
845
851
|
import fs from "fs";
|
|
846
852
|
import path from "path";
|
|
847
853
|
import { pathToFileURL } from "url";
|
|
848
|
-
import { getCsrfConfig, getPrismaClient, getProjectConfig, getSrcDir, tryCatch as tryCatch4, tryCatchSync } from "@luckystack/core";
|
|
854
|
+
import { getCsrfConfig, getPrismaClient, getProjectConfig as getProjectConfig2, getSrcDir, tryCatch as tryCatch4, tryCatchSync } from "@luckystack/core";
|
|
849
855
|
|
|
850
856
|
// src/streamWatcher.ts
|
|
851
857
|
import { io as createSocketIoClient } from "socket.io-client";
|
|
@@ -1174,7 +1180,7 @@ var buildCallSync = (baseUrl, routePath, state, cookieName) => {
|
|
|
1174
1180
|
};
|
|
1175
1181
|
};
|
|
1176
1182
|
var buildContext = (discovery, input) => {
|
|
1177
|
-
const cookieName = input.sessionCookieName ??
|
|
1183
|
+
const cookieName = input.sessionCookieName ?? getProjectConfig2().http.sessionCookieName;
|
|
1178
1184
|
const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
|
|
1179
1185
|
const routeFullName = `sync/${routePath}`;
|
|
1180
1186
|
const apiMethod = input.apiMethodMap?.[discovery.route.page]?.[discovery.route.name]?.[discovery.route.version] ?? "POST";
|
|
@@ -1297,10 +1303,21 @@ var runCustomTests = async (input) => {
|
|
|
1297
1303
|
};
|
|
1298
1304
|
|
|
1299
1305
|
// src/runAllTests.ts
|
|
1300
|
-
import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as
|
|
1301
|
-
var inputForEndpoint = (apiInputSchemas) => (endpoint) => {
|
|
1306
|
+
import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as getProjectConfig3 } from "@luckystack/core";
|
|
1307
|
+
var inputForEndpoint = (apiInputSchemas, stringPrefix) => (endpoint) => {
|
|
1302
1308
|
const schema = apiInputSchemas[endpoint.page]?.[endpoint.name]?.[endpoint.version];
|
|
1303
|
-
return schema ? sampleSchemaInput(schema) : {};
|
|
1309
|
+
return schema ? sampleSchemaInput(schema, { stringPrefix }) : {};
|
|
1310
|
+
};
|
|
1311
|
+
var resolveResetBookendToken = (input) => {
|
|
1312
|
+
if (input.resetBookends === false) return null;
|
|
1313
|
+
const token = input.resetToken ?? process.env.TEST_RESET_TOKEN;
|
|
1314
|
+
if (!token) {
|
|
1315
|
+
console.warn(
|
|
1316
|
+
"[test-runner] runAllTests: reset bookends skipped \u2014 no reset token (set TEST_RESET_TOKEN or pass resetToken/resetBookends). /_test/reset requires it, so the clean-slate + cleanup resets around the sweep are skipped."
|
|
1317
|
+
);
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
return token;
|
|
1304
1321
|
};
|
|
1305
1322
|
var matchesFilter = (endpoint, filter) => {
|
|
1306
1323
|
if (!filter) return true;
|
|
@@ -1314,7 +1331,7 @@ var cloneSummary = (summary, filter) => {
|
|
|
1314
1331
|
var buildAuthHeaders = async (input) => {
|
|
1315
1332
|
const headers = {};
|
|
1316
1333
|
if (!input.authToken) return headers;
|
|
1317
|
-
const cookieName = input.sessionCookieName ??
|
|
1334
|
+
const cookieName = input.sessionCookieName ?? getProjectConfig3().http.sessionCookieName;
|
|
1318
1335
|
headers.Cookie = `${cookieName}=${input.authToken}`;
|
|
1319
1336
|
try {
|
|
1320
1337
|
const { getSession } = await import("@luckystack/login");
|
|
@@ -1332,6 +1349,10 @@ var buildAuthHeaders = async (input) => {
|
|
|
1332
1349
|
return headers;
|
|
1333
1350
|
};
|
|
1334
1351
|
var runSweepLayers = async (input, summary, headers, inputFor) => {
|
|
1352
|
+
const resetBookendToken = resolveResetBookendToken(input);
|
|
1353
|
+
if (resetBookendToken) {
|
|
1354
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1355
|
+
}
|
|
1335
1356
|
const contract = await runContractTests({
|
|
1336
1357
|
apiMethodMap: input.apiMethodMap,
|
|
1337
1358
|
baseUrl: input.baseUrl,
|
|
@@ -1358,9 +1379,13 @@ var runSweepLayers = async (input, summary, headers, inputFor) => {
|
|
|
1358
1379
|
inputFor,
|
|
1359
1380
|
//? Reset the shared per-IP rate-limit bucket between endpoints so a
|
|
1360
1381
|
//? neighbour saturating the window doesn't cause false pass/fail on the
|
|
1361
|
-
//? next endpoint. Requires the server to expose /_test/reset (
|
|
1362
|
-
//?
|
|
1363
|
-
resetBetweenEndpoints: true
|
|
1382
|
+
//? next endpoint. Requires the server to expose /_test/reset (NODE_ENV in
|
|
1383
|
+
//? { 'development', 'test' } AND TEST_RESET_TOKEN set + matching header).
|
|
1384
|
+
resetBetweenEndpoints: true,
|
|
1385
|
+
//? Thread the reset token (explicit input, else the TEST_RESET_TOKEN env the
|
|
1386
|
+
//? server validates against) — without it `/_test/reset` returns 403 and the
|
|
1387
|
+
//? between-endpoint reset silently never happens.
|
|
1388
|
+
resetToken: input.resetToken ?? process.env.TEST_RESET_TOKEN
|
|
1364
1389
|
});
|
|
1365
1390
|
summary.rateLimit = cloneSummary(rateLimit, input.filter);
|
|
1366
1391
|
}
|
|
@@ -1384,6 +1409,9 @@ var runSweepLayers = async (input, summary, headers, inputFor) => {
|
|
|
1384
1409
|
});
|
|
1385
1410
|
summary.fuzz = cloneSummary(fuzz, input.filter);
|
|
1386
1411
|
}
|
|
1412
|
+
if (resetBookendToken) {
|
|
1413
|
+
await resetServerState({ baseUrl: input.baseUrl, token: resetBookendToken });
|
|
1414
|
+
}
|
|
1387
1415
|
};
|
|
1388
1416
|
var runCustomLayer = async (input, summary) => {
|
|
1389
1417
|
try {
|
|
@@ -1437,7 +1465,7 @@ var runAllTests = async (input) => {
|
|
|
1437
1465
|
}
|
|
1438
1466
|
}
|
|
1439
1467
|
const headers = await buildAuthHeaders(input);
|
|
1440
|
-
const inputFor = inputForEndpoint(input.apiInputSchemas);
|
|
1468
|
+
const inputFor = inputForEndpoint(input.apiInputSchemas, createTestDataMarker());
|
|
1441
1469
|
if (!input.noSweep) {
|
|
1442
1470
|
await runSweepLayers(input, summary, headers, inputFor);
|
|
1443
1471
|
}
|
|
@@ -1559,8 +1587,79 @@ var resetTestExtensionsForTests = () => {
|
|
|
1559
1587
|
fixtures.clear();
|
|
1560
1588
|
reporter = null;
|
|
1561
1589
|
};
|
|
1590
|
+
|
|
1591
|
+
// src/runRegisteredLayers.ts
|
|
1592
|
+
import { tryCatch as tryCatch5, tryCatchSync as tryCatchSync2 } from "@luckystack/core";
|
|
1593
|
+
var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
1594
|
+
var postWebhook = async (summary) => {
|
|
1595
|
+
const reporter2 = getTestReporter();
|
|
1596
|
+
const webhookUrl = reporter2?.webhookUrl;
|
|
1597
|
+
if (!reporter2 || !webhookUrl) return;
|
|
1598
|
+
const [, parsedUrl] = tryCatchSync2(() => new URL(webhookUrl));
|
|
1599
|
+
if (parsedUrl?.protocol === "http:" && !isLoopbackHost(parsedUrl.hostname)) {
|
|
1600
|
+
console.warn(
|
|
1601
|
+
`[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.`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
const headers = { "Content-Type": "application/json" };
|
|
1605
|
+
if (reporter2.webhookAuth?.type === "bearer") headers.Authorization = `Bearer ${reporter2.webhookAuth.token}`;
|
|
1606
|
+
const controller = new AbortController();
|
|
1607
|
+
const timeoutHandle = setTimeout(() => {
|
|
1608
|
+
controller.abort();
|
|
1609
|
+
}, 1e4);
|
|
1610
|
+
await tryCatch5(() => fetch(webhookUrl, {
|
|
1611
|
+
method: "POST",
|
|
1612
|
+
headers,
|
|
1613
|
+
body: JSON.stringify(summary),
|
|
1614
|
+
signal: controller.signal
|
|
1615
|
+
}));
|
|
1616
|
+
clearTimeout(timeoutHandle);
|
|
1617
|
+
};
|
|
1618
|
+
var runRegisteredLayers = async (input) => {
|
|
1619
|
+
const layers2 = listTestLayers();
|
|
1620
|
+
const endpoints = walkEndpoints(input.apiMethodMap);
|
|
1621
|
+
const reporter2 = getTestReporter();
|
|
1622
|
+
const skip = input.skip ?? [];
|
|
1623
|
+
const results = [];
|
|
1624
|
+
for (const layer of layers2) {
|
|
1625
|
+
for (const endpoint of endpoints) {
|
|
1626
|
+
if (shouldSkip(endpoint, skip)) continue;
|
|
1627
|
+
const started = Date.now();
|
|
1628
|
+
const [runError, layerResult] = await tryCatch5(async () => layer.run({ endpoint: endpoint.fullPath, method: endpoint.method, authToken: input.authToken }));
|
|
1629
|
+
const durationMs = Date.now() - started;
|
|
1630
|
+
const result = runError || !layerResult ? {
|
|
1631
|
+
layer: layer.name,
|
|
1632
|
+
endpoint: endpoint.fullPath,
|
|
1633
|
+
passed: false,
|
|
1634
|
+
message: runError?.message ?? "layer returned no result",
|
|
1635
|
+
durationMs
|
|
1636
|
+
} : {
|
|
1637
|
+
layer: layer.name,
|
|
1638
|
+
endpoint: endpoint.fullPath,
|
|
1639
|
+
passed: layerResult.passed,
|
|
1640
|
+
message: layerResult.message,
|
|
1641
|
+
durationMs,
|
|
1642
|
+
metadata: layerResult.metadata
|
|
1643
|
+
};
|
|
1644
|
+
results.push(result);
|
|
1645
|
+
await tryCatch5(async () => reporter2?.onResult?.(result));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const summary = {
|
|
1649
|
+
totalLayers: layers2.length,
|
|
1650
|
+
totalEndpoints: endpoints.length,
|
|
1651
|
+
passed: results.filter((r) => r.passed).length,
|
|
1652
|
+
failed: results.filter((r) => !r.passed).length,
|
|
1653
|
+
results
|
|
1654
|
+
};
|
|
1655
|
+
await tryCatch5(async () => reporter2?.onSummary?.(summary));
|
|
1656
|
+
await postWebhook(summary);
|
|
1657
|
+
return summary;
|
|
1658
|
+
};
|
|
1562
1659
|
export {
|
|
1563
1660
|
LAYER_KEYS,
|
|
1661
|
+
TEST_DATA_PREFIX,
|
|
1662
|
+
createTestDataMarker,
|
|
1564
1663
|
discoverCustomTestFiles,
|
|
1565
1664
|
getTestFixture,
|
|
1566
1665
|
getTestReporter,
|
|
@@ -1586,6 +1685,7 @@ export {
|
|
|
1586
1685
|
runFuzzTests,
|
|
1587
1686
|
runRateLimitCheck,
|
|
1588
1687
|
runRateLimitTests,
|
|
1688
|
+
runRegisteredLayers,
|
|
1589
1689
|
sampleSchemaInput,
|
|
1590
1690
|
walkEndpoints,
|
|
1591
1691
|
walkSyncEndpoints
|