@aventara/testing 0.1.0-pilot.2 → 0.1.0-pilot.4
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/dist/adapters/adapter-factory.d.ts +1 -1
- package/dist/adapters/fake-adapter.d.ts +18 -4
- package/dist/protocol/corpus-contract.fixture.d.ts +17 -21
- package/dist/protocol/in-process.driver.d.ts +4 -6
- package/dist/protocol/per-route.driver.d.ts +5 -6
- package/dist/protocol/protocol-conformance.d.ts +15 -14
- package/dist/protocol/protocol-driver.d.ts +5 -8
- package/dist/protocol/protocol-fixture.d.ts +14 -11
- package/dist/protocol/protocol-fixtures.d.ts +5 -0
- package/package.json +2 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Adapter, AdapterModel } from "@aventara/core";
|
|
2
|
-
/**
|
|
2
|
+
/** Builds the Adapter under test; reusable adapter conformance suites take one. */
|
|
3
3
|
export type AdapterFactory<M extends AdapterModel = AdapterModel> = () => Adapter<M> | Promise<Adapter<M>>;
|
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
import type { Adapter, AdapterModel, AdapterOperation } from "@aventara/core";
|
|
2
|
+
/** Answers one operation the fake Adapter is handed. */
|
|
2
3
|
export type FakeAdapterExecute<M extends AdapterModel> = (operation: AdapterOperation<M>) => unknown | Promise<unknown>;
|
|
4
|
+
/** How a transaction ended: `"committed"` or `"rolled-back"`. */
|
|
3
5
|
export type FakeTransactionOutcome = "committed" | "rolled-back";
|
|
4
6
|
/** One settled transaction boundary: what ran inside it, and how it ended. */
|
|
5
7
|
export type FakeTransactionTrace<M extends AdapterModel> = {
|
|
8
|
+
/** The operations run inside the transaction, in order. */
|
|
6
9
|
readonly operations: readonly AdapterOperation<M>[];
|
|
10
|
+
/** How the transaction ended. */
|
|
7
11
|
readonly outcome: FakeTransactionOutcome;
|
|
8
12
|
};
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
11
|
-
* work rejects. Without it the fake Adapter records
|
|
12
|
-
* whatever the work wrote
|
|
14
|
+
* Rollback for state the test owns: `snapshot` when a transaction begins,
|
|
15
|
+
* `restore` when its work rejects. Without it, the fake Adapter records
|
|
16
|
+
* transaction boundaries but keeps whatever the work wrote.
|
|
13
17
|
*/
|
|
14
18
|
export type FakeAdapterRollback<S> = {
|
|
19
|
+
/** Captures the state before a transaction's work runs. */
|
|
15
20
|
readonly snapshot: () => S;
|
|
21
|
+
/** Puts the captured state back after the work rejects. */
|
|
16
22
|
readonly restore: (snapshot: S) => void;
|
|
17
23
|
};
|
|
24
|
+
/** Options for {@link createFakeAdapter}. */
|
|
18
25
|
export type FakeAdapterOptions<M extends AdapterModel, S = unknown> = {
|
|
26
|
+
/** The adapter model the fake Adapter reports. */
|
|
19
27
|
readonly model: M;
|
|
28
|
+
/** Answers each operation; without it, every operation resolves `undefined`. */
|
|
20
29
|
readonly execute?: FakeAdapterExecute<M>;
|
|
30
|
+
/** Makes transactions roll back the test's own state; see {@link FakeAdapterRollback}. */
|
|
21
31
|
readonly rollback?: FakeAdapterRollback<S>;
|
|
22
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The Adapter {@link createFakeAdapter} returns, with a record of every
|
|
35
|
+
* operation and transaction it ran.
|
|
36
|
+
*/
|
|
23
37
|
export type FakeAdapter<M extends AdapterModel = AdapterModel> = Adapter<M> & {
|
|
24
38
|
readonly operations: readonly AdapterOperation<M>[];
|
|
25
39
|
readonly transactions: readonly FakeTransactionTrace<M>[];
|
|
26
40
|
};
|
|
27
|
-
/**
|
|
41
|
+
/** A provider-free, in-memory Adapter for framework and conformance tests. */
|
|
28
42
|
export declare function createFakeAdapter<M extends AdapterModel, S = unknown>(options: FakeAdapterOptions<M, S>): FakeAdapter<M>;
|
|
@@ -3,14 +3,15 @@ import { Decimal } from "@aventara/core";
|
|
|
3
3
|
/**
|
|
4
4
|
* The corpus's adapter model:
|
|
5
5
|
*
|
|
6
|
-
* - `authors
|
|
7
|
-
*
|
|
8
|
-
* - `books
|
|
9
|
-
* ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope
|
|
10
|
-
* a computed `label` to it;
|
|
11
|
-
* - `secrets
|
|
12
|
-
* - `hiddenRes
|
|
13
|
-
* - `samples
|
|
6
|
+
* - `authors`: every standard operation; its `secret` field is hidden at
|
|
7
|
+
* client scope;
|
|
8
|
+
* - `books`: references `authors`, and deleting an author cascades into it
|
|
9
|
+
* ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope
|
|
10
|
+
* adds a computed `label` to it ({@link CORPUS_CLIENT_CONFIG});
|
|
11
|
+
* - `secrets`: its one operation restricted away at client scope;
|
|
12
|
+
* - `hiddenRes`: hidden at client scope;
|
|
13
|
+
* - `samples`: one field of each scalar whose wire form is not its runtime
|
|
14
|
+
* form (`bigint`, `datetime`, `decimal`, `bytes`).
|
|
14
15
|
*/
|
|
15
16
|
export declare const CORPUS_MODEL: {
|
|
16
17
|
readonly transactions: "interactive";
|
|
@@ -278,8 +279,7 @@ export declare const CORPUS_REFERENTIAL_ACTIONS: {
|
|
|
278
279
|
}];
|
|
279
280
|
};
|
|
280
281
|
/**
|
|
281
|
-
* The `where.id` a row sends to make the corpus Framework fail one way
|
|
282
|
-
* (families 10 and 12, "a fake adapter returning the code, labelled so"):
|
|
282
|
+
* The `where.id` a row sends to make the corpus Framework fail one way:
|
|
283
283
|
*
|
|
284
284
|
* - `A4000`, `A4001`, `A4002`, `A3000` — the client guard throws
|
|
285
285
|
* `FrameworkError("A4000")`, denies (`false`), throws
|
|
@@ -314,7 +314,7 @@ declare function corpusGuard(context: PipelineContext): boolean;
|
|
|
314
314
|
declare function corpusPipe(args: unknown): unknown;
|
|
315
315
|
/**
|
|
316
316
|
* The corpus's client-scope configuration: what the ClientContract hides or
|
|
317
|
-
* restricts, and `books.label
|
|
317
|
+
* restricts, and `books.label`, a VIRTUAL field added at client scope and
|
|
318
318
|
* computed on read from `id`.
|
|
319
319
|
*/
|
|
320
320
|
export declare const CORPUS_CLIENT_CONFIG: {
|
|
@@ -372,24 +372,20 @@ export declare const CORPUS_CLIENT_CONFIG: {
|
|
|
372
372
|
export declare const CORPUS_ENTRYPOINT = "/api";
|
|
373
373
|
/**
|
|
374
374
|
* The corpus ClientContract's hash: what a generated client for it sends as
|
|
375
|
-
* `Aventara-Contract-Hash`. A literal, as a generated client holds it
|
|
376
|
-
* `corpus-contract.spec.ts` fails, naming this constant, when the compiled
|
|
377
|
-
* contract moves.
|
|
375
|
+
* `Aventara-Contract-Hash`. A literal, as a generated client holds it.
|
|
378
376
|
*/
|
|
379
377
|
export declare const CORPUS_CLIENT_CONTRACT_HASH = "sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0";
|
|
380
378
|
/**
|
|
381
379
|
* The corpus ClientContract as `GET /_contract` serves it: its JCS-canonical
|
|
382
380
|
* bytes, `protocol.hash` included. A literal, as a host must serve it byte for
|
|
383
|
-
* byte
|
|
384
|
-
* contract moves.
|
|
381
|
+
* byte.
|
|
385
382
|
*/
|
|
386
383
|
export declare const CORPUS_CLIENT_CONTRACT_DOCUMENT = "{\"enums\":{},\"limits\":{\"maxBooleanNodes\":50,\"maxListLimit\":250,\"maxNestingDepth\":12,\"maxRequestBytes\":1048576,\"maxTransactionOperations\":20},\"protocol\":{\"hash\":\"sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0\",\"version\":1},\"resources\":{\"authors\":{\"fields\":{\"id\":{\"capabilities\":{\"filter\":[\"equals\"],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}},\"name\":{\"capabilities\":{\"create\":[],\"filter\":[\"equals\"],\"select\":true,\"update\":[]},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}}},\"identifiers\":[[\"id\"]],\"operations\":{\"create\":{\"count\":true,\"many\":true,\"one\":true},\"delete\":{\"count\":true,\"first\":true,\"many\":true,\"unique\":true},\"find\":{\"count\":true,\"first\":true,\"many\":true,\"unique\":true},\"update\":{\"count\":true,\"first\":true,\"many\":true,\"unique\":true},\"upsert\":{\"unique\":true}}},\"books\":{\"fields\":{\"authorId\":{\"capabilities\":{\"filter\":[\"equals\"],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}},\"id\":{\"capabilities\":{\"filter\":[\"equals\"],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}},\"label\":{\"capabilities\":{\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[\"COMPUTED_ON_READ\",\"VIRTUAL\"],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}}},\"identifiers\":[[\"id\"]],\"operations\":{\"find\":{\"many\":true}}},\"samples\":{\"fields\":{\"amount\":{\"capabilities\":{\"create\":[],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"decimal\"}},\"at\":{\"capabilities\":{\"create\":[],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"datetime\"}},\"blob\":{\"capabilities\":{\"create\":[],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"bytes\"}},\"count\":{\"capabilities\":{\"create\":[],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"bigint\"}},\"id\":{\"capabilities\":{\"filter\":[\"equals\"],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}}},\"identifiers\":[[\"id\"]],\"operations\":{\"create\":{\"one\":true},\"find\":{\"unique\":true}}},\"secrets\":{\"fields\":{\"id\":{\"capabilities\":{\"filter\":[\"equals\"],\"select\":true},\"kind\":\"scalar\",\"lifecycle\":[],\"list\":false,\"nullable\":false,\"type\":{\"scalar\":\"string\"}}},\"identifiers\":[[\"id\"]],\"operations\":{}}},\"scalars\":{\"bigint\":{\"builtin\":true},\"bytes\":{\"builtin\":true},\"datetime\":{\"builtin\":true},\"decimal\":{\"builtin\":true},\"string\":{\"builtin\":true}},\"transactions\":\"interactive\"}";
|
|
387
384
|
/**
|
|
388
|
-
* The hash of the corpus's
|
|
389
|
-
* configuration with `transactions: "none"` (
|
|
390
|
-
* A row targets it by sending this hash, exactly as a client generated
|
|
391
|
-
*
|
|
392
|
-
* literal, held to the compiled value by `corpus-contract.spec.ts`.
|
|
385
|
+
* The hash of the corpus's second ClientContract: the same model and
|
|
386
|
+
* configuration with `transactions: "none"` (the `/_transactions` absence
|
|
387
|
+
* rows). A row targets it by sending this hash, exactly as a client generated
|
|
388
|
+
* against it would.
|
|
393
389
|
*/
|
|
394
390
|
export declare const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:d333e8396b9bf9127e0859ed69b9874f230058a597e2129e6b9e9b380daef618";
|
|
395
391
|
/** The one record the corpus Adapter knows. */
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* proves is the framework's own answer, host-free; driver 2 must answer every row
|
|
8
|
-
* identically through a real server.
|
|
3
|
+
* The protocol hosted in process, with no HTTP stack: `/_contract` goes to
|
|
4
|
+
* `protocol.encodeContract`, `/_transactions` to `protocol.handleTransaction`,
|
|
5
|
+
* every other request to `protocol.handleOperation`, and the answer is written
|
|
6
|
+
* back as it is.
|
|
9
7
|
*/
|
|
10
8
|
export declare const inProcessDriver: AvProtocolDriver;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
3
|
+
* The protocol hosted in process as a host that mounts one route per
|
|
4
|
+
* `protocol.surface()` entry (each operation on `handleOperation`,
|
|
5
5
|
* `/_transactions` on `handleTransaction`, `/_contract` on `encodeContract`,
|
|
6
|
-
* matched by method and path
|
|
7
|
-
* everything it did not mount. `undefined` from the fallback is a path
|
|
8
|
-
* protocol, which this host answers with its own bodiless 404.
|
|
9
|
-
* must answer here exactly as through driver 1's catch-all.
|
|
6
|
+
* matched by method and path) and uses `protocol.answerAbsent` as the fallback
|
|
7
|
+
* for everything it did not mount. `undefined` from the fallback is a path
|
|
8
|
+
* outside the protocol, which this host answers with its own bodiless 404.
|
|
10
9
|
*/
|
|
11
10
|
export declare const perRouteDriver: AvProtocolDriver;
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
* `runAvProtocolConformance(driver)` — the
|
|
3
|
+
* `runAvProtocolConformance(driver)` — drives the protocol fixture corpus
|
|
4
|
+
* through one host.
|
|
4
5
|
*
|
|
5
|
-
* Every row a host answers is driven
|
|
6
|
+
* Every row a host answers is driven; rows whose `client.outcome` is
|
|
6
7
|
* `"transport-error"` describe what a client receives instead of a framework
|
|
7
8
|
* answer, so they are skipped. Each driven row runs against a fresh corpus
|
|
8
|
-
* Framework, through a fresh driver session, with one request
|
|
9
|
-
* Framework whose ClientContract hash the row's `Aventara-Contract-Hash`
|
|
10
|
-
*
|
|
11
|
-
* block:
|
|
9
|
+
* Framework, through a fresh driver session, with one request (the corpus
|
|
10
|
+
* Framework whose ClientContract hash the row's `Aventara-Contract-Hash`
|
|
11
|
+
* names; the main corpus contract for any other hash), and its answer is
|
|
12
|
+
* held to the row's `server` block:
|
|
12
13
|
*
|
|
13
14
|
* - `status`, exactly;
|
|
14
|
-
* - `code`: the body is a framework envelope
|
|
15
|
-
*
|
|
15
|
+
* - `code`: the body is a framework envelope carrying that code — or, for
|
|
16
|
+
* `null`, is none;
|
|
16
17
|
* - `causeKeys`: the exact sorted key set of `cause`;
|
|
17
18
|
* - `issues`: each issue's `code` and `path`, in order;
|
|
18
19
|
* - `adapterCalls`: the operations the corpus Adapter was handed, exactly;
|
|
19
|
-
* - `responseHeaders`: each named header present (names matched without
|
|
20
|
-
* case) with exactly that value — a host may add its own;
|
|
20
|
+
* - `responseHeaders`: each named header present (names matched without
|
|
21
|
+
* regard to case) with exactly that value — a host may add its own;
|
|
21
22
|
* - `bodyBytes`: the body text, exactly.
|
|
22
23
|
*
|
|
23
|
-
* Resolves one result per driven row, in corpus order, each with the
|
|
24
|
-
* found — none when the host conforms. A driver that throws is a
|
|
25
|
-
* row, not a rejection of the run.
|
|
26
|
-
* results with whatever runner
|
|
24
|
+
* Resolves one result per driven row, in corpus order, each with the
|
|
25
|
+
* mismatches found — none when the host conforms. A driver that throws is a
|
|
26
|
+
* mismatch of its row, not a rejection of the run. Works with any test
|
|
27
|
+
* runner: assert on the results with whatever runner you use.
|
|
27
28
|
*/
|
|
28
29
|
export declare function runAvProtocolConformance(driver: AvProtocolDriver): Promise<readonly {
|
|
29
30
|
readonly label: string;
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { Framework } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
3
|
/**
|
|
4
|
-
* One host under test, as
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Driver 1 (`in-process.driver.ts`) calls the bound protocol's shortcuts directly;
|
|
10
|
-
* a real host sends the request over its own stack and reads back what arrived.
|
|
11
|
-
* The run calls `close` once per row, after its one request.
|
|
4
|
+
* One host under test, as `runAvProtocolConformance` drives it: given the
|
|
5
|
+
* corpus's Framework, mount the protocol the way that host does, then answer
|
|
6
|
+
* each corpus request as that host would answer it over HTTP (status, headers
|
|
7
|
+
* and body text, as an `AvProtocolResponse`). The run calls `close` once per
|
|
8
|
+
* row, after its one request.
|
|
12
9
|
*/
|
|
13
10
|
export type AvProtocolDriver = (framework: Framework) => Promise<{
|
|
14
11
|
readonly send: (request: AvProtocolRequest) => Promise<AvProtocolResponse>;
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import type { OperationCode, ValidationCode } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolRequest } from "@aventara/core/protocol";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* One row of the protocol fixture corpus: the HTTP request a host receives
|
|
5
|
+
* (`request`), what the host must answer (`server`), and what a generated
|
|
6
|
+
* client must do with that answer (`client`).
|
|
5
7
|
*
|
|
6
|
-
* `request` is
|
|
7
|
-
*
|
|
8
|
-
* protocol: no second request shape exists for the corpus to drift from.
|
|
8
|
+
* `request` is an `AvProtocolRequest` from `@aventara/core/protocol`, exactly
|
|
9
|
+
* what a host hands the bound protocol.
|
|
9
10
|
*/
|
|
10
11
|
export type AvProtocolFixture = {
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* operation name (`find.many …`, `create.one …`) or with `transaction` for
|
|
14
|
-
* committed-plan row.
|
|
13
|
+
* The row's name. A row for a standard operation's success begins with that
|
|
14
|
+
* operation's name (`find.many …`, `create.one …`), or with `transaction` for
|
|
15
|
+
* the committed-plan row.
|
|
15
16
|
*/
|
|
16
17
|
readonly label: string;
|
|
17
18
|
/** Method, entrypoint-relative path, headers, and a raw or parsed body. */
|
|
18
19
|
readonly request: AvProtocolRequest;
|
|
20
|
+
/** What the host must answer. */
|
|
19
21
|
readonly server: {
|
|
20
22
|
/**
|
|
21
|
-
* The HTTP status. `0` only on a transport-error row
|
|
22
|
-
*
|
|
23
|
+
* The HTTP status. `0` only on a transport-error row, where no HTTP
|
|
24
|
+
* response arrived at all (the Fetch API's network-error status).
|
|
23
25
|
*/
|
|
24
26
|
readonly status: number;
|
|
25
27
|
/** `null`: the host produces no framework envelope at all. */
|
|
@@ -36,11 +38,12 @@ export type AvProtocolFixture = {
|
|
|
36
38
|
/** The exact body, where bytes matter. */
|
|
37
39
|
readonly bodyBytes?: string;
|
|
38
40
|
};
|
|
41
|
+
/** What a generated client must do with the answer. */
|
|
39
42
|
readonly client: {
|
|
40
43
|
/**
|
|
41
44
|
* What a generated client does with the answer. `"transport-error"`
|
|
42
|
-
* also marks a row no host produces
|
|
43
|
-
*
|
|
45
|
+
* also marks a row no host produces: a host driver filters on
|
|
46
|
+
* `client.outcome === "transport-error"` and does not drive those rows.
|
|
44
47
|
*/
|
|
45
48
|
readonly outcome: "resolve" | "framework-error" | "transport-error";
|
|
46
49
|
/** Present iff `outcome` is `"framework-error"`. */
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { AvProtocolFixture } from "./protocol-fixture.js";
|
|
2
|
+
/**
|
|
3
|
+
* The protocol fixture corpus: passive data rows, each an HTTP request, the
|
|
4
|
+
* answer a host must give and what a generated client must do with it. Drive
|
|
5
|
+
* them through a host with `runAvProtocolConformance`.
|
|
6
|
+
*/
|
|
2
7
|
export declare const AvProtocolFixtures: readonly AvProtocolFixture[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/testing",
|
|
3
|
-
"version": "0.1.0-pilot.
|
|
3
|
+
"version": "0.1.0-pilot.4",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Conformance and extension testing utilities for Aventara.",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"LICENSE-ADDITIONAL-PERMISSION.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@aventara/core": "0.1.0-pilot.
|
|
23
|
+
"@aventara/core": "0.1.0-pilot.4"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|