@luckystack/test-runner 0.10.2 → 0.11.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/AGENTS.md +7 -0
- package/CHANGELOG.md +7 -0
- package/README.md +13 -68
- package/dist/authEnforcementCheck.d.ts +12 -0
- package/dist/contractCheck.d.ts +24 -0
- package/dist/csrfEnforcementCheck.d.ts +25 -0
- package/dist/customTests.d.ts +145 -0
- package/dist/extensionRegistry.d.ts +83 -0
- package/dist/fuzzCheck.d.ts +12 -0
- package/dist/index.d.ts +49 -745
- package/dist/index.js.map +1 -1
- package/dist/probeRequest.d.ts +48 -0
- package/dist/rateLimitCheck.d.ts +14 -0
- package/dist/reporter.d.ts +3 -0
- package/dist/resetServerState.d.ts +13 -0
- package/dist/resolveTestBaseUrl.d.ts +7 -0
- package/dist/resolveTestEnvironment.d.ts +21 -0
- package/dist/runAllTests.d.ts +88 -0
- package/dist/runAuthEnforcementTests.d.ts +17 -0
- package/dist/runContractTests.d.ts +22 -0
- package/dist/runCsrfEnforcementTests.d.ts +29 -0
- package/dist/runFuzzTests.d.ts +16 -0
- package/dist/runRateLimitTests.d.ts +38 -0
- package/dist/runRegisteredLayers.d.ts +24 -0
- package/dist/sampleInputFor.d.ts +13 -0
- package/dist/schemaSampleInput.d.ts +12 -0
- package/dist/socketProbeTransport.d.ts +9 -0
- package/dist/streamWatcher.d.ts +37 -0
- package/dist/testDataMarker.d.ts +2 -0
- package/dist/testLayerHelpers.d.ts +55 -0
- package/dist/testSocket.d.ts +58 -0
- package/dist/types.d.ts +46 -0
- package/dist/walkEndpoints.d.ts +6 -0
- package/docs/auth-tests.md +4 -223
- package/docs/contract-tests.md +11 -307
- package/docs/extension-hooks.md +4 -467
- package/docs/fuzz-tests.md +4 -245
- package/docs/rate-limit-tests.md +9 -261
- package/package.json +5 -5
- package/CLAUDE.md +0 -117
package/AGENTS.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @luckystack/test-runner — agent guidance
|
|
2
|
+
|
|
3
|
+
Use this package to test a running LuckyStack server against generated route contracts and security guards. It cannot substitute for project-specific business, role or tenant-isolation tests. See `README.md` for a minimal invocation and `docs/` for each layer's intent.
|
|
4
|
+
|
|
5
|
+
Never expose the test-reset endpoint in production or without its token; reset affects session/rate-limit state and is not an application-database reset. Load environment files and resolve configured secret pointers in the test process before importing modules that construct clients: resolution in the server process does not propagate to the test runner. An auth guard rejection must be established before interpreting a rate-limit probe. Fuzz tests establish a shaped error response, not an absence of server errors; run expensive fuzz sweeps outside the fast PR gate.
|
|
6
|
+
|
|
7
|
+
The runner must never test a route over a different path than real clients use, nor change server state its own run depends on (ADR 0064). Probes follow the project's `transport.invocation` (CSRF probes stay on HTTP, since CSRF is an HTTP-only guard); GET payloads use the same query-string encoding as the real client (ADR 0062). The runner's own resets are always scoped to rate limits, so an authenticated sweep keeps its session; sockets it opens on the caller's session close as an intentional disconnect so presence does not log that session out. Reset targets live in one list in `@luckystack/core` shared with the server.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to `@luckystack/test-runner` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.11.0] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Type declarations are emitted per source file by TypeScript 7 (`tsc -b`) instead of one bundled `.d.ts` per entry, so the framework build no longer runs package by package. The exported types are unchanged; relative imports inside the declarations carry `.js`, so `moduleResolution: "node16"`/`"nodenext"` projects resolve them as before.
|
|
13
|
+
- Package design guidance now ships as `AGENTS.md`, not `CLAUDE.md`. Handwritten docs focus on rationale and safety; exact API contracts remain in installed declarations/source.
|
|
14
|
+
|
|
8
15
|
## [0.10.2] - 2026-10-09
|
|
9
16
|
|
|
10
17
|
### Added
|
package/README.md
CHANGED
|
@@ -1,88 +1,33 @@
|
|
|
1
1
|
# @luckystack/test-runner
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generated-route contract, auth, rate-limit, CSRF and crash-resistance checks for a running LuckyStack server, over both API and sync routes. The checks probe each route over the transport real clients use, so a green sweep says something about the path users actually hit (ADR 0064).
|
|
4
4
|
|
|
5
|
-
## Install
|
|
5
|
+
## Install and run
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install --save-dev @luckystack/test-runner zod
|
|
8
|
+
npm install --save-dev @luckystack/test-runner zod socket.io-client
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Run all four layers against a running server:
|
|
11
|
+
Start from the scaffolded project's `scripts/testAll.ts` and its test command; consult the installed types for a custom harness. A minimal entry passes the generated maps for **both** route kinds and the lazy config loader:
|
|
14
12
|
|
|
15
13
|
```ts
|
|
16
|
-
import {
|
|
17
|
-
runAllTests,
|
|
18
|
-
resolveTestBaseUrl,
|
|
19
|
-
logRunAllSummary,
|
|
20
|
-
} from '@luckystack/test-runner';
|
|
21
|
-
import { apiInputSchemas, syncInputSchemas } from './src/_sockets/apiInputSchemas.generated';
|
|
22
|
-
import { apiMetaMap, apiMethodMap, syncMetaMap } from './src/_sockets/apiTypes.generated';
|
|
23
|
-
import { ports } from './config.ports';
|
|
24
|
-
|
|
25
|
-
const baseUrl = resolveTestBaseUrl({ fallbackUrl: `http://localhost:${ports.backend}` });
|
|
26
14
|
const summary = await runAllTests({
|
|
27
|
-
apiMethodMap,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
syncMetaMap, // omit and sync routes are not swept (a warning says so)
|
|
31
|
-
syncInputSchemas,
|
|
32
|
-
baseUrl,
|
|
33
|
-
// Loaded only after .env/.env.local. If config.secretManager.url is set,
|
|
34
|
-
// pointers are resolved in THIS process before Layer-5 tests touch Prisma.
|
|
15
|
+
apiMethodMap, apiMetaMap, apiInputSchemas,
|
|
16
|
+
syncMetaMap, syncInputSchemas, // omit and sync routes are not swept (a warning says so)
|
|
17
|
+
baseUrl: resolveTestBaseUrl({ fallbackUrl: `http://localhost:${ports.backend}` }),
|
|
35
18
|
loadProjectConfig: async () => (await import('./config')).default,
|
|
36
19
|
});
|
|
37
20
|
logRunAllSummary(summary);
|
|
38
21
|
```
|
|
39
22
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`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.
|
|
43
|
-
|
|
44
|
-
## Layers
|
|
45
|
-
|
|
46
|
-
| Layer | What it asserts |
|
|
47
|
-
| --- | --- |
|
|
48
|
-
| **Contract** | A Zod-valid input payload reaches `main` and returns `{ status: 'success' }` (or a typed failure status). Catches schema/route mismatches. |
|
|
49
|
-
| **Auth enforcement** | Endpoints flagged `auth.login: true` reject unauthenticated requests with the framework's standard 401 shape. |
|
|
50
|
-
| **Rate limit** | Endpoints with `rateLimit: <N>` reject the `(N + 1)`th request inside the window. |
|
|
51
|
-
| **Fuzz** | Bad/random inputs never crash the server (still returns a typed error response, never 500 with a stack). |
|
|
52
|
-
|
|
53
|
-
The fuzz layer is intentionally exhaustive and slow — wire it into nightly CI rather than every PR.
|
|
54
|
-
|
|
55
|
-
## Server hooks it depends on
|
|
56
|
-
|
|
57
|
-
- `/_test/reset` endpoint — served by `@luckystack/server` only when `NODE_ENV` is exactly `development` or `test`, and always gated by a non-empty `TEST_RESET_TOKEN`. `resetServerState` clears framework rate-limit state plus the default Redis session/active-user namespaces, or only the targets passed as `only` (the runner's own resets clear rate limits only, so an authenticated sweep keeps its session); it does not clear application database rows or custom `SessionAdapter` stores.
|
|
58
|
-
- `apiMethodMap.generated.ts` — produced by `@luckystack/devkit` from your `_api/*` files. Defaults are read via `getApiMethodMapPath()` from `@luckystack/core`.
|
|
59
|
-
|
|
60
|
-
## Public API
|
|
61
|
-
|
|
62
|
-
| Export | Purpose |
|
|
63
|
-
| --- | --- |
|
|
64
|
-
| `walkEndpoints(apiMethodMap)` | Returns `EndpointDescriptor[]` from the generated map. |
|
|
65
|
-
| `runContractCheck(input)` / `runContractTests(input)` | Single endpoint / full sweep. |
|
|
66
|
-
| `runAuthEnforcementCheck(input)` / `runAuthEnforcementTests(input)` | Auth layer. |
|
|
67
|
-
| `runRateLimitCheck(input)` / `runRateLimitTests(input)` | Rate-limit layer. |
|
|
68
|
-
| `runFuzzCheck(input)` / `runFuzzTests(input)` | Fuzz layer. |
|
|
69
|
-
| `resetServerState(input)` | POST to `/_test/reset`. |
|
|
70
|
-
| `resolveTestEnvironment({ loadProjectConfig? })` | Load env layers and optional secret-manager pointers for the test process; `runAllTests` calls it automatically. |
|
|
71
|
-
| `sampleSchemaInput(schema)` | Generate a Zod-valid sample payload. |
|
|
72
|
-
| `logContractResult` / `logContractSummary` | Pretty-printers. |
|
|
73
|
-
|
|
74
|
-
Types: `EndpointDescriptor`, `HttpMethod`, `ContractCheckResult`, `RunContractSummary`, plus per-layer input types.
|
|
75
|
-
|
|
76
|
-
## Related architecture docs
|
|
23
|
+
`runAllTests` writes the results file itself; an entry script should not write its own copy. A route filter is applied before any request is sent, so filtered-out routes are never called — not merely left out of the report.
|
|
77
24
|
|
|
78
|
-
|
|
79
|
-
- [`docs/ARCHITECTURE_PACKAGING.md`](../../docs/ARCHITECTURE_PACKAGING.md) — how generated maps are emitted per preset.
|
|
25
|
+
**Upgrading an existing entry script to 0.10.2:** import `syncMetaMap` / `syncInputSchemas` from the generated files and pass them, and remove any manual write of the summary file. In a socket-mode project, Layer-5 `ctx.callSync` now runs over Socket.io; a test that depended on HTTP-only behaviour must request HTTP for that call. See the package changelog for the full list.
|
|
80
26
|
|
|
81
|
-
##
|
|
27
|
+
## Safety
|
|
82
28
|
|
|
83
|
-
-
|
|
84
|
-
- Optional peers: `@luckystack/login`, `@luckystack/secret-manager` (loaded only for their corresponding features).
|
|
29
|
+
A test process must load local environment layers and resolve configured secret pointers **before** importing database-backed tests. The server resolving its own secrets does not resolve the separate runner process. Never run against production or expose the test reset route there. The reset route requires a non-empty token in development/test and affects framework state, not your application database or custom session storage. The runner's own resets clear rate limits only, so an authenticated sweep keeps its session; see [rate-limit tests](./docs/rate-limit-tests.md).
|
|
85
30
|
|
|
86
|
-
|
|
31
|
+
An authenticated sweep calls every route with its own session, including routes that destroy it (logout, account deletion, session revoke); skip those routes or every later layer runs logged out.
|
|
87
32
|
|
|
88
|
-
|
|
33
|
+
The built-in checks are not a substitute for project-specific role, tenant or business assertions. Run the slower fuzz layer separately from fast PR checks. A fuzz pass means responses keep the framework envelope, **not** that no HTTP 5xx occurred; see [fuzz tests](./docs/fuzz-tests.md).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContractCheckResult, EndpointDescriptor } from './types.js';
|
|
2
|
+
import type { ProbeTransport } from './probeRequest.js';
|
|
3
|
+
export interface AuthEnforcementCheckInput {
|
|
4
|
+
endpoint: EndpointDescriptor;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
inputFor?: (endpoint: EndpointDescriptor) => unknown;
|
|
7
|
+
/** Per-call request timeout in ms. Defaults to 5000. */
|
|
8
|
+
requestTimeoutMs?: number;
|
|
9
|
+
/** How the probe reaches the server. Defaults to HTTP. */
|
|
10
|
+
transport?: ProbeTransport;
|
|
11
|
+
}
|
|
12
|
+
export declare const runAuthEnforcementCheck: (input: AuthEnforcementCheckInput) => Promise<ContractCheckResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ContractCheckResult, EndpointDescriptor } from './types.js';
|
|
2
|
+
import type { ProbeTransport } from './probeRequest.js';
|
|
3
|
+
export interface ContractCheckInput {
|
|
4
|
+
endpoint: EndpointDescriptor;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/**
|
|
7
|
+
* Optional input factory. Defaults to `{}`. When an endpoint requires a
|
|
8
|
+
* specific shape to not crash, callers can override per-endpoint here.
|
|
9
|
+
*/
|
|
10
|
+
inputFor?: (endpoint: EndpointDescriptor) => unknown;
|
|
11
|
+
/**
|
|
12
|
+
* Optional headers (auth tokens, cookies). Applied to every request.
|
|
13
|
+
*/
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Per-call request timeout in ms. Defaults to 5000. Bump for slow endpoints
|
|
17
|
+
* (AI calls, large reports) where the framework default would false-fail
|
|
18
|
+
* the contract check.
|
|
19
|
+
*/
|
|
20
|
+
requestTimeoutMs?: number;
|
|
21
|
+
/** How the probe reaches the server. Defaults to HTTP. */
|
|
22
|
+
transport?: ProbeTransport;
|
|
23
|
+
}
|
|
24
|
+
export declare const runContractCheck: (input: ContractCheckInput) => Promise<ContractCheckResult>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ContractCheckResult, EndpointDescriptor } from './types.js';
|
|
2
|
+
import type { ProbeTransport } from './probeRequest.js';
|
|
3
|
+
export interface CsrfEnforcementCheckInput {
|
|
4
|
+
endpoint: EndpointDescriptor;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/**
|
|
7
|
+
* A valid session Cookie header (`<name>=<token>`). The probe deliberately
|
|
8
|
+
* carries it so the request passes the auth guard and reaches the CSRF check.
|
|
9
|
+
*/
|
|
10
|
+
authCookie: string;
|
|
11
|
+
inputFor?: (endpoint: EndpointDescriptor) => unknown;
|
|
12
|
+
/** Per-call request timeout in ms. Defaults to 5000. */
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
/** Error code expected on CSRF rejection. Defaults to `auth.csrfMismatch`. */
|
|
15
|
+
expectedErrorCode?: string;
|
|
16
|
+
/** HTTP status expected on CSRF rejection. Defaults to 403; pass `false` to skip. */
|
|
17
|
+
expectedHttpStatus?: number | false;
|
|
18
|
+
/**
|
|
19
|
+
* HTTP transport the probe uses (e.g. one carrying the sync `receiver`).
|
|
20
|
+
* CSRF is an HTTP-only guard, so a non-HTTP transport makes the check skip.
|
|
21
|
+
* Defaults to plain HTTP.
|
|
22
|
+
*/
|
|
23
|
+
transport?: ProbeTransport;
|
|
24
|
+
}
|
|
25
|
+
export declare const runCsrfEnforcementCheck: (input: CsrfEnforcementCheckInput) => Promise<ContractCheckResult>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getPrismaClient } from '@luckystack/core';
|
|
2
|
+
import { type ResolveTestEnvironmentInput } from './resolveTestEnvironment.js';
|
|
3
|
+
import { type StreamChunkFrame, type StreamWatcher } from './streamWatcher.js';
|
|
4
|
+
import type { ApiMethodMap } from './types.js';
|
|
5
|
+
export interface CustomTestCase {
|
|
6
|
+
name: string;
|
|
7
|
+
run: (ctx: TestContext) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Mark this case as a KNOWN, accepted failure (e.g. a documented bug not
|
|
10
|
+
* yet fixed, or a scenario the current implementation cannot satisfy). The
|
|
11
|
+
* string is the reason shown in the report. A case marked `expectedToFail`
|
|
12
|
+
* that throws is reported as `xfail` — counted separately, NOT a red
|
|
13
|
+
* failure. If it unexpectedly PASSES it is reported as `xpass` so the stale
|
|
14
|
+
* marker can be removed. Leave undefined for normal cases.
|
|
15
|
+
*/
|
|
16
|
+
expectedToFail?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CustomTestResult {
|
|
19
|
+
routePath: string;
|
|
20
|
+
caseName: string;
|
|
21
|
+
status: 'pass' | 'fail' | 'xfail' | 'xpass';
|
|
22
|
+
durationMs: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
/** Documented reason when the case carried an `expectedToFail` marker. */
|
|
25
|
+
expectedToFail?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RunCustomTestsSummary {
|
|
28
|
+
total: number;
|
|
29
|
+
passed: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
/** Cases marked `expectedToFail` that failed as expected (known issues). */
|
|
32
|
+
xfailed: number;
|
|
33
|
+
/** Cases marked `expectedToFail` that unexpectedly passed (remove the marker). */
|
|
34
|
+
xpassed: number;
|
|
35
|
+
results: CustomTestResult[];
|
|
36
|
+
}
|
|
37
|
+
export interface RunCustomTestsInput {
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
/** Optional cookie name for the session token. Defaults to the project's configured cookie. */
|
|
40
|
+
sessionCookieName?: string;
|
|
41
|
+
/** Filter to a subset of routes — substring match against `<page>/<name>/<version>`. */
|
|
42
|
+
filter?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Generated `apiMethodMap` so the harness sends each API route its declared
|
|
45
|
+
* HTTP method (e.g. logout is DELETE). Falls back to POST when absent.
|
|
46
|
+
*/
|
|
47
|
+
apiMethodMap?: ApiMethodMap;
|
|
48
|
+
/**
|
|
49
|
+
* Lazy consumer config for direct Layer-5 runs. Required so the public path
|
|
50
|
+
* cannot silently skip a configured secret manager; `runAllTests` prepares
|
|
51
|
+
* the environment separately before using the internal Layer-5 entrypoint.
|
|
52
|
+
*/
|
|
53
|
+
loadProjectConfig: NonNullable<ResolveTestEnvironmentInput['loadProjectConfig']>;
|
|
54
|
+
onResult?: (result: CustomTestResult) => void;
|
|
55
|
+
}
|
|
56
|
+
type PreparedRunCustomTestsInput = Omit<RunCustomTestsInput, 'loadProjectConfig'>;
|
|
57
|
+
interface DiscoveredTestFile {
|
|
58
|
+
/** Absolute path to the test file. */
|
|
59
|
+
filePath: string;
|
|
60
|
+
/** `api` or `sync` — which transport this route uses. */
|
|
61
|
+
kind: 'api' | 'sync';
|
|
62
|
+
/** Route descriptor parsed from the filename + directory location. */
|
|
63
|
+
route: {
|
|
64
|
+
page: string;
|
|
65
|
+
name: string;
|
|
66
|
+
version: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export declare const discoverCustomTestFiles: (srcDir?: string) => DiscoveredTestFile[];
|
|
70
|
+
export interface CallSyncOptions {
|
|
71
|
+
/** Room the sync targets (the server requires one). */
|
|
72
|
+
receiver?: string;
|
|
73
|
+
/** Skip the sender's own socket in the fan-out (socket transport). */
|
|
74
|
+
ignoreSelf?: boolean;
|
|
75
|
+
/** Force a transport. Defaults to the project's `transport.invocation`. */
|
|
76
|
+
transport?: 'socket' | 'http';
|
|
77
|
+
/** Socket transport: join `receiver` before sending. Default `true`. */
|
|
78
|
+
joinRoom?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface TestContext {
|
|
81
|
+
/**
|
|
82
|
+
* Invoke the route under test. Page / name / version are baked in.
|
|
83
|
+
*
|
|
84
|
+
* `input` travels exactly like the real client sends it: as the JSON body
|
|
85
|
+
* for POST / PUT / DELETE routes, and as the `__luckystack_data` query-string
|
|
86
|
+
* value for GET routes (a GET cannot carry a body). Either way the server
|
|
87
|
+
* validates it as the route's `data`, so a GET route with required fields
|
|
88
|
+
* receives them.
|
|
89
|
+
*/
|
|
90
|
+
callApi: <TInput = unknown, TOutput = unknown>(input: TInput) => Promise<TOutput>;
|
|
91
|
+
/**
|
|
92
|
+
* Invoke the sync route under test over the SAME transport the project's
|
|
93
|
+
* real client uses (`transport.invocation`): Socket.io for `'socket'` (the
|
|
94
|
+
* default), the `/sync/*` HTTP route for `'routed-http'`. Override per call
|
|
95
|
+
* with `opts.transport`. Over the socket, the case's session socket first
|
|
96
|
+
* joins `opts.receiver` (like a real client joins its room before syncing);
|
|
97
|
+
* pass `joinRoom: false` to test a non-member.
|
|
98
|
+
*/
|
|
99
|
+
callSync: <TInput = unknown, TOutput = unknown>(input: TInput, opts?: CallSyncOptions) => Promise<TOutput>;
|
|
100
|
+
/**
|
|
101
|
+
* Open a second socket connection that joins `roomCode` as a pure
|
|
102
|
+
* subscriber and exposes the incoming chunk stream for assertions.
|
|
103
|
+
*
|
|
104
|
+
* The route is auto-derived from the test file location, so a watcher
|
|
105
|
+
* only surfaces chunks for the route this test belongs to. Multiple
|
|
106
|
+
* watchers per test are allowed; all are auto-closed when the test
|
|
107
|
+
* case finishes (whether it passes or throws).
|
|
108
|
+
*/
|
|
109
|
+
watchStream: <TChunk extends StreamChunkFrame = StreamChunkFrame>(roomCode: string) => Promise<StreamWatcher<TChunk>>;
|
|
110
|
+
session: {
|
|
111
|
+
login: (user?: {
|
|
112
|
+
email?: string;
|
|
113
|
+
id?: string;
|
|
114
|
+
name?: string;
|
|
115
|
+
}) => Promise<{
|
|
116
|
+
token: string;
|
|
117
|
+
userId: string;
|
|
118
|
+
}>;
|
|
119
|
+
logout: () => Promise<void>;
|
|
120
|
+
current: () => {
|
|
121
|
+
token: string | null;
|
|
122
|
+
userId: string | null;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
prisma: ReturnType<typeof getPrismaClient>;
|
|
126
|
+
expect: TestExpect;
|
|
127
|
+
}
|
|
128
|
+
export interface TestExpect {
|
|
129
|
+
eq: <T>(actual: T, expected: T, message?: string) => void;
|
|
130
|
+
ok: (value: unknown, message?: string) => void;
|
|
131
|
+
throws: (fn: () => Promise<unknown>, message?: string) => Promise<Error>;
|
|
132
|
+
matches: (value: string, pattern: RegExp, message?: string) => void;
|
|
133
|
+
}
|
|
134
|
+
interface TestSessionState {
|
|
135
|
+
token: string | null;
|
|
136
|
+
userId: string | null;
|
|
137
|
+
csrfToken: string | null;
|
|
138
|
+
lastResponse: unknown;
|
|
139
|
+
}
|
|
140
|
+
/** @internal Exported for the unit test only; consumers use `TestContext.callApi`. */
|
|
141
|
+
export declare const buildCallApi: (baseUrl: string, routePath: string, state: TestSessionState, cookieName: string, method: string) => TestContext['callApi'];
|
|
142
|
+
/** @internal Environment is already prepared by `runAllTests`. */
|
|
143
|
+
export declare const runCustomTestsAfterEnvironmentPrepared: (input: PreparedRunCustomTestsInput) => Promise<RunCustomTestsSummary>;
|
|
144
|
+
export declare const runCustomTests: (input: RunCustomTestsInput) => Promise<RunCustomTestsSummary>;
|
|
145
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface TestLayerInput {
|
|
2
|
+
/** Resolved route name (`api/billing/getInvoice/v1` or `sync/chat/send/v1`). */
|
|
3
|
+
endpoint: string;
|
|
4
|
+
/** Method when applicable, undefined for sync endpoints. */
|
|
5
|
+
method?: string;
|
|
6
|
+
/** Bearer token / session token to use for auth-aware checks. */
|
|
7
|
+
authToken?: string;
|
|
8
|
+
/**
|
|
9
|
+
* A Zod-valid sample `data` payload for this route — generated from the
|
|
10
|
+
* route's input schema when `runRegisteredLayers` received the generated
|
|
11
|
+
* schemas (or an `inputFor`), `{}` otherwise. The same payload the built-in
|
|
12
|
+
* contract sweep sends, so a layer can exercise the route past validation.
|
|
13
|
+
*/
|
|
14
|
+
sampleInput?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface TestLayerResult {
|
|
17
|
+
passed: boolean;
|
|
18
|
+
/** Short reason key — i18n-style errorCode when failed. */
|
|
19
|
+
message?: string;
|
|
20
|
+
/** Optional extras for the reporter (latency, response sample, ...). */
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface TestLayer {
|
|
24
|
+
name: string;
|
|
25
|
+
run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;
|
|
26
|
+
}
|
|
27
|
+
export interface TestFixture<TPayload = unknown> {
|
|
28
|
+
valid: TPayload[];
|
|
29
|
+
invalid: TPayload[];
|
|
30
|
+
}
|
|
31
|
+
export interface TestResult {
|
|
32
|
+
layer: string;
|
|
33
|
+
endpoint: string;
|
|
34
|
+
passed: boolean;
|
|
35
|
+
message?: string;
|
|
36
|
+
durationMs?: number;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface TestSummary {
|
|
40
|
+
totalLayers: number;
|
|
41
|
+
totalEndpoints: number;
|
|
42
|
+
passed: number;
|
|
43
|
+
failed: number;
|
|
44
|
+
results: TestResult[];
|
|
45
|
+
}
|
|
46
|
+
export interface TestReporter {
|
|
47
|
+
/** Called once per individual TestResult as the runner progresses. */
|
|
48
|
+
onResult?: (result: TestResult) => void | Promise<void>;
|
|
49
|
+
/** Called once at the end with the aggregated summary. */
|
|
50
|
+
onSummary?: (summary: TestSummary) => void | Promise<void>;
|
|
51
|
+
/** Optional webhook the runner POSTs the summary to. */
|
|
52
|
+
webhookUrl?: string;
|
|
53
|
+
webhookAuth?: {
|
|
54
|
+
type: 'bearer';
|
|
55
|
+
token: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register a custom test layer. Re-registering the same name replaces
|
|
60
|
+
* the previous entry. Use for CORS enforcement, business-rule checks,
|
|
61
|
+
* custom auth schemes, multi-tenant isolation, GDPR data-flow checks.
|
|
62
|
+
*/
|
|
63
|
+
export declare const registerTestLayer: (layer: TestLayer) => void;
|
|
64
|
+
export declare const listTestLayers: () => TestLayer[];
|
|
65
|
+
/**
|
|
66
|
+
* Register a realistic payload fixture for a specific type key. The fuzz
|
|
67
|
+
* layer prefers these over schema-random generated inputs when a fixture
|
|
68
|
+
* exists for the endpoint's input type.
|
|
69
|
+
*
|
|
70
|
+
* `valid` payloads are used to verify happy-path behavior; `invalid`
|
|
71
|
+
* payloads probe the validator's error responses.
|
|
72
|
+
*/
|
|
73
|
+
export declare const registerTestFixture: <TPayload = unknown>(typeKey: string, fixture: TestFixture<TPayload>) => void;
|
|
74
|
+
export declare const getTestFixture: (typeKey: string) => TestFixture | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Register a test reporter. Set `null` to unregister. The runner calls
|
|
77
|
+
* `onResult` per individual result as the run progresses and `onSummary`
|
|
78
|
+
* once at the end. When `webhookUrl` is set, the runner additionally
|
|
79
|
+
* POSTs the JSON-serialised summary to that URL.
|
|
80
|
+
*/
|
|
81
|
+
export declare const registerTestReporter: (newReporter: TestReporter | null) => void;
|
|
82
|
+
export declare const getTestReporter: () => TestReporter | null;
|
|
83
|
+
export declare const resetTestExtensionsForTests: () => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContractCheckResult, EndpointDescriptor } from './types.js';
|
|
2
|
+
import type { ProbeTransport } from './probeRequest.js';
|
|
3
|
+
export interface FuzzCheckInput {
|
|
4
|
+
endpoint: EndpointDescriptor;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
/** Per-call request timeout in ms. Defaults to 5000. */
|
|
8
|
+
requestTimeoutMs?: number;
|
|
9
|
+
/** How the probes reach the server. Defaults to HTTP. */
|
|
10
|
+
transport?: ProbeTransport;
|
|
11
|
+
}
|
|
12
|
+
export declare const runFuzzCheck: (input: FuzzCheckInput) => Promise<ContractCheckResult>;
|