@aventara/testing 0.1.0-pilot.3 → 1.0.0-rc.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/CHANGELOG.md +159 -0
- package/README.md +51 -0
- package/dist/adapters/adapter-factory.d.ts +1 -1
- package/dist/adapters/fake-adapter.d.ts +20 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol/corpus-contract.fixture.d.ts +18 -21
- package/dist/protocol/corpus-contract.fixture.js +8 -1
- 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-corpus.d.ts +30 -0
- package/dist/protocol/protocol-corpus.js +6 -0
- 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/dist/protocol/protocol-fixtures.js +39 -9
- package/dist/protocol/protocol-fixtures.json-codec.d.ts +43 -0
- package/dist/protocol/protocol-fixtures.json-codec.js +89 -0
- package/fixtures/protocol-v1.json +136 -0
- package/package.json +11 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @aventara/testing
|
|
2
|
+
|
|
3
|
+
## 1.0.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- First release candidate of Aventara 1.0. The public API is now considered stable; changes before 1.0.0 are possible
|
|
8
|
+
based on feedback.
|
|
9
|
+
|
|
10
|
+
### Minor Changes
|
|
11
|
+
|
|
12
|
+
- 24ffbab: The protocol fixtures gain two transaction rows: a committed plan whose unique `where` and filter reference an earlier
|
|
13
|
+
step's result (`200 A1009`), and a reference in a `where` to a step that does not exist (`422 A2007` / `V1016`).
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 24ffbab: `@aventara/testing` loads from CommonJS: `require("@aventara/testing")` returns the same module `import` does (its
|
|
18
|
+
export gains a `default` condition, as core's and nest's have). The package stays ESM; Node's `require()` of an ES
|
|
19
|
+
module needs Node 22.18 or later, which the package already requires.
|
|
20
|
+
- 24ffbab: Each package ships its `CHANGELOG.md`, so the release notes are in the installed package beside the README.
|
|
21
|
+
- 24ffbab: New export `AvProtocolCorpus`: the protocol corpus's server side, for serving it through a client or host you are
|
|
22
|
+
writing. `await AvProtocolCorpus.framework()` gives a fresh corpus `Framework` (pass a row's `Aventara-Contract-Hash`
|
|
23
|
+
to get the contract that row targets), `AvProtocolCorpus.entrypoint` is where to mount it (`"/api"`, every row's path is
|
|
24
|
+
relative to it), and `AvProtocolCorpus.clientContractHash` is the hash a client generated against it sends. The README
|
|
25
|
+
has a section on serving the corpus through your own stack.
|
|
26
|
+
- 24ffbab: The protocol corpus now ships as plain JSON at `@aventara/testing/fixtures/protocol-v1.json`, for clients and hosts
|
|
27
|
+
written in any language: the protocol version, the entrypoint, each ClientContract the rows target (its hash and exact
|
|
28
|
+
`GET /_contract` bytes) and every row of `AvProtocolFixtures`. It is generated from the TypeScript corpus when the
|
|
29
|
+
package is built. The one body too large to write out (1 MiB + 1 byte) is a recipe: `prefix` + `repeat` × `times` +
|
|
30
|
+
`suffix`. The README describes the format.
|
|
31
|
+
- 384a50c: **Behaviour changes, transactions:** every issue about a transaction step's arguments now names where it is
|
|
32
|
+
in the plan, `["operations", i, "args", …]`, beside `cause.operation: i`. Before, the framework's own checks — a wrong
|
|
33
|
+
type, an unknown field or operator, a structural limit, a Json value out of its `AvZ` shape, a value that is not valid
|
|
34
|
+
JSON — answered `["arguments", …]` inside a plan while a bad `$ref`, a refused value and a step that depends on a
|
|
35
|
+
cascaded delete already answered `["operations", i, "args", …]`; now one plan uses one spelling, through
|
|
36
|
+
`framework.transaction`, `framework.executeTransaction` and `POST /_transactions` alike. A single call is unchanged
|
|
37
|
+
(`["arguments", …]`). If your code reads `issue.path` from a transaction's answer, drop the `"operations", i, "args"`
|
|
38
|
+
prefix to get the path inside that step's arguments. `@aventara/testing`'s protocol corpus row "a client-hidden
|
|
39
|
+
field — /_transactions" and `fixtures/protocol-v1.json` carry the new path.
|
|
40
|
+
- 032f792: **Behaviour changes, transactions:** an error about a transaction step itself — rather than its arguments — now
|
|
41
|
+
names the step too, so every path in a transaction's answer is a path through the request you sent: an unknown or
|
|
42
|
+
hidden Resource is at `["operations", i, "resource"]`, a fingerprint that does not match at
|
|
43
|
+
`["operations", i, "fingerprint"]`, a missing or unknown key on the step at `["operations", i, "<key>"]`, and a step
|
|
44
|
+
that is not an object at `["operations", i]`. An operation the step cannot run points at the key that is wrong:
|
|
45
|
+
`["operations", i, "family"]` when the family is not a string, not a standard family, or one the Resource offers no
|
|
46
|
+
variant of; `["operations", i, "variant"]` when the family is right and the variant is not (unknown, not offered or
|
|
47
|
+
restricted for it). Before, these answered `["resource"]`, `["operation"]`, `["fingerprint"]` and `["<key>"]`.
|
|
48
|
+
`cause.operation` and the outcome code are unchanged: an unknown Resource is still `A2001`, an unavailable operation
|
|
49
|
+
`A2002`. `framework.transaction([...])` refuses an entry that is not a handle, or one handle placed twice, at
|
|
50
|
+
`["operations", i]`. A single call is unchanged. `@aventara/testing`'s protocol corpus and `fixtures/protocol-v1.json`
|
|
51
|
+
carry the new paths.
|
|
52
|
+
- dbe6961: **Behaviour changes:** a single call to an operation a Resource does not offer now names the key of the
|
|
53
|
+
request that is wrong instead of `["operation"]`, which no request has: `["family"]` when the family is not a standard
|
|
54
|
+
one or the Resource offers none of its operations, `["variant"]` when the family is right and the variant is not. This
|
|
55
|
+
holds for `framework.execute` and for an HTTP route, whose `/_resources/:resource/:family/:variant` segments are named
|
|
56
|
+
by those same keys. The code is unchanged (`A2002` / `V1006`), and so is `["resource"]` for an unknown Resource.
|
|
57
|
+
`@aventara/testing`'s protocol corpus and `fixtures/protocol-v1.json` carry the new paths.
|
|
58
|
+
- Updated dependencies [24ffbab]
|
|
59
|
+
- Updated dependencies [24ffbab]
|
|
60
|
+
- Updated dependencies [24ffbab]
|
|
61
|
+
- Updated dependencies [24ffbab]
|
|
62
|
+
- Updated dependencies [24ffbab]
|
|
63
|
+
- Updated dependencies [24ffbab]
|
|
64
|
+
- Updated dependencies [24ffbab]
|
|
65
|
+
- Updated dependencies [24ffbab]
|
|
66
|
+
- Updated dependencies [24ffbab]
|
|
67
|
+
- Updated dependencies [24ffbab]
|
|
68
|
+
- Updated dependencies [24ffbab]
|
|
69
|
+
- Updated dependencies [24ffbab]
|
|
70
|
+
- Updated dependencies [a973e89]
|
|
71
|
+
- Updated dependencies [1e65e9a]
|
|
72
|
+
- Updated dependencies
|
|
73
|
+
- Updated dependencies [a973e89]
|
|
74
|
+
- Updated dependencies [a973e89]
|
|
75
|
+
- Updated dependencies [1e65e9a]
|
|
76
|
+
- Updated dependencies [1e65e9a]
|
|
77
|
+
- Updated dependencies [24ffbab]
|
|
78
|
+
- Updated dependencies [2d00ad7]
|
|
79
|
+
- Updated dependencies [384a50c]
|
|
80
|
+
- Updated dependencies [d43d1cc]
|
|
81
|
+
- Updated dependencies [3f98749]
|
|
82
|
+
- Updated dependencies [032f792]
|
|
83
|
+
- Updated dependencies [dbe6961]
|
|
84
|
+
- @aventara/core@1.0.0-rc.0
|
|
85
|
+
|
|
86
|
+
## 0.1.0-pilot.4
|
|
87
|
+
|
|
88
|
+
### Patch Changes
|
|
89
|
+
|
|
90
|
+
- f37dcd6: The documentation shipped with the package — the comments an editor shows on hover, and the README — now describes
|
|
91
|
+
what each export does, how to use it, what it returns and what it throws. Notes about how the package was built and
|
|
92
|
+
why are no longer part of what is published.
|
|
93
|
+
- Updated dependencies [f37dcd6]
|
|
94
|
+
- @aventara/core@0.1.0-pilot.4
|
|
95
|
+
|
|
96
|
+
## 0.1.0-pilot.3
|
|
97
|
+
|
|
98
|
+
### Patch Changes
|
|
99
|
+
|
|
100
|
+
- @aventara/core@0.1.0-pilot.3
|
|
101
|
+
|
|
102
|
+
## 0.1.0-pilot.2
|
|
103
|
+
|
|
104
|
+
### Patch Changes
|
|
105
|
+
|
|
106
|
+
- e69e88d: The published packages are written for the people who use them. The JavaScript ships without comments; the type
|
|
107
|
+
declarations keep their documentation, and it, the error messages, the protocol fixtures' labels and the comments in a
|
|
108
|
+
generated client no longer carry the project's internal development notes.
|
|
109
|
+
- ab58424: A pipe's output is checked against the server's own rules before it reaches the adapter. The request is validated
|
|
110
|
+
against the caller's Contract; after each pipe, the arguments are validated against the Core Contract (plus fields the
|
|
111
|
+
caller's scope adds), so a pipe may still write what the caller cannot — a field the client Contract hides, such as a
|
|
112
|
+
password hash — but not what the server cannot store. A refusal is the new internal code `A3004 PIPE_OUTPUT_INVALID` (HTTP
|
|
113
|
+
500). The caller receives only the generic `A server pipeline produced invalid arguments.`, like every internal
|
|
114
|
+
failure; the pipe (`pipes[i]` and its function name), the operation, the offending field and its V-code go to the
|
|
115
|
+
diagnostics sink as a `PipeOutputError`, and `@aventara/nest` writes them into its error log line. Nothing reaches the
|
|
116
|
+
adapter. This holds for operations and transaction steps alike (a plan answers with `cause.operation`). Generated
|
|
117
|
+
clients map `A3004` to `InternalError`; the protocol fixtures in `@aventara/testing` include an `A3004` row.
|
|
118
|
+
- Updated dependencies [c2a3b7c]
|
|
119
|
+
- Updated dependencies [e69e88d]
|
|
120
|
+
- Updated dependencies [ab58424]
|
|
121
|
+
- Updated dependencies [23937f8]
|
|
122
|
+
- @aventara/core@0.1.0-pilot.2
|
|
123
|
+
|
|
124
|
+
## 0.1.0-pilot.1
|
|
125
|
+
|
|
126
|
+
### Patch Changes
|
|
127
|
+
|
|
128
|
+
- a45f8a8: The `400 A2000` for a request without its identity headers now says which one: `cause.message` is "The
|
|
129
|
+
Aventara-Contract-Hash header is missing." (or "The Aventara-Protocol-Version header is missing.", or both, naming both),
|
|
130
|
+
instead of the generic "The request is malformed and cannot be interpreted." It names headers only and echoes no value.
|
|
131
|
+
The code, status and every other `A2000` are unchanged. The protocol fixtures in `@aventara/testing` check the new messages, with a row
|
|
132
|
+
for a request with neither header.
|
|
133
|
+
- 799346e: The package READMEs (each package's npm page) are rewritten for the people who use the packages. The Prisma 7
|
|
134
|
+
adapter's README now states the actual behaviour of a to-many relation or `$count` projected on
|
|
135
|
+
`update.many`/`create.many`: a validation refusal, `A2004` with `V1008`, before any statement runs (it said `A3001`).
|
|
136
|
+
Every install or run command names the `@pilot` dist-tag.
|
|
137
|
+
- Updated dependencies [a45f8a8]
|
|
138
|
+
- Updated dependencies [799346e]
|
|
139
|
+
- @aventara/core@0.1.0-pilot.1
|
|
140
|
+
|
|
141
|
+
## 0.1.0-pilot.0
|
|
142
|
+
|
|
143
|
+
### Minor Changes
|
|
144
|
+
|
|
145
|
+
- First pilot release of `@aventara/testing`: tools for testing an Aventara application, an adapter or a host.
|
|
146
|
+
|
|
147
|
+
- `createFakeAdapter` — an in-memory adapter, so a Framework can be built and exercised without a database. Its
|
|
148
|
+
discovered model is `FakeAdapter.model`.
|
|
149
|
+
- `AvProtocolFixtures` (each one an `AvProtocolFixture`) — the HTTP protocol's conformance fixtures: every outcome
|
|
150
|
+
code and success status, contract discovery (`GET /_contract`), missing routes and a stale client, argument
|
|
151
|
+
validation, body, media-type and method refusals, transactions (including client-scope rules on `/_transactions`),
|
|
152
|
+
wire scalars and request ids.
|
|
153
|
+
- `AvProtocolDriver` and `runAvProtocolConformance(driver)` — run the fixtures against your own host: the driver
|
|
154
|
+
mounts the protocol for a Framework and answers requests, and each fixture's mismatches are reported.
|
|
155
|
+
|
|
156
|
+
### Patch Changes
|
|
157
|
+
|
|
158
|
+
- Updated dependencies
|
|
159
|
+
- @aventara/core@0.1.0-pilot.0
|
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ from outside.
|
|
|
8
8
|
- `runAvProtocolConformance(driver)` — drives the protocol corpus (`AvProtocolFixtures`) through one host and reports,
|
|
9
9
|
per row, every mismatch with the expected status, code, cause, headers and body. A host supplies an
|
|
10
10
|
`AvProtocolDriver`: given a `Framework`, a session that sends one protocol request and returns the answer, and closes.
|
|
11
|
+
- `AvProtocolCorpus` — the corpus's server side, for serving it through your own client or host (below).
|
|
12
|
+
- `@aventara/testing/fixtures/protocol-v1.json` — the same corpus as plain JSON, for a client or host written in any
|
|
13
|
+
language (below).
|
|
11
14
|
|
|
12
15
|
It depends on `@aventara/core` only.
|
|
13
16
|
|
|
@@ -19,6 +22,54 @@ npm install --save-dev @aventara/testing
|
|
|
19
22
|
|
|
20
23
|
Node `^22.18.0 || >=24.2.0`. ES modules.
|
|
21
24
|
|
|
25
|
+
## Serving the corpus through your own stack
|
|
26
|
+
|
|
27
|
+
`runAvProtocolConformance` builds the corpus `Framework` itself. To serve it from a host or test bench of your own — to
|
|
28
|
+
point a client you are writing at it, say — take it from `AvProtocolCorpus`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { AvProtocolCorpus, AvProtocolFixtures } from "@aventara/testing";
|
|
32
|
+
|
|
33
|
+
const framework = await AvProtocolCorpus.framework();
|
|
34
|
+
// Mount `framework` under AvProtocolCorpus.entrypoint ("/api") with your host,
|
|
35
|
+
// then send each row's `request` and compare the answer with the row's `server`.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `framework(contractHash?)` — a fresh corpus `Framework`, backed by an in-memory adapter whose answers every row
|
|
39
|
+
states. Pass a row's `Aventara-Contract-Hash` to get the contract that row targets: a few rows target a second
|
|
40
|
+
contract without transactions, and every other hash (a stale one included) gets the main contract.
|
|
41
|
+
- `entrypoint` — where the corpus is mounted; every row's `path` is relative to it.
|
|
42
|
+
- `clientContractHash` — the main ClientContract's hash: what a client generated against it sends, and what
|
|
43
|
+
`GET /_contract` names.
|
|
44
|
+
|
|
45
|
+
A row whose `client.outcome` is `"transport-error"` describes an answer no host produces (a proxy's HTML page, a
|
|
46
|
+
dropped connection); skip it when serving, as the conformance run does. A row's `adapterCalls` counts the operations
|
|
47
|
+
the corpus adapter was handed, which only the conformance run can observe.
|
|
48
|
+
|
|
49
|
+
## The corpus as JSON
|
|
50
|
+
|
|
51
|
+
`@aventara/testing/fixtures/protocol-v1.json` holds the whole corpus for protocol version 1, generated from
|
|
52
|
+
`AvProtocolFixtures` when the package is built — every row, in the same order and with the same fields — so a client or
|
|
53
|
+
host written in another language can be held to the same answers:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"protocolVersion": 1,
|
|
58
|
+
"entrypoint": "/api",
|
|
59
|
+
"clientContractHash": "sha256:…",
|
|
60
|
+
"contracts": [{ "hash": "sha256:…", "document": "{\"enums\":{},…}" }, …],
|
|
61
|
+
"rows": [{ "label": "…", "request": { … }, "server": { … }, "client": { … } }, …]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `contracts` — each ClientContract a row targets: its hash and its exact `GET /_contract` bytes (`document`, a
|
|
66
|
+
string). The first is the main contract, `clientContractHash`; a row targets the other by sending its hash.
|
|
67
|
+
- `rows` — as `AvProtocolFixture` describes them. One body is too large to write out (one byte over the 1 MiB request
|
|
68
|
+
limit): its `request.body.content` is a recipe, `{ "prefix": "…", "repeat": "x", "times": 1048567, "suffix": "…" }`,
|
|
69
|
+
standing for `prefix` + `repeat` repeated `times` times + `suffix`. Every other `content` is the body's text.
|
|
70
|
+
|
|
71
|
+
From JavaScript: `require("@aventara/testing/fixtures/protocol-v1.json")`, or `import` it `with { type: "json" }`.
|
|
72
|
+
|
|
22
73
|
## Documentation
|
|
23
74
|
|
|
24
75
|
`@aventara/core`'s README is where to start (the framework, its adapters and hosts). `@aventara/nest` is held to this
|
|
@@ -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,43 @@
|
|
|
1
|
-
import type { Adapter, AdapterModel
|
|
1
|
+
import type { Adapter, AdapterModel } from "@aventara/core";
|
|
2
|
+
import type { AdapterOperation } from "@aventara/core/internal";
|
|
3
|
+
/** Answers one operation the fake Adapter is handed. */
|
|
2
4
|
export type FakeAdapterExecute<M extends AdapterModel> = (operation: AdapterOperation<M>) => unknown | Promise<unknown>;
|
|
5
|
+
/** How a transaction ended: `"committed"` or `"rolled-back"`. */
|
|
3
6
|
export type FakeTransactionOutcome = "committed" | "rolled-back";
|
|
4
7
|
/** One settled transaction boundary: what ran inside it, and how it ended. */
|
|
5
8
|
export type FakeTransactionTrace<M extends AdapterModel> = {
|
|
9
|
+
/** The operations run inside the transaction, in order. */
|
|
6
10
|
readonly operations: readonly AdapterOperation<M>[];
|
|
11
|
+
/** How the transaction ended. */
|
|
7
12
|
readonly outcome: FakeTransactionOutcome;
|
|
8
13
|
};
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
11
|
-
* work rejects. Without it the fake Adapter records
|
|
12
|
-
* whatever the work wrote
|
|
15
|
+
* Rollback for state the test owns: `snapshot` when a transaction begins,
|
|
16
|
+
* `restore` when its work rejects. Without it, the fake Adapter records
|
|
17
|
+
* transaction boundaries but keeps whatever the work wrote.
|
|
13
18
|
*/
|
|
14
19
|
export type FakeAdapterRollback<S> = {
|
|
20
|
+
/** Captures the state before a transaction's work runs. */
|
|
15
21
|
readonly snapshot: () => S;
|
|
22
|
+
/** Puts the captured state back after the work rejects. */
|
|
16
23
|
readonly restore: (snapshot: S) => void;
|
|
17
24
|
};
|
|
25
|
+
/** Options for {@link createFakeAdapter}. */
|
|
18
26
|
export type FakeAdapterOptions<M extends AdapterModel, S = unknown> = {
|
|
27
|
+
/** The adapter model the fake Adapter reports. */
|
|
19
28
|
readonly model: M;
|
|
29
|
+
/** Answers each operation; without it, every operation resolves `undefined`. */
|
|
20
30
|
readonly execute?: FakeAdapterExecute<M>;
|
|
31
|
+
/** Makes transactions roll back the test's own state; see {@link FakeAdapterRollback}. */
|
|
21
32
|
readonly rollback?: FakeAdapterRollback<S>;
|
|
22
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The Adapter {@link createFakeAdapter} returns, with a record of every
|
|
36
|
+
* operation and transaction it ran.
|
|
37
|
+
*/
|
|
23
38
|
export type FakeAdapter<M extends AdapterModel = AdapterModel> = Adapter<M> & {
|
|
24
39
|
readonly operations: readonly AdapterOperation<M>[];
|
|
25
40
|
readonly transactions: readonly FakeTransactionTrace<M>[];
|
|
26
41
|
};
|
|
27
|
-
/**
|
|
42
|
+
/** A provider-free, in-memory Adapter for framework and conformance tests. */
|
|
28
43
|
export declare function createFakeAdapter<M extends AdapterModel, S = unknown>(options: FakeAdapterOptions<M, S>): FakeAdapter<M>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type { AdapterFactory } from "./adapters/adapter-factory.js";
|
|
|
2
2
|
export type { FakeAdapter, FakeAdapterExecute, FakeAdapterOptions, FakeAdapterRollback, FakeTransactionOutcome, FakeTransactionTrace, } from "./adapters/fake-adapter.js";
|
|
3
3
|
export { createFakeAdapter } from "./adapters/fake-adapter.js";
|
|
4
4
|
export { runAvProtocolConformance } from "./protocol/protocol-conformance.js";
|
|
5
|
+
export { AvProtocolCorpus } from "./protocol/protocol-corpus.js";
|
|
5
6
|
export type { AvProtocolDriver } from "./protocol/protocol-driver.js";
|
|
6
7
|
export type { AvProtocolFixture } from "./protocol/protocol-fixture.js";
|
|
7
8
|
export { AvProtocolFixtures } from "./protocol/protocol-fixtures.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createFakeAdapter } from "./adapters/fake-adapter.js";
|
|
2
2
|
export { runAvProtocolConformance } from "./protocol/protocol-conformance.js";
|
|
3
|
+
export { AvProtocolCorpus } from "./protocol/protocol-corpus.js";
|
|
3
4
|
export { AvProtocolFixtures } from "./protocol/protocol-fixtures.js";
|
|
@@ -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";
|
|
@@ -274,12 +275,12 @@ export declare const CORPUS_REFERENTIAL_ACTIONS: {
|
|
|
274
275
|
readonly books: readonly [{
|
|
275
276
|
readonly field: "author";
|
|
276
277
|
readonly target: "authors";
|
|
278
|
+
readonly references: readonly ["authorId"];
|
|
277
279
|
readonly onDelete: "cascade";
|
|
278
280
|
}];
|
|
279
281
|
};
|
|
280
282
|
/**
|
|
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"):
|
|
283
|
+
* The `where.id` a row sends to make the corpus Framework fail one way:
|
|
283
284
|
*
|
|
284
285
|
* - `A4000`, `A4001`, `A4002`, `A3000` — the client guard throws
|
|
285
286
|
* `FrameworkError("A4000")`, denies (`false`), throws
|
|
@@ -314,7 +315,7 @@ declare function corpusGuard(context: PipelineContext): boolean;
|
|
|
314
315
|
declare function corpusPipe(args: unknown): unknown;
|
|
315
316
|
/**
|
|
316
317
|
* The corpus's client-scope configuration: what the ClientContract hides or
|
|
317
|
-
* restricts, and `books.label
|
|
318
|
+
* restricts, and `books.label`, a VIRTUAL field added at client scope and
|
|
318
319
|
* computed on read from `id`.
|
|
319
320
|
*/
|
|
320
321
|
export declare const CORPUS_CLIENT_CONFIG: {
|
|
@@ -372,24 +373,20 @@ export declare const CORPUS_CLIENT_CONFIG: {
|
|
|
372
373
|
export declare const CORPUS_ENTRYPOINT = "/api";
|
|
373
374
|
/**
|
|
374
375
|
* 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.
|
|
376
|
+
* `Aventara-Contract-Hash`. A literal, as a generated client holds it.
|
|
378
377
|
*/
|
|
379
378
|
export declare const CORPUS_CLIENT_CONTRACT_HASH = "sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0";
|
|
380
379
|
/**
|
|
381
380
|
* The corpus ClientContract as `GET /_contract` serves it: its JCS-canonical
|
|
382
381
|
* bytes, `protocol.hash` included. A literal, as a host must serve it byte for
|
|
383
|
-
* byte
|
|
384
|
-
* contract moves.
|
|
382
|
+
* byte.
|
|
385
383
|
*/
|
|
386
384
|
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
385
|
/**
|
|
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`.
|
|
386
|
+
* The hash of the corpus's second ClientContract: the same model and
|
|
387
|
+
* configuration with `transactions: "none"` (the `/_transactions` absence
|
|
388
|
+
* rows). A row targets it by sending this hash, exactly as a client generated
|
|
389
|
+
* against it would.
|
|
393
390
|
*/
|
|
394
391
|
export declare const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:d333e8396b9bf9127e0859ed69b9874f230058a597e2129e6b9e9b380daef618";
|
|
395
392
|
/** The one record the corpus Adapter knows. */
|
|
@@ -81,7 +81,14 @@ export const CORPUS_MODEL = {
|
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
83
|
export const CORPUS_REFERENTIAL_ACTIONS = {
|
|
84
|
-
books: [
|
|
84
|
+
books: [
|
|
85
|
+
{
|
|
86
|
+
field: "author",
|
|
87
|
+
target: "authors",
|
|
88
|
+
references: ["authorId"],
|
|
89
|
+
onDelete: "cascade",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
85
92
|
};
|
|
86
93
|
export const CORPUS_TRIGGER = {
|
|
87
94
|
unauthenticated: "A4000",
|
|
@@ -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;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Framework } from "@aventara/core";
|
|
2
|
+
/**
|
|
3
|
+
* The server side of the protocol fixture corpus (`AvProtocolFixtures`), for
|
|
4
|
+
* a client or host author who serves it through their own stack: a Framework
|
|
5
|
+
* whose answers every corpus row states, the entrypoint its rows' paths are
|
|
6
|
+
* relative to, and the hash of the ClientContract it serves.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const framework = await AvProtocolCorpus.framework();
|
|
10
|
+
* // mount it under AvProtocolCorpus.entrypoint with your host, then send the
|
|
11
|
+
* // rows of AvProtocolFixtures and compare each answer with the row's `server`
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare const AvProtocolCorpus: {
|
|
15
|
+
/**
|
|
16
|
+
* A fresh corpus Framework, backed by an in-memory Adapter whose answer is a
|
|
17
|
+
* pure function of the operation it is handed. `contractHash` picks which
|
|
18
|
+
* corpus ClientContract it serves, as a row's `Aventara-Contract-Hash` does:
|
|
19
|
+
* the corpus's `transactions: "none"` contract for that contract's hash (the
|
|
20
|
+
* `/_transactions` absence rows send it), the main one for anything else.
|
|
21
|
+
*/
|
|
22
|
+
readonly framework: (contractHash?: string) => Promise<Framework>;
|
|
23
|
+
/** Where the corpus Framework is mounted; every row's `path` is relative to it. */
|
|
24
|
+
readonly entrypoint: string;
|
|
25
|
+
/**
|
|
26
|
+
* The hash of the main corpus ClientContract — what a client generated against
|
|
27
|
+
* it sends as `Aventara-Contract-Hash`, and what `GET /_contract` names.
|
|
28
|
+
*/
|
|
29
|
+
readonly clientContractHash: string;
|
|
30
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CORPUS_CLIENT_CONTRACT_HASH, CORPUS_ENTRYPOINT, createCorpusFramework, } from "./corpus-contract.fixture.js";
|
|
2
|
+
export const AvProtocolCorpus = Object.freeze({
|
|
3
|
+
framework: async (contractHash) => (await createCorpusFramework(contractHash)).framework,
|
|
4
|
+
entrypoint: CORPUS_ENTRYPOINT,
|
|
5
|
+
clientContractHash: CORPUS_CLIENT_CONTRACT_HASH,
|
|
6
|
+
});
|
|
@@ -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[];
|