@luckystack/test-runner 0.7.3 → 0.7.5
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 +24 -0
- package/CLAUDE.md +6 -2
- package/README.md +23 -20
- package/dist/index.d.ts +37 -1
- package/dist/index.js +118 -32
- package/dist/index.js.map +1 -1
- package/docs/contract-tests.md +41 -1
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.4] - 2026-07-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `resolveTestEnvironment({ loadProjectConfig? })` and `RunAllTestsInput.loadProjectConfig`
|
|
15
|
+
give the test process the same env prefix as server/ORM boot: load env files,
|
|
16
|
+
lazily read consumer config, then dynamically run optional secret-manager
|
|
17
|
+
resolution before any layer or Layer-5 test-module import.
|
|
18
|
+
- `resolveTestBaseUrl({ cwd?, fallbackUrl? })` centralizes live-test target
|
|
19
|
+
resolution: explicit `TEST_BASE_URL`, then the backend's actually-bound dev
|
|
20
|
+
port advertisement (only while its owner PID is alive), then the caller's
|
|
21
|
+
config-derived fallback.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Layer-5 tests that directly use Prisma/Redis no longer receive unresolved
|
|
26
|
+
values such as `DATABASE_URL_V1` merely because only the separate live-server
|
|
27
|
+
process ran secret-manager bootstrap. A configured resolver that cannot load
|
|
28
|
+
now fails before test execution with an actionable error.
|
|
29
|
+
- Direct `runCustomTests(...)` calls now have the same lazy config/env bootstrap
|
|
30
|
+
as `runAllTests`; both public orchestrators require the loader and fail closed
|
|
31
|
+
when an untyped caller omits it. The internal prepared entrypoint prevents a
|
|
32
|
+
second env load from replacing resolved secrets with pointers.
|
|
33
|
+
|
|
10
34
|
## [0.5.0] - 2026-07-11
|
|
11
35
|
|
|
12
36
|
### Changed
|
package/CLAUDE.md
CHANGED
|
@@ -27,7 +27,7 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
|
|
|
27
27
|
| Function / Export | 1-regel | Deep doc |
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| `runAllTests({ apiMethodMap, apiMetaMap, apiInputSchemas, baseUrl, ... })` | Orchestrator: draait contract + auth + rate-limit + fuzz + custom (Layer 5) in volgorde, levert een `RunAllTestsSummary`. | → docs/contract-tests.md |
|
|
30
|
-
| `runCustomTests(input)` | Layer 5: draait per-route
|
|
30
|
+
| `runCustomTests(input)` | Layer 5: draait per-route tests. Direct callers moeten lazy `loadProjectConfig` meegeven; env/secrets resolven vóór test-module imports. `runAllTests` gebruikt een interne already-prepared entrypoint om pointer-reload te voorkomen. | → docs/contract-tests.md |
|
|
31
31
|
| `discoverCustomTestFiles(srcDir?)` | Walker die elke `<name>_v<N>.tests.ts` onder de configured src dir yields. | → docs/contract-tests.md |
|
|
32
32
|
| `logRunAllSummary(summary)` | Pretty-print één `RunAllTestsSummary`. | → docs/extension-hooks.md |
|
|
33
33
|
| `walkEndpoints(apiMethodMap)` | Genereert `EndpointDescriptor[]` uit de generated map | → docs/contract-tests.md |
|
|
@@ -40,6 +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
|
+
| `resolveTestBaseUrl({ cwd?, fallbackUrl? })` | Resolves `TEST_BASE_URL` → actually-bound dev port advertisement → caller fallback, so an auto-hop does not create false connection failures. | → docs/contract-tests.md |
|
|
44
|
+
| `resolveTestEnvironment({ loadProjectConfig? })` | Laadt `.env`-lagen en resolveert optionele secret-manager-pointers in het testproces vóór DB/Redis-backed Layer-5 modules importeren. | → docs/contract-tests.md |
|
|
43
45
|
| `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
46
|
| `createTestDataMarker()` / `TEST_DATA_PREFIX` | Run-unique `lstest_<uuid>_` marker (+ vaste `lstest_` prefix-constant) waarmee `runAllTests` gegenereerde test-strings identificeerbaar maakt. | → docs/contract-tests.md |
|
|
45
47
|
| `logContractResult(result)` | Pretty-print één `ContractCheckResult` | → docs/extension-hooks.md |
|
|
@@ -76,6 +78,8 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
|
|
|
76
78
|
|
|
77
79
|
## Config keys (env vars + register* slots)
|
|
78
80
|
|
|
81
|
+
- `TEST_BASE_URL` (env, test process) — explicit target override consumed first by `resolveTestBaseUrl`; otherwise it reads `node_modules/.luckystack/dev-server.json` and then uses the supplied config-derived fallback.
|
|
82
|
+
- `RunAllTestsInput.loadProjectConfig` / `RunCustomTestsInput.loadProjectConfig` — verplichte lazy loader voor de default export van consumer `config.ts`. De runner laadt eerst `.env`/`.env.local`, leest daarna deze config en draait bij een niet-lege `secretManager.url` `initSecretManager({...config, source:'remote'})` vóór custom test imports. `runAllTests` doet dit één keer voor alle lagen; directe Layer-5-calls doen het zelf. Een ontbrekende loader of geconfigureerd-maar-ontbrekende secret-manager is een harde fout.
|
|
79
83
|
- `NODE_ENV` (env, server-side) — `/_test/reset` is automatisch beschikbaar wanneer NIET `production`.
|
|
80
84
|
- `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.
|
|
81
85
|
- `RunRateLimitTestsInput.maxRateLimitToTest` (default `50`) — endpoints met hogere `rateLimit` worden geskipt om duizenden requests in CI te vermijden.
|
|
@@ -90,7 +94,7 @@ Generated-type-driven test layers voor LuckyStack APIs. Walkt elke endpoint uit
|
|
|
90
94
|
- **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).
|
|
91
95
|
- **Required server-side**: `@luckystack/server` met `/_test/reset` endpoint gemount (default ingebouwd, gated op `NODE_ENV !== 'production'`).
|
|
92
96
|
- **Required generated artefact**: `apiMethodMap.generated.ts` (en `apiMetaMap.generated.ts` voor de auth/rate-limit layers), geproduceerd door `@luckystack/devkit`.
|
|
93
|
-
- **Optional**:
|
|
97
|
+
- **Optional**: `@luckystack/secret-manager@^0.7.3` (optional peer) — alleen dynamisch geladen wanneer `loadProjectConfig` een niet-lege `config.secretManager.url` teruggeeft. Zonder secret-managerconfig blijft de runner een gewone lokale-env flow.
|
|
94
98
|
|
|
95
99
|
## Related
|
|
96
100
|
|
package/README.md
CHANGED
|
@@ -14,29 +14,31 @@ Run all four layers against a running server:
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
runFuzzTests,
|
|
21
|
-
resetServerState,
|
|
22
|
-
logContractSummary,
|
|
17
|
+
runAllTests,
|
|
18
|
+
resolveTestBaseUrl,
|
|
19
|
+
logRunAllSummary,
|
|
23
20
|
} from '@luckystack/test-runner';
|
|
24
|
-
import {
|
|
25
|
-
import { apiMetaMap } from './src/_sockets/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
import { apiInputSchemas } from './src/_sockets/apiInputSchemas.generated';
|
|
22
|
+
import { apiMetaMap, apiMethodMap } from './src/_sockets/apiTypes.generated';
|
|
23
|
+
import { ports } from './config.ports';
|
|
24
|
+
|
|
25
|
+
const baseUrl = resolveTestBaseUrl({ fallbackUrl: `http://localhost:${ports.backend}` });
|
|
26
|
+
const summary = await runAllTests({
|
|
27
|
+
apiMethodMap,
|
|
28
|
+
apiMetaMap,
|
|
29
|
+
apiInputSchemas,
|
|
30
|
+
baseUrl,
|
|
31
|
+
// Loaded only after .env/.env.local. If config.secretManager.url is set,
|
|
32
|
+
// pointers are resolved in THIS process before Layer-5 tests touch Prisma.
|
|
33
|
+
loadProjectConfig: async () => (await import('./config')).default,
|
|
34
|
+
});
|
|
35
|
+
logRunAllSummary(summary);
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Each layer is independently runnable — you can skip the ones that don't apply or rearrange them.
|
|
39
39
|
|
|
40
|
+
`runAllTests` always loads the normal env-file layers and requires `loadProjectConfig` as above, even in projects that currently use only local env values. A configured resolver is loaded dynamically and runs in fail-fast remote mode before custom test modules are imported. Direct `runCustomTests` callers must pass the same callback. Both public paths reject a missing loader so a later secret-manager rollout cannot silently expose pointer values. This matters because the live server resolving its own `DATABASE_URL` does not mutate the separate test process.
|
|
41
|
+
|
|
40
42
|
## Layers
|
|
41
43
|
|
|
42
44
|
| Layer | What it asserts |
|
|
@@ -63,6 +65,7 @@ The fuzz layer is intentionally exhaustive and slow — wire it into nightly CI
|
|
|
63
65
|
| `runRateLimitCheck(input)` / `runRateLimitTests(input)` | Rate-limit layer. |
|
|
64
66
|
| `runFuzzCheck(input)` / `runFuzzTests(input)` | Fuzz layer. |
|
|
65
67
|
| `resetServerState(input)` | POST to `/_test/reset`. |
|
|
68
|
+
| `resolveTestEnvironment({ loadProjectConfig? })` | Load env layers and optional secret-manager pointers for the test process; `runAllTests` calls it automatically. |
|
|
66
69
|
| `sampleSchemaInput(schema)` | Generate a Zod-valid sample payload. |
|
|
67
70
|
| `logContractResult` / `logContractSummary` | Pretty-printers. |
|
|
68
71
|
|
|
@@ -75,8 +78,8 @@ Types: `EndpointDescriptor`, `HttpMethod`, `ContractCheckResult`, `RunContractSu
|
|
|
75
78
|
|
|
76
79
|
## Dependencies
|
|
77
80
|
|
|
78
|
-
-
|
|
79
|
-
|
|
81
|
+
- Required peers: `zod@^4.0.0`, `socket.io-client@^4.8.0`.
|
|
82
|
+
- Optional peers: `@luckystack/login`, `@luckystack/secret-manager` (loaded only for their corresponding features).
|
|
80
83
|
|
|
81
84
|
## License
|
|
82
85
|
|
package/dist/index.d.ts
CHANGED
|
@@ -199,6 +199,30 @@ interface ResetServerStateInput {
|
|
|
199
199
|
}
|
|
200
200
|
declare const resetServerState: ({ baseUrl, token, requestTimeoutMs }: ResetServerStateInput) => Promise<boolean>;
|
|
201
201
|
|
|
202
|
+
interface ResolveTestBaseUrlOptions {
|
|
203
|
+
/** Project root containing node_modules/.luckystack/dev-server.json. */
|
|
204
|
+
cwd?: string;
|
|
205
|
+
/** Used when no explicit override or valid live advertisement exists. */
|
|
206
|
+
fallbackUrl?: string;
|
|
207
|
+
}
|
|
208
|
+
declare const resolveTestBaseUrl: (options?: ResolveTestBaseUrlOptions) => string;
|
|
209
|
+
|
|
210
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
211
|
+
interface ResolveTestEnvironmentInput {
|
|
212
|
+
/**
|
|
213
|
+
* Loads the consumer's default-exported `config.ts` object after `.env` files
|
|
214
|
+
* are available. Keeping this as a callback avoids hardcoding a consumer path
|
|
215
|
+
* inside the package and keeps `@luckystack/secret-manager` optional.
|
|
216
|
+
*/
|
|
217
|
+
loadProjectConfig?: () => MaybePromise<unknown>;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Load the project's env files and resolve optional secret-manager pointers for
|
|
221
|
+
* the TEST process. Call before importing integration-test modules that touch a
|
|
222
|
+
* database, Redis, or another env-backed SDK.
|
|
223
|
+
*/
|
|
224
|
+
declare const resolveTestEnvironment: (input?: ResolveTestEnvironmentInput) => Promise<void>;
|
|
225
|
+
|
|
202
226
|
interface SampleSchemaOptions {
|
|
203
227
|
/**
|
|
204
228
|
* Marker prefix applied to generated UNCONSTRAINED string values so any rows
|
|
@@ -298,6 +322,12 @@ interface RunCustomTestsInput {
|
|
|
298
322
|
* HTTP method (e.g. logout is DELETE). Falls back to POST when absent.
|
|
299
323
|
*/
|
|
300
324
|
apiMethodMap?: ApiMethodMap$1;
|
|
325
|
+
/**
|
|
326
|
+
* Lazy consumer config for direct Layer-5 runs. Required so the public path
|
|
327
|
+
* cannot silently skip a configured secret manager; `runAllTests` prepares
|
|
328
|
+
* the environment separately before using the internal Layer-5 entrypoint.
|
|
329
|
+
*/
|
|
330
|
+
loadProjectConfig: NonNullable<ResolveTestEnvironmentInput['loadProjectConfig']>;
|
|
301
331
|
onResult?: (result: CustomTestResult) => void;
|
|
302
332
|
}
|
|
303
333
|
interface DiscoveredTestFile {
|
|
@@ -358,6 +388,12 @@ declare const runCustomTests: (input: RunCustomTestsInput) => Promise<RunCustomT
|
|
|
358
388
|
type ApiInputSchemas = Partial<Record<string, Partial<Record<string, Partial<Record<string, ZodType | undefined>>>>>>;
|
|
359
389
|
interface RunAllTestsInput {
|
|
360
390
|
apiMethodMap: ApiMethodMap$1;
|
|
391
|
+
/**
|
|
392
|
+
* Lazy loader for the consumer's default `config.ts` export. Required so
|
|
393
|
+
* runAllTests cannot silently skip a configured secret manager; resolution
|
|
394
|
+
* happens in THIS process before any layer or env-backed client is touched.
|
|
395
|
+
*/
|
|
396
|
+
loadProjectConfig: NonNullable<ResolveTestEnvironmentInput['loadProjectConfig']>;
|
|
361
397
|
apiMetaMap: ApiMetaMap;
|
|
362
398
|
apiInputSchemas: ApiInputSchemas;
|
|
363
399
|
baseUrl: string;
|
|
@@ -510,4 +546,4 @@ declare const LAYER_KEYS: {
|
|
|
510
546
|
readonly custom: "custom";
|
|
511
547
|
};
|
|
512
548
|
|
|
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 };
|
|
549
|
+
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 ResolveTestBaseUrlOptions, type ResolveTestEnvironmentInput, 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, resolveTestBaseUrl, resolveTestEnvironment, runAllTests, runAuthEnforcementCheck, runAuthEnforcementTests, runContractCheck, runContractTests, runCsrfEnforcementCheck, runCsrfEnforcementTests, runCustomTests, runFuzzCheck, runFuzzTests, runRateLimitCheck, runRateLimitTests, runRegisteredLayers, sampleSchemaInput, walkEndpoints, walkSyncEndpoints };
|
package/dist/index.js
CHANGED
|
@@ -694,6 +694,83 @@ var runFuzzTests = async (input) => {
|
|
|
694
694
|
return calculateSummary(results);
|
|
695
695
|
};
|
|
696
696
|
|
|
697
|
+
// src/resolveTestBaseUrl.ts
|
|
698
|
+
import fs from "fs";
|
|
699
|
+
import path from "path";
|
|
700
|
+
var isProcessRunning = (pid) => {
|
|
701
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
702
|
+
try {
|
|
703
|
+
process.kill(pid, 0);
|
|
704
|
+
return true;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var resolveTestBaseUrl = (options = {}) => {
|
|
710
|
+
const explicit = process.env.TEST_BASE_URL;
|
|
711
|
+
if (explicit) return explicit;
|
|
712
|
+
try {
|
|
713
|
+
const raw = fs.readFileSync(
|
|
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)}`;
|
|
720
|
+
}
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
return options.fallbackUrl ?? "http://localhost:80";
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// src/resolveTestEnvironment.ts
|
|
727
|
+
import { loadEnvFiles, tryCatch as tryCatch3 } from "@luckystack/core";
|
|
728
|
+
var assertProjectConfigLoader = (input, caller) => {
|
|
729
|
+
if (typeof input !== "object" || input === null || !("loadProjectConfig" in input) || typeof input.loadProjectConfig !== "function") {
|
|
730
|
+
throw new TypeError(`[test-runner] ${caller} requires a lazy loadProjectConfig callback`);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
734
|
+
var isToken = (value) => typeof value === "string" || isRecord(value) && typeof value.fromFile === "string";
|
|
735
|
+
var readSecretManagerConfig = (projectConfig) => {
|
|
736
|
+
if (!isRecord(projectConfig)) return null;
|
|
737
|
+
const candidate = projectConfig.secretManager;
|
|
738
|
+
if (!isRecord(candidate)) return null;
|
|
739
|
+
const url = candidate.url;
|
|
740
|
+
if (url === void 0 || url === "") return null;
|
|
741
|
+
if (typeof url !== "string") {
|
|
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 };
|
|
750
|
+
};
|
|
751
|
+
var resolveTestEnvironment = async (input = {}) => {
|
|
752
|
+
loadEnvFiles();
|
|
753
|
+
if (!input.loadProjectConfig) return;
|
|
754
|
+
const projectConfig = await input.loadProjectConfig();
|
|
755
|
+
const secretManagerConfig = readSecretManagerConfig(projectConfig);
|
|
756
|
+
if (!secretManagerConfig) return;
|
|
757
|
+
const [importError, secretManager] = await tryCatch3(
|
|
758
|
+
() => import("@luckystack/secret-manager")
|
|
759
|
+
);
|
|
760
|
+
if (importError || !secretManager) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
"[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.",
|
|
763
|
+
{ cause: importError ?? void 0 }
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
await secretManager.initSecretManager({
|
|
767
|
+
...secretManagerConfig,
|
|
768
|
+
//? Match the normal server/ORM bootstrap: configured remote resolution is
|
|
769
|
+
//? fail-fast. A raw DATABASE_URL_V<n> must never leak into Prisma.
|
|
770
|
+
source: "remote"
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
|
|
697
774
|
// src/schemaSampleInput.ts
|
|
698
775
|
var innerDef = (node) => node ? node.def ?? node._def : void 0;
|
|
699
776
|
var sampleForDef = (def, options) => {
|
|
@@ -859,17 +936,17 @@ var logContractSummary = (summary) => {
|
|
|
859
936
|
|
|
860
937
|
// src/customTests.ts
|
|
861
938
|
import crypto from "crypto";
|
|
862
|
-
import
|
|
863
|
-
import
|
|
939
|
+
import fs2 from "fs";
|
|
940
|
+
import path2 from "path";
|
|
864
941
|
import { pathToFileURL } from "url";
|
|
865
|
-
import { getCsrfConfig, getPrismaClient, getProjectConfig as getProjectConfig3, getSrcDir, tryCatch as
|
|
942
|
+
import { getCsrfConfig, getPrismaClient, getProjectConfig as getProjectConfig3, getSrcDir, tryCatch as tryCatch5, tryCatchSync } from "@luckystack/core";
|
|
866
943
|
|
|
867
944
|
// src/streamWatcher.ts
|
|
868
945
|
import { io as createSocketIoClient } from "socket.io-client";
|
|
869
946
|
import {
|
|
870
947
|
buildJoinRoomResponseEventName,
|
|
871
948
|
socketEventNames,
|
|
872
|
-
tryCatch as
|
|
949
|
+
tryCatch as tryCatch4,
|
|
873
950
|
getProjectConfig as getProjectConfig2
|
|
874
951
|
} from "@luckystack/core";
|
|
875
952
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
@@ -956,7 +1033,7 @@ var openStreamWatcher = async (input) => {
|
|
|
956
1033
|
}
|
|
957
1034
|
};
|
|
958
1035
|
socket.on(socketEventNames.sync, handleChunk);
|
|
959
|
-
const [connectError] = await
|
|
1036
|
+
const [connectError] = await tryCatch4(() => waitForConnect(socket));
|
|
960
1037
|
if (connectError) {
|
|
961
1038
|
socket.off(socketEventNames.sync, handleChunk);
|
|
962
1039
|
socket.disconnect();
|
|
@@ -967,7 +1044,7 @@ var openStreamWatcher = async (input) => {
|
|
|
967
1044
|
group: input.roomCode,
|
|
968
1045
|
responseIndex
|
|
969
1046
|
});
|
|
970
|
-
const [joinError] = await
|
|
1047
|
+
const [joinError] = await tryCatch4(() => waitForJoinAck(socket, responseIndex));
|
|
971
1048
|
if (joinError) {
|
|
972
1049
|
socket.off(socketEventNames.sync, handleChunk);
|
|
973
1050
|
socket.disconnect();
|
|
@@ -1024,36 +1101,36 @@ var API_TEST_FILE_PATTERN = /_v(\d+)\.tests\.ts$/;
|
|
|
1024
1101
|
var SYNC_SERVER_TEST_FILE_PATTERN = /_server_v(\d+)\.tests\.ts$/;
|
|
1025
1102
|
var DEFAULT_CALL_TIMEOUT_MS = 1e4;
|
|
1026
1103
|
var discoverCustomTestFiles = (srcDir = getSrcDir()) => {
|
|
1027
|
-
if (!
|
|
1104
|
+
if (!fs2.existsSync(srcDir)) return [];
|
|
1028
1105
|
const found = [];
|
|
1029
1106
|
const stack = [srcDir];
|
|
1030
1107
|
const visited = /* @__PURE__ */ new Set();
|
|
1031
1108
|
while (stack.length > 0) {
|
|
1032
1109
|
const current = stack.pop();
|
|
1033
1110
|
if (current === void 0) break;
|
|
1034
|
-
const [realError, realPath] = tryCatchSync(() =>
|
|
1111
|
+
const [realError, realPath] = tryCatchSync(() => fs2.realpathSync(current));
|
|
1035
1112
|
const canonical = realError || realPath === null ? current : realPath;
|
|
1036
1113
|
if (visited.has(canonical)) continue;
|
|
1037
1114
|
visited.add(canonical);
|
|
1038
|
-
const [readError, entries] = tryCatchSync(() =>
|
|
1115
|
+
const [readError, entries] = tryCatchSync(() => fs2.readdirSync(current, { withFileTypes: true }));
|
|
1039
1116
|
if (readError || entries === null) continue;
|
|
1040
1117
|
for (const entry of entries) {
|
|
1041
|
-
const fullPath =
|
|
1118
|
+
const fullPath = path2.join(current, entry.name);
|
|
1042
1119
|
if (entry.isSymbolicLink()) continue;
|
|
1043
1120
|
if (entry.isDirectory()) {
|
|
1044
1121
|
stack.push(fullPath);
|
|
1045
1122
|
continue;
|
|
1046
1123
|
}
|
|
1047
1124
|
if (!entry.isFile() || !entry.name.endsWith(".tests.ts")) continue;
|
|
1048
|
-
const parentDir =
|
|
1125
|
+
const parentDir = path2.basename(current);
|
|
1049
1126
|
if (parentDir !== "_api" && parentDir !== "_sync") continue;
|
|
1050
1127
|
const pattern = parentDir === "_api" ? API_TEST_FILE_PATTERN : SYNC_SERVER_TEST_FILE_PATTERN;
|
|
1051
1128
|
const versionMatch = pattern.exec(entry.name);
|
|
1052
1129
|
if (!versionMatch) continue;
|
|
1053
1130
|
const version = `v${versionMatch[1]}`;
|
|
1054
1131
|
const namePart = entry.name.replace(parentDir === "_api" ? /_v\d+\.tests\.ts$/ : /_server_v\d+\.tests\.ts$/, "");
|
|
1055
|
-
const pageDir =
|
|
1056
|
-
const relativePage =
|
|
1132
|
+
const pageDir = path2.dirname(current);
|
|
1133
|
+
const relativePage = path2.relative(srcDir, pageDir).replaceAll("\\", "/") || "system";
|
|
1057
1134
|
found.push({
|
|
1058
1135
|
filePath: fullPath,
|
|
1059
1136
|
kind: parentDir === "_api" ? "api" : "sync",
|
|
@@ -1081,7 +1158,7 @@ var buildExpect = () => ({
|
|
|
1081
1158
|
if (!value) throw new AssertionError(message ?? `expected truthy value, got ${JSON.stringify(value)}`);
|
|
1082
1159
|
},
|
|
1083
1160
|
throws: async (fn, message) => {
|
|
1084
|
-
const [err] = await
|
|
1161
|
+
const [err] = await tryCatch5(fn);
|
|
1085
1162
|
if (!err) throw new AssertionError(message ?? "expected fn to throw, but it did not");
|
|
1086
1163
|
return err;
|
|
1087
1164
|
},
|
|
@@ -1133,7 +1210,7 @@ var buildSessionHelpers = (state) => {
|
|
|
1133
1210
|
};
|
|
1134
1211
|
};
|
|
1135
1212
|
var parseResponse = async (response, state) => {
|
|
1136
|
-
const [textError, text] = await
|
|
1213
|
+
const [textError, text] = await tryCatch5(() => response.text());
|
|
1137
1214
|
if (textError || text === null) {
|
|
1138
1215
|
state.lastResponse = null;
|
|
1139
1216
|
throw new Error(`failed to read response body (HTTP ${response.status}): ${textError?.message ?? "unknown"}`);
|
|
@@ -1159,7 +1236,7 @@ var buildCallApi = (baseUrl, routePath, state, cookieName, method) => {
|
|
|
1159
1236
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1160
1237
|
const init = { method, headers, signal: controller.signal };
|
|
1161
1238
|
if (method !== "GET" && method !== "HEAD") init.body = JSON.stringify(input ?? {});
|
|
1162
|
-
const [fetchError, response] = await
|
|
1239
|
+
const [fetchError, response] = await tryCatch5(() => fetch(url, init));
|
|
1163
1240
|
clearTimeout(timeoutHandle);
|
|
1164
1241
|
if (fetchError || !response) {
|
|
1165
1242
|
state.lastResponse = null;
|
|
@@ -1179,7 +1256,7 @@ var buildCallSync = (baseUrl, routePath, state, cookieName) => {
|
|
|
1179
1256
|
const timeoutHandle = setTimeout(() => {
|
|
1180
1257
|
controller.abort();
|
|
1181
1258
|
}, DEFAULT_CALL_TIMEOUT_MS);
|
|
1182
|
-
const [fetchError, response] = await
|
|
1259
|
+
const [fetchError, response] = await tryCatch5(() => fetch(url, {
|
|
1183
1260
|
method: "POST",
|
|
1184
1261
|
headers,
|
|
1185
1262
|
body: JSON.stringify(body),
|
|
@@ -1214,7 +1291,7 @@ var buildContext = (discovery, input) => {
|
|
|
1214
1291
|
while (watchers.length > 0) {
|
|
1215
1292
|
const watcher = watchers.pop();
|
|
1216
1293
|
if (!watcher) continue;
|
|
1217
|
-
const [closeError] = await
|
|
1294
|
+
const [closeError] = await tryCatch5(() => watcher.close());
|
|
1218
1295
|
if (closeError) {
|
|
1219
1296
|
}
|
|
1220
1297
|
}
|
|
@@ -1234,17 +1311,17 @@ var buildContext = (discovery, input) => {
|
|
|
1234
1311
|
};
|
|
1235
1312
|
return { ctx, closeAllWatchers, state };
|
|
1236
1313
|
};
|
|
1237
|
-
var
|
|
1314
|
+
var runCustomTestsAfterEnvironmentPrepared = async (input) => {
|
|
1238
1315
|
const srcDir = getSrcDir();
|
|
1239
1316
|
const files = discoverCustomTestFiles(srcDir);
|
|
1240
1317
|
const results = [];
|
|
1241
1318
|
for (const discovery of files) {
|
|
1242
1319
|
const routePath = `${discovery.route.page}/${discovery.route.name}/${discovery.route.version}`;
|
|
1243
1320
|
if (input.filter && !routePath.includes(input.filter)) continue;
|
|
1244
|
-
const [realPathErr, realFilePath] = tryCatchSync(() =>
|
|
1321
|
+
const [realPathErr, realFilePath] = tryCatchSync(() => fs2.realpathSync(discovery.filePath));
|
|
1245
1322
|
const resolvedPath = realPathErr ? discovery.filePath : realFilePath ?? discovery.filePath;
|
|
1246
|
-
const relToSrc =
|
|
1247
|
-
if (relToSrc.startsWith("..") ||
|
|
1323
|
+
const relToSrc = path2.relative(srcDir, resolvedPath);
|
|
1324
|
+
if (relToSrc.startsWith("..") || path2.isAbsolute(relToSrc)) {
|
|
1248
1325
|
const r = {
|
|
1249
1326
|
routePath,
|
|
1250
1327
|
caseName: "(import)",
|
|
@@ -1256,7 +1333,7 @@ var runCustomTests = async (input) => {
|
|
|
1256
1333
|
input.onResult?.(r);
|
|
1257
1334
|
continue;
|
|
1258
1335
|
}
|
|
1259
|
-
const [importError, mod] = await
|
|
1336
|
+
const [importError, mod] = await tryCatch5(
|
|
1260
1337
|
() => import(pathToFileURL(resolvedPath).href)
|
|
1261
1338
|
);
|
|
1262
1339
|
if (importError || !mod) {
|
|
@@ -1281,11 +1358,11 @@ var runCustomTests = async (input) => {
|
|
|
1281
1358
|
const expectedToFail = typeof c.expectedToFail === "string" ? c.expectedToFail : void 0;
|
|
1282
1359
|
const built = buildContext(discovery, input);
|
|
1283
1360
|
const started = Date.now();
|
|
1284
|
-
const [runError] = await
|
|
1285
|
-
await
|
|
1361
|
+
const [runError] = await tryCatch5(async () => c.run(built.ctx));
|
|
1362
|
+
await tryCatch5(() => built.closeAllWatchers());
|
|
1286
1363
|
const residualToken = built.state.token;
|
|
1287
1364
|
if (residualToken) {
|
|
1288
|
-
await
|
|
1365
|
+
await tryCatch5(async () => {
|
|
1289
1366
|
const { deleteSession } = await import("@luckystack/login");
|
|
1290
1367
|
await deleteSession(residualToken);
|
|
1291
1368
|
});
|
|
@@ -1319,6 +1396,11 @@ var runCustomTests = async (input) => {
|
|
|
1319
1396
|
results
|
|
1320
1397
|
};
|
|
1321
1398
|
};
|
|
1399
|
+
var runCustomTests = async (input) => {
|
|
1400
|
+
assertProjectConfigLoader(input, "runCustomTests");
|
|
1401
|
+
await resolveTestEnvironment({ loadProjectConfig: input.loadProjectConfig });
|
|
1402
|
+
return runCustomTestsAfterEnvironmentPrepared(input);
|
|
1403
|
+
};
|
|
1322
1404
|
|
|
1323
1405
|
// src/runAllTests.ts
|
|
1324
1406
|
import { clearAllRateLimits, getCsrfConfig as getCsrfConfig2, getProjectConfig as getProjectConfig4 } from "@luckystack/core";
|
|
@@ -1436,7 +1518,7 @@ var runCustomLayer = async (input, summary) => {
|
|
|
1436
1518
|
await clearAllRateLimits();
|
|
1437
1519
|
} catch {
|
|
1438
1520
|
}
|
|
1439
|
-
const custom = await
|
|
1521
|
+
const custom = await runCustomTestsAfterEnvironmentPrepared({
|
|
1440
1522
|
baseUrl: input.baseUrl,
|
|
1441
1523
|
sessionCookieName: input.sessionCookieName,
|
|
1442
1524
|
filter: input.filter,
|
|
@@ -1472,6 +1554,8 @@ var computeTotals = (summary) => {
|
|
|
1472
1554
|
summary.totalFailed = failed + (summary.custom?.failed ?? 0);
|
|
1473
1555
|
};
|
|
1474
1556
|
var runAllTests = async (input) => {
|
|
1557
|
+
assertProjectConfigLoader(input, "runAllTests");
|
|
1558
|
+
await resolveTestEnvironment({ loadProjectConfig: input.loadProjectConfig });
|
|
1475
1559
|
const summary = { totalPassed: 0, totalFailed: 0 };
|
|
1476
1560
|
if (!input.noSweep) {
|
|
1477
1561
|
const hasRoutes = Object.keys(input.apiMethodMap).length > 0;
|
|
@@ -1607,7 +1691,7 @@ var resetTestExtensionsForTests = () => {
|
|
|
1607
1691
|
};
|
|
1608
1692
|
|
|
1609
1693
|
// src/runRegisteredLayers.ts
|
|
1610
|
-
import { tryCatch as
|
|
1694
|
+
import { tryCatch as tryCatch6, tryCatchSync as tryCatchSync2 } from "@luckystack/core";
|
|
1611
1695
|
var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
1612
1696
|
var postWebhook = async (summary) => {
|
|
1613
1697
|
const reporter2 = getTestReporter();
|
|
@@ -1625,7 +1709,7 @@ var postWebhook = async (summary) => {
|
|
|
1625
1709
|
const timeoutHandle = setTimeout(() => {
|
|
1626
1710
|
controller.abort();
|
|
1627
1711
|
}, 1e4);
|
|
1628
|
-
await
|
|
1712
|
+
await tryCatch6(() => fetch(webhookUrl, {
|
|
1629
1713
|
method: "POST",
|
|
1630
1714
|
headers,
|
|
1631
1715
|
body: JSON.stringify(summary),
|
|
@@ -1643,7 +1727,7 @@ var runRegisteredLayers = async (input) => {
|
|
|
1643
1727
|
for (const endpoint of endpoints) {
|
|
1644
1728
|
if (shouldSkip(endpoint, skip)) continue;
|
|
1645
1729
|
const started = Date.now();
|
|
1646
|
-
const [runError, layerResult] = await
|
|
1730
|
+
const [runError, layerResult] = await tryCatch6(async () => layer.run({ endpoint: endpoint.fullPath, method: endpoint.method, authToken: input.authToken }));
|
|
1647
1731
|
const durationMs = Date.now() - started;
|
|
1648
1732
|
const result = runError || !layerResult ? {
|
|
1649
1733
|
layer: layer.name,
|
|
@@ -1660,7 +1744,7 @@ var runRegisteredLayers = async (input) => {
|
|
|
1660
1744
|
metadata: layerResult.metadata
|
|
1661
1745
|
};
|
|
1662
1746
|
results.push(result);
|
|
1663
|
-
await
|
|
1747
|
+
await tryCatch6(async () => reporter2?.onResult?.(result));
|
|
1664
1748
|
}
|
|
1665
1749
|
}
|
|
1666
1750
|
const summary = {
|
|
@@ -1670,7 +1754,7 @@ var runRegisteredLayers = async (input) => {
|
|
|
1670
1754
|
failed: results.filter((r) => !r.passed).length,
|
|
1671
1755
|
results
|
|
1672
1756
|
};
|
|
1673
|
-
await
|
|
1757
|
+
await tryCatch6(async () => reporter2?.onSummary?.(summary));
|
|
1674
1758
|
await postWebhook(summary);
|
|
1675
1759
|
return summary;
|
|
1676
1760
|
};
|
|
@@ -1691,6 +1775,8 @@ export {
|
|
|
1691
1775
|
registerTestReporter,
|
|
1692
1776
|
resetServerState,
|
|
1693
1777
|
resetTestExtensionsForTests,
|
|
1778
|
+
resolveTestBaseUrl,
|
|
1779
|
+
resolveTestEnvironment,
|
|
1694
1780
|
runAllTests,
|
|
1695
1781
|
runAuthEnforcementCheck,
|
|
1696
1782
|
runAuthEnforcementTests,
|