@nifrajs/testing 1.6.0 → 1.9.1

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/README.md CHANGED
@@ -1,9 +1,78 @@
1
1
  # @nifrajs/testing
2
2
 
3
- Testing helpers for nifra apps. [`@nifrajs/client`](../client)'s **`testClient`** is already the typed,
4
- no-network in-process request client (nifra's supertest / `inject`). This package adds what it doesn't: a
5
- **cookie jar** and a **cookie-persisting `testSession`**, so a login → authenticated-request flow tests as
6
- easily as a single request.
3
+ Contract-derived adversarial testing and stateful in-process helpers for Nifra apps.
4
+
5
+ ## Contract laboratory
6
+
7
+ `assertAdversarialContract` turns the schemas already registered on an app into executable tests. It:
8
+
9
+ - synthesizes a valid **contract witness** from inspectable JSON Schema;
10
+ - makes small nested **hostile mutations** and sends only values the route's own Standard Schema
11
+ validator has proved invalid after HTTP transport normalization;
12
+ - expects rejection at the real request boundary (422 by default);
13
+ - executes a valid witness and validates the real success body against `response`;
14
+ - runs the same stable case IDs across a Bun/Node/Workers runtime matrix; and
15
+ - retains a replay seed and greedily shrinks unexpected acceptance failures.
16
+
17
+ ```ts
18
+ import { test } from "bun:test"
19
+ import { assertAdversarialContract } from "@nifrajs/testing"
20
+ import { app } from "../src/app"
21
+
22
+ test("the API contract withstands hostile inputs", async () => {
23
+ await assertAdversarialContract(app, {
24
+ seed: 73,
25
+ prepareRequest(request) {
26
+ const headers = new Headers(request.headers)
27
+ headers.set("authorization", "Bearer test-session")
28
+ return new Request(request, { headers })
29
+ },
30
+ })
31
+ })
32
+ ```
33
+
34
+ Nifra's `t` schemas carry JSON Schema, so witnesses are automatic. Standard Schema deliberately does
35
+ not require introspection; for an opaque Zod/Valibot/ArkType schema, provide only its known-good values:
36
+
37
+ ```ts
38
+ await assertAdversarialContract(app, {
39
+ witnesses: {
40
+ "POST /users/:id": {
41
+ params: { id: "user-1" },
42
+ body: { name: "Ada" },
43
+ query: { notify: "true" },
44
+ },
45
+ },
46
+ })
47
+ ```
48
+
49
+ The validator still decides which generated mutations are truly invalid; the supplied witness does not
50
+ weaken validation. A missing/invalid witness is a coverage gap, not a silent pass. Set
51
+ `requireCoverage: false` only when gaps are intentionally advisory.
52
+
53
+ To exercise adapter parity, pass runtime fetch targets. The reflected `app` remains the one source of
54
+ the contract:
55
+
56
+ ```ts
57
+ const report = await assertAdversarialContract(app, {
58
+ runtimes: [
59
+ { name: "bun", fetch: (request) => bunApp.fetch(request) },
60
+ { name: "node", fetch: (request) => nodeAdapter.fetch(request) },
61
+ { name: "worker", fetch: (request) => worker.fetch(request, env) },
62
+ ],
63
+ })
64
+ ```
65
+
66
+ `runAdversarialContract` returns the same structured report without throwing. Every failure includes
67
+ `replay: { seed, caseId, runtime }`; rerun one case with `only: caseId`. Contract runs execute one valid
68
+ request for each declared response schema, so use an isolated test app/database—never point the
69
+ laboratory at production.
70
+
71
+ ## Stateful sessions
72
+
73
+ [`@nifrajs/client`](../client)'s **`testClient`** is already the typed, no-network in-process request
74
+ client (Nifra's supertest / `inject`). `testSession` adds a **cookie jar**, so a login → authenticated
75
+ request flow tests as easily as a single request.
7
76
 
8
77
  ```ts
9
78
  import { testSession } from "@nifrajs/testing"
@@ -26,6 +95,8 @@ that every call carries and captures cookies via a shared jar.
26
95
 
27
96
  ## API
28
97
 
98
+ - `assertAdversarialContract(app, options?)` → green report or throws `AdversarialContractError`.
99
+ - `runAdversarialContract(app, options?)` → structured `{ ok, results, failures, gaps, seed }` report.
29
100
  - `testSession<App>(app, { origin?, cookies? })` → `{ client: Treaty<App>, cookies: CookieJar }`.
30
101
  - `cookieJar()` → `CookieJar` — `header()` · `applyTo(headers)` · `store(response)` · `set` · `get` · `clear` · `size`.
31
102
  Honours removal (`Max-Age=0` / past `Expires`); other cookie attributes are ignored (in-process, same-origin).
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Contract-derived adversarial tests.
3
+ *
4
+ * A contract witness is a request known to satisfy a route's input schemas. From that witness this
5
+ * module derives small hostile mutations, proves each mutation invalid with the route's own Standard
6
+ * Schema validator, and then drives it through one or more real fetch runtimes. Declared success
7
+ * responses are validated off the hot path as part of the same laboratory run.
8
+ */
9
+ import { type ReflectedRoute } from "@nifrajs/core/reflection";
10
+ type HeaderSource = ConstructorParameters<typeof Headers>[0];
11
+ export type ContractTarget = "body" | "query" | "response";
12
+ export type ContractCaseKind = "input-rejection" | "response-conformance";
13
+ /** Anything that exposes reflected routes and a Web-standard in-process fetch handler. */
14
+ export interface ContractTestApp {
15
+ routes(): readonly unknown[];
16
+ fetch(request: Request): Response | Promise<Response>;
17
+ }
18
+ /** A runtime target for the same generated contract cases (for example Bun, Node, and Workers). */
19
+ export interface ContractRuntime {
20
+ readonly name: string;
21
+ fetch(request: Request): Response | Promise<Response>;
22
+ }
23
+ /** A known-good request. Missing body/query values are synthesized from inspectable JSON Schema. */
24
+ export interface ContractWitness {
25
+ readonly params?: Readonly<Record<string, string>>;
26
+ readonly query?: unknown;
27
+ readonly body?: unknown;
28
+ readonly headers?: HeaderSource;
29
+ }
30
+ /** Stable context passed to request/rejection hooks. It contains no request payloads or secrets. */
31
+ export interface ContractCaseContext {
32
+ readonly seed: number;
33
+ readonly caseId: string;
34
+ readonly route: ReflectedRoute;
35
+ readonly runtime: string;
36
+ readonly kind: ContractCaseKind;
37
+ readonly target: ContractTarget;
38
+ readonly mutation?: string;
39
+ }
40
+ export interface AdversarialContractOptions {
41
+ /** Base origin for generated requests. Default `http://nifra.contract`. */
42
+ readonly origin?: string;
43
+ /** Replayable seed used for deterministic witness generation. */
44
+ readonly seed?: number;
45
+ /** Known-good request values keyed by `"METHOD /path"`. Required for opaque Standard Schemas. */
46
+ readonly witnesses?: Readonly<Record<string, ContractWitness>>;
47
+ /** Select a subset of reflected routes. */
48
+ readonly include?: (route: ReflectedRoute) => boolean;
49
+ /** Execute every case against these targets. Default: the supplied app, named `in-process`. */
50
+ readonly runtimes?: readonly ContractRuntime[];
51
+ /** Maximum validator-proven mutations per body/query schema. Default 32. */
52
+ readonly maxMutationsPerInput?: number;
53
+ /** Maximum serialized witness size. Default 256 KiB. */
54
+ readonly maxWitnessBytes?: number;
55
+ /** Statuses that prove an invalid input was rejected. Default `[422]`. */
56
+ readonly expectedValidationStatuses?: readonly number[];
57
+ /**
58
+ * Attach auth, tenant, or runtime-specific state. Return a new Request; the input Request is already
59
+ * fresh for this case and runtime.
60
+ */
61
+ readonly prepareRequest?: (request: Request, context: ContractCaseContext) => Request | Promise<Request>;
62
+ /** Override rejection semantics (for apps with a custom validation response). */
63
+ readonly isRejected?: (response: Response, context: ContractCaseContext) => boolean | Promise<boolean>;
64
+ /** Validate a real successful response for every declared response schema. Default true. */
65
+ readonly validateResponses?: boolean;
66
+ /** Fail the report when a contracted target cannot be exercised. Default true. */
67
+ readonly requireCoverage?: boolean;
68
+ /** Greedily minimize an invalid input that unexpectedly reaches the app. Default true. */
69
+ readonly shrinkFailures?: boolean;
70
+ /** Cap shrink probes for one failure. Default 12. */
71
+ readonly maxShrinkAttempts?: number;
72
+ /** Replay only these stable case IDs. Coverage gaps are advisory when this is set. */
73
+ readonly only?: string | readonly string[];
74
+ }
75
+ export type ContractCoverageGapCode = "NO_CONTRACT_TARGETS" | "NO_VALIDATOR" | "NO_WITNESS" | "INVALID_WITNESS" | "WITNESS_TOO_LARGE" | "UNSUPPORTED_BODY_METHOD" | "NO_REJECTED_MUTATION" | "NO_RUNTIME" | "INVALID_RUNTIME" | "CASE_NOT_FOUND";
76
+ export interface ContractCoverageGap {
77
+ readonly route?: string;
78
+ readonly target?: ContractTarget;
79
+ readonly code: ContractCoverageGapCode;
80
+ readonly message: string;
81
+ }
82
+ export interface ContractReplay {
83
+ readonly seed: number;
84
+ readonly caseId: string;
85
+ readonly runtime: string;
86
+ }
87
+ export interface AdversarialContractResult {
88
+ readonly id: string;
89
+ readonly route: string;
90
+ readonly runtime: string;
91
+ readonly kind: ContractCaseKind;
92
+ readonly target: ContractTarget;
93
+ readonly mutation?: string;
94
+ readonly ok: boolean;
95
+ readonly status?: number;
96
+ readonly message: string;
97
+ /** Number of successful greedy reductions applied to a failing hostile input. */
98
+ readonly shrinkSteps?: number;
99
+ readonly replay: ContractReplay;
100
+ }
101
+ export interface AdversarialContractReport {
102
+ readonly ok: boolean;
103
+ readonly seed: number;
104
+ readonly routeCount: number;
105
+ readonly targetCount: number;
106
+ readonly runtimeCount: number;
107
+ readonly results: readonly AdversarialContractResult[];
108
+ readonly failures: readonly AdversarialContractResult[];
109
+ readonly gaps: readonly ContractCoverageGap[];
110
+ readonly counts: {
111
+ readonly passed: number;
112
+ readonly failed: number;
113
+ readonly gaps: number;
114
+ };
115
+ }
116
+ export declare class AdversarialContractError extends Error {
117
+ readonly report: AdversarialContractReport;
118
+ constructor(report: AdversarialContractReport);
119
+ }
120
+ /**
121
+ * Execute contract-derived hostile inputs and declared-response conformance against a runtime matrix.
122
+ * Runtime/request failures are captured in the report; inspect `report.ok`, `failures`, and `gaps`
123
+ * (or use {@link assertAdversarialContract} for a throwing test assertion).
124
+ */
125
+ export declare function runAdversarialContract(app: ContractTestApp, options?: AdversarialContractOptions): Promise<AdversarialContractReport>;
126
+ /** Run the contract laboratory and throw an {@link AdversarialContractError} unless it is fully green. */
127
+ export declare function assertAdversarialContract(app: ContractTestApp, options?: AdversarialContractOptions): Promise<AdversarialContractReport>;
128
+ export {};
129
+ //# sourceMappingURL=adversarial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adversarial.d.ts","sourceRoot":"","sources":["../src/adversarial.ts"],"names":[],"mappings":"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"}