@aventara/testing 0.1.0-pilot.4 → 1.0.0-rc.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/CHANGELOG.md +588 -0
- package/README.md +60 -8
- package/dist/adapters/adapter-factory.d.ts +2 -2
- package/dist/adapters/fake-adapter.d.ts +16 -15
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/protocol/corpus-contract.fixture.d.ts +60 -46
- package/dist/protocol/corpus-contract.fixture.js +48 -32
- package/dist/protocol/protocol-conformance.d.ts +4 -4
- package/dist/protocol/protocol-corpus.d.ts +30 -0
- package/dist/protocol/protocol-corpus.js +6 -0
- package/dist/protocol/protocol-driver.d.ts +2 -2
- package/dist/protocol/protocol-fixture.d.ts +4 -4
- package/dist/protocol/protocol-fixtures.js +43 -12
- 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/README.md
CHANGED
|
@@ -1,24 +1,75 @@
|
|
|
1
1
|
# `@aventara/testing`
|
|
2
2
|
|
|
3
|
-
Conformance fixtures and testing utilities for Aventara extensions
|
|
4
|
-
|
|
3
|
+
Conformance fixtures and testing utilities for Aventara extensions. They test an adapter or a transport host from
|
|
4
|
+
outside.
|
|
5
5
|
|
|
6
|
-
- `createFakeAdapter
|
|
6
|
+
- `createFakeAdapter`: an in-memory adapter, with transaction tracing, for testing code that runs an `AvFramework`
|
|
7
7
|
without a database.
|
|
8
|
-
- `runAvProtocolConformance(driver)
|
|
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
|
-
`AvProtocolDriver`: given
|
|
10
|
+
`AvProtocolDriver`: given an `AvFramework`, 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
|
|
|
14
17
|
## Install
|
|
15
18
|
|
|
16
19
|
```bash
|
|
17
|
-
npm install --save-dev @aventara/testing
|
|
20
|
+
npm install --save-dev @aventara/testing@rc
|
|
18
21
|
```
|
|
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 `AvFramework` itself. To serve it from a host or test bench of your own,
|
|
28
|
+
for example to point a client you are writing at it, 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 `AvFramework`, 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. A client generated against it sends this hash, and
|
|
43
|
+
`GET /_contract` names it.
|
|
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. It is generated from
|
|
52
|
+
`AvProtocolFixtures` when the package is built, with every row in the same order and with the same fields, so a client
|
|
53
|
+
or 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
|
|
@@ -26,5 +77,6 @@ corpus on Express and Fastify in its own tests. Each package's README ships in i
|
|
|
26
77
|
|
|
27
78
|
## License
|
|
28
79
|
|
|
29
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
30
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
80
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
81
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
82
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvAdapter, AvAdapterModel } from "@aventara/core";
|
|
2
2
|
/** Builds the Adapter under test; reusable adapter conformance suites take one. */
|
|
3
|
-
export type
|
|
3
|
+
export type AvAdapterFactory<M extends AvAdapterModel = AvAdapterModel> = () => AvAdapter<M> | Promise<AvAdapter<M>>;
|
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvAdapter, AvAdapterModel } from "@aventara/core";
|
|
2
|
+
import type { AvAdapterOperation } from "@aventara/core/internal";
|
|
2
3
|
/** Answers one operation the fake Adapter is handed. */
|
|
3
|
-
export type
|
|
4
|
+
export type AvFakeAdapterExecute<M extends AvAdapterModel> = (operation: AvAdapterOperation<M>) => unknown | Promise<unknown>;
|
|
4
5
|
/** How a transaction ended: `"committed"` or `"rolled-back"`. */
|
|
5
|
-
export type
|
|
6
|
+
export type AvFakeTransactionOutcome = "committed" | "rolled-back";
|
|
6
7
|
/** One settled transaction boundary: what ran inside it, and how it ended. */
|
|
7
|
-
export type
|
|
8
|
+
export type AvFakeTransactionTrace<M extends AvAdapterModel> = {
|
|
8
9
|
/** The operations run inside the transaction, in order. */
|
|
9
|
-
readonly operations: readonly
|
|
10
|
+
readonly operations: readonly AvAdapterOperation<M>[];
|
|
10
11
|
/** How the transaction ended. */
|
|
11
|
-
readonly outcome:
|
|
12
|
+
readonly outcome: AvFakeTransactionOutcome;
|
|
12
13
|
};
|
|
13
14
|
/**
|
|
14
15
|
* Rollback for state the test owns: `snapshot` when a transaction begins,
|
|
15
16
|
* `restore` when its work rejects. Without it, the fake Adapter records
|
|
16
17
|
* transaction boundaries but keeps whatever the work wrote.
|
|
17
18
|
*/
|
|
18
|
-
export type
|
|
19
|
+
export type AvFakeAdapterRollback<S> = {
|
|
19
20
|
/** Captures the state before a transaction's work runs. */
|
|
20
21
|
readonly snapshot: () => S;
|
|
21
22
|
/** Puts the captured state back after the work rejects. */
|
|
22
23
|
readonly restore: (snapshot: S) => void;
|
|
23
24
|
};
|
|
24
25
|
/** Options for {@link createFakeAdapter}. */
|
|
25
|
-
export type
|
|
26
|
+
export type AvFakeAdapterOptions<M extends AvAdapterModel, S = unknown> = {
|
|
26
27
|
/** The adapter model the fake Adapter reports. */
|
|
27
28
|
readonly model: M;
|
|
28
29
|
/** Answers each operation; without it, every operation resolves `undefined`. */
|
|
29
|
-
readonly execute?:
|
|
30
|
-
/** Makes transactions roll back the test's own state; see {@link
|
|
31
|
-
readonly rollback?:
|
|
30
|
+
readonly execute?: AvFakeAdapterExecute<M>;
|
|
31
|
+
/** Makes transactions roll back the test's own state; see {@link AvFakeAdapterRollback}. */
|
|
32
|
+
readonly rollback?: AvFakeAdapterRollback<S>;
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* The Adapter {@link createFakeAdapter} returns, with a record of every
|
|
35
36
|
* operation and transaction it ran.
|
|
36
37
|
*/
|
|
37
|
-
export type
|
|
38
|
-
readonly operations: readonly
|
|
39
|
-
readonly transactions: readonly
|
|
38
|
+
export type AvFakeAdapter<M extends AvAdapterModel = AvAdapterModel> = AvAdapter<M> & {
|
|
39
|
+
readonly operations: readonly AvAdapterOperation<M>[];
|
|
40
|
+
readonly transactions: readonly AvFakeTransactionTrace<M>[];
|
|
40
41
|
};
|
|
41
42
|
/** A provider-free, in-memory Adapter for framework and conformance tests. */
|
|
42
|
-
export declare function createFakeAdapter<M extends
|
|
43
|
+
export declare function createFakeAdapter<M extends AvAdapterModel, S = unknown>(options: AvFakeAdapterOptions<M, S>): AvFakeAdapter<M>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export type {
|
|
1
|
+
export type { AvAdapterFactory } from "./adapters/adapter-factory.js";
|
|
2
|
+
export type { AvFakeAdapter, AvFakeAdapterExecute, AvFakeAdapterOptions, AvFakeAdapterRollback, AvFakeTransactionOutcome, AvFakeTransactionTrace, } 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";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { AvContext, AvFramework } from "@aventara/core";
|
|
2
|
+
import { AvDecimal } from "@aventara/core";
|
|
3
3
|
/**
|
|
4
4
|
* The corpus's adapter model:
|
|
5
5
|
*
|
|
@@ -7,7 +7,7 @@ import { Decimal } from "@aventara/core";
|
|
|
7
7
|
* client scope;
|
|
8
8
|
* - `books`: references `authors`, and deleting an author cascades into it
|
|
9
9
|
* ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope
|
|
10
|
-
*
|
|
10
|
+
* defines a computed `label` the client alone sees ({@link CORPUS_RESOURCES});
|
|
11
11
|
* - `secrets`: its one operation restricted away at client scope;
|
|
12
12
|
* - `hiddenRes`: hidden at client scope;
|
|
13
13
|
* - `samples`: one field of each scalar whose wire form is not its runtime
|
|
@@ -275,6 +275,7 @@ export declare const CORPUS_REFERENTIAL_ACTIONS: {
|
|
|
275
275
|
readonly books: readonly [{
|
|
276
276
|
readonly field: "author";
|
|
277
277
|
readonly target: "authors";
|
|
278
|
+
readonly references: readonly ["authorId"];
|
|
278
279
|
readonly onDelete: "cascade";
|
|
279
280
|
}];
|
|
280
281
|
};
|
|
@@ -282,9 +283,9 @@ export declare const CORPUS_REFERENTIAL_ACTIONS: {
|
|
|
282
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
|
-
* `
|
|
287
|
-
* - `A2008`, `A2013`, `A2014` — the Adapter throws `
|
|
286
|
+
* `AvError("A4000")`, denies (`false`), throws
|
|
287
|
+
* `AvError("A4002")`, or throws a plain `Error`;
|
|
288
|
+
* - `A2008`, `A2013`, `A2014` — the Adapter throws `AvError` with that
|
|
288
289
|
* conflict code; `A3001` — it throws a plain `Error`;
|
|
289
290
|
* - `A3002` — a plan step carrying it runs, and the Adapter's transaction then
|
|
290
291
|
* fails to commit;
|
|
@@ -306,65 +307,78 @@ export declare const CORPUS_TRIGGER: {
|
|
|
306
307
|
readonly pipeOutputInvalid: "A3004";
|
|
307
308
|
};
|
|
308
309
|
/** The corpus's one client guard: it decides nothing unless a row names a trigger. */
|
|
309
|
-
declare function corpusGuard(context:
|
|
310
|
+
declare function corpusGuard(context: AvContext): boolean;
|
|
310
311
|
/**
|
|
311
312
|
* The corpus's one client pipe: it rewrites nothing unless a row names the
|
|
312
313
|
* `A3004` trigger, and then adds a filter on a field the server does not have.
|
|
313
314
|
*/
|
|
314
315
|
declare function corpusPipe(args: unknown): unknown;
|
|
315
316
|
/**
|
|
316
|
-
* The corpus's
|
|
317
|
-
*
|
|
318
|
-
* computed on read from `id`.
|
|
317
|
+
* The corpus's root `resources` tree: `books.label`, a VIRTUAL field computed on
|
|
318
|
+
* read from `id`.
|
|
319
319
|
*/
|
|
320
|
-
export declare const
|
|
321
|
-
readonly
|
|
322
|
-
readonly
|
|
320
|
+
export declare const CORPUS_RESOURCES: {
|
|
321
|
+
readonly books: {
|
|
322
|
+
readonly fields: {
|
|
323
323
|
readonly label: {
|
|
324
|
-
readonly
|
|
325
|
-
readonly
|
|
324
|
+
readonly definition: {
|
|
325
|
+
readonly type: {
|
|
326
|
+
readonly scalar: "string";
|
|
327
|
+
};
|
|
328
|
+
readonly nullable: false;
|
|
329
|
+
readonly list: false;
|
|
330
|
+
readonly lifecycle: readonly ["VIRTUAL"];
|
|
331
|
+
};
|
|
332
|
+
readonly pipelines: {
|
|
333
|
+
readonly compute: {
|
|
334
|
+
readonly read: {
|
|
335
|
+
readonly dependsOn: readonly ["id"];
|
|
336
|
+
readonly compute: ({ record, }: {
|
|
337
|
+
readonly record: Readonly<Record<string, unknown>>;
|
|
338
|
+
}) => string;
|
|
339
|
+
};
|
|
340
|
+
};
|
|
326
341
|
};
|
|
327
|
-
readonly nullable: false;
|
|
328
|
-
readonly list: false;
|
|
329
|
-
readonly lifecycle: readonly ["VIRTUAL"];
|
|
330
342
|
};
|
|
331
343
|
};
|
|
332
344
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
};
|
|
345
|
+
};
|
|
346
|
+
/** The corpus's application layer: `books.label` is the client's only. */
|
|
347
|
+
export declare const CORPUS_APPLICATION_CONFIG: {
|
|
348
|
+
readonly exposed: true;
|
|
349
|
+
readonly exemptions: {
|
|
350
|
+
readonly resources: {
|
|
351
|
+
readonly books: {
|
|
352
|
+
readonly fields: {
|
|
353
|
+
readonly label: true;
|
|
343
354
|
};
|
|
344
355
|
};
|
|
345
356
|
};
|
|
346
357
|
};
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* The corpus's client layer: what the ClientContract hides or restricts, and
|
|
361
|
+
* the client's own guard and pipe.
|
|
362
|
+
*/
|
|
363
|
+
export declare const CORPUS_CLIENT_CONFIG: {
|
|
347
364
|
readonly pipelines: {
|
|
348
365
|
readonly guards: readonly [typeof corpusGuard];
|
|
349
366
|
readonly pipes: readonly [typeof corpusPipe];
|
|
350
367
|
};
|
|
351
|
-
readonly
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
368
|
+
readonly exposed: true;
|
|
369
|
+
readonly exemptions: {
|
|
370
|
+
readonly resources: {
|
|
371
|
+
readonly authors: {
|
|
372
|
+
readonly fields: {
|
|
373
|
+
readonly secret: true;
|
|
356
374
|
};
|
|
357
375
|
};
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
readonly find: {
|
|
362
|
-
readonly many: false;
|
|
376
|
+
readonly secrets: {
|
|
377
|
+
readonly operations: {
|
|
378
|
+
readonly find: readonly ["many"];
|
|
363
379
|
};
|
|
364
380
|
};
|
|
365
|
-
|
|
366
|
-
readonly hiddenRes: {
|
|
367
|
-
readonly hidden: true;
|
|
381
|
+
readonly hiddenRes: true;
|
|
368
382
|
};
|
|
369
383
|
};
|
|
370
384
|
};
|
|
@@ -374,20 +388,20 @@ export declare const CORPUS_ENTRYPOINT = "/api";
|
|
|
374
388
|
* The corpus ClientContract's hash: what a generated client for it sends as
|
|
375
389
|
* `Aventara-Contract-Hash`. A literal, as a generated client holds it.
|
|
376
390
|
*/
|
|
377
|
-
export declare const CORPUS_CLIENT_CONTRACT_HASH = "sha256:
|
|
391
|
+
export declare const CORPUS_CLIENT_CONTRACT_HASH = "sha256:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307";
|
|
378
392
|
/**
|
|
379
393
|
* The corpus ClientContract as `GET /_contract` serves it: its JCS-canonical
|
|
380
394
|
* bytes, `protocol.hash` included. A literal, as a host must serve it byte for
|
|
381
395
|
* byte.
|
|
382
396
|
*/
|
|
383
|
-
export declare const CORPUS_CLIENT_CONTRACT_DOCUMENT = "{\"enums\":{},\"limits\":{\"maxBooleanNodes\":50,\"maxListLimit\":250,\"maxNestingDepth\":12,\"maxRequestBytes\":1048576,\"maxTransactionOperations\":20},\"protocol\":{\"hash\":\"sha256:
|
|
397
|
+
export declare const CORPUS_CLIENT_CONTRACT_DOCUMENT = "{\"enums\":{},\"limits\":{\"maxBooleanNodes\":50,\"maxJsonDepth\":64,\"maxListLimit\":250,\"maxNestingDepth\":12,\"maxRequestBytes\":1048576,\"maxTransactionOperations\":20},\"protocol\":{\"hash\":\"sha256:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307\",\"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\"}";
|
|
384
398
|
/**
|
|
385
399
|
* The hash of the corpus's second ClientContract: the same model and
|
|
386
400
|
* configuration with `transactions: "none"` (the `/_transactions` absence
|
|
387
401
|
* rows). A row targets it by sending this hash, exactly as a client generated
|
|
388
402
|
* against it would.
|
|
389
403
|
*/
|
|
390
|
-
export declare const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:
|
|
404
|
+
export declare const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:5e99809294a2aae922b635e9a44b464b1b6e03359a54d30420292c62676a7ce5";
|
|
391
405
|
/** The one record the corpus Adapter knows. */
|
|
392
406
|
export declare const CORPUS_AUTHOR: {
|
|
393
407
|
readonly id: "a1";
|
|
@@ -403,7 +417,7 @@ export declare const CORPUS_SAMPLE: {
|
|
|
403
417
|
readonly id: "s1";
|
|
404
418
|
readonly count: 9007199254740993n;
|
|
405
419
|
readonly at: Date;
|
|
406
|
-
readonly amount:
|
|
420
|
+
readonly amount: AvDecimal;
|
|
407
421
|
readonly blob: Uint8Array<ArrayBuffer>;
|
|
408
422
|
};
|
|
409
423
|
/**
|
|
@@ -414,7 +428,7 @@ export declare const CORPUS_SAMPLE: {
|
|
|
414
428
|
* otherwise (a stale or absent hash included).
|
|
415
429
|
*/
|
|
416
430
|
export declare function createCorpusFramework(contractHash?: string): Promise<{
|
|
417
|
-
readonly framework:
|
|
431
|
+
readonly framework: AvFramework;
|
|
418
432
|
readonly adapterCalls: () => number;
|
|
419
433
|
}>;
|
|
420
434
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AvDecimal, AvError, createFramework } from "@aventara/core";
|
|
2
2
|
import { createFakeAdapter } from "../adapters/fake-adapter.js";
|
|
3
3
|
const identityString = {
|
|
4
4
|
kind: "scalar",
|
|
@@ -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",
|
|
@@ -106,13 +113,13 @@ function whereIdOf(args) {
|
|
|
106
113
|
return typeof id === "string" ? id : undefined;
|
|
107
114
|
}
|
|
108
115
|
function corpusGuard(context) {
|
|
109
|
-
switch (whereIdOf(context
|
|
116
|
+
switch (whereIdOf(context.$args)) {
|
|
110
117
|
case CORPUS_TRIGGER.unauthenticated:
|
|
111
|
-
throw new
|
|
118
|
+
throw new AvError("A4000", "Authentication is required.");
|
|
112
119
|
case CORPUS_TRIGGER.guardDenies:
|
|
113
120
|
return false;
|
|
114
121
|
case CORPUS_TRIGGER.policyDenies:
|
|
115
|
-
throw new
|
|
122
|
+
throw new AvError("A4002", "Denied by the corpus policy.");
|
|
116
123
|
case CORPUS_TRIGGER.guardThrows:
|
|
117
124
|
throw new Error("the corpus guard failed");
|
|
118
125
|
default:
|
|
@@ -126,47 +133,54 @@ function corpusPipe(args) {
|
|
|
126
133
|
const record = args;
|
|
127
134
|
return { ...record, where: { ...record.where, nope: "x" } };
|
|
128
135
|
}
|
|
129
|
-
export const
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
export const CORPUS_RESOURCES = {
|
|
137
|
+
books: {
|
|
138
|
+
fields: {
|
|
132
139
|
label: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
dependsOn: ["id"],
|
|
146
|
-
compute: ({ record, }) => `book:${String(record.id)}`,
|
|
140
|
+
definition: {
|
|
141
|
+
type: { scalar: "string" },
|
|
142
|
+
nullable: false,
|
|
143
|
+
list: false,
|
|
144
|
+
lifecycle: ["VIRTUAL"],
|
|
145
|
+
},
|
|
146
|
+
pipelines: {
|
|
147
|
+
compute: {
|
|
148
|
+
read: {
|
|
149
|
+
dependsOn: ["id"],
|
|
150
|
+
compute: ({ record, }) => `book:${String(record.id)}`,
|
|
151
|
+
},
|
|
147
152
|
},
|
|
148
153
|
},
|
|
149
154
|
},
|
|
150
155
|
},
|
|
151
156
|
},
|
|
157
|
+
};
|
|
158
|
+
export const CORPUS_APPLICATION_CONFIG = {
|
|
159
|
+
exposed: true,
|
|
160
|
+
exemptions: { resources: { books: { fields: { label: true } } } },
|
|
161
|
+
};
|
|
162
|
+
export const CORPUS_CLIENT_CONFIG = {
|
|
152
163
|
pipelines: { guards: [corpusGuard], pipes: [corpusPipe] },
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
exposed: true,
|
|
165
|
+
exemptions: {
|
|
166
|
+
resources: {
|
|
167
|
+
authors: { fields: { secret: true } },
|
|
168
|
+
secrets: { operations: { find: ["many"] } },
|
|
169
|
+
hiddenRes: true,
|
|
170
|
+
},
|
|
157
171
|
},
|
|
158
172
|
};
|
|
159
173
|
export const CORPUS_ENTRYPOINT = "/api";
|
|
160
|
-
export const CORPUS_CLIENT_CONTRACT_HASH = "sha256:
|
|
161
|
-
export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxBooleanNodes":50,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:
|
|
162
|
-
export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:
|
|
174
|
+
export const CORPUS_CLIENT_CONTRACT_HASH = "sha256:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307";
|
|
175
|
+
export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxBooleanNodes":50,"maxJsonDepth":64,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307","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"}';
|
|
176
|
+
export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:5e99809294a2aae922b635e9a44b464b1b6e03359a54d30420292c62676a7ce5";
|
|
163
177
|
export const CORPUS_AUTHOR = { id: "a1", name: "Ada" };
|
|
164
178
|
export const CORPUS_NOBODY = "nobody";
|
|
165
179
|
export const CORPUS_SAMPLE = {
|
|
166
180
|
id: "s1",
|
|
167
181
|
count: 9007199254740993n,
|
|
168
182
|
at: new Date("2026-10-05T12:34:56.789Z"),
|
|
169
|
-
amount: new
|
|
183
|
+
amount: new AvDecimal("12.50"),
|
|
170
184
|
blob: new Uint8Array([255, 0, 16]),
|
|
171
185
|
};
|
|
172
186
|
const CONFLICTS = {
|
|
@@ -183,7 +197,7 @@ function corpusAnswer(operation) {
|
|
|
183
197
|
const id = whereIdOf(operation.arguments);
|
|
184
198
|
const conflict = id === undefined ? undefined : CONFLICTS[id];
|
|
185
199
|
if (conflict !== undefined) {
|
|
186
|
-
throw new
|
|
200
|
+
throw new AvError(conflict, `The corpus Adapter answered ${conflict}.`);
|
|
187
201
|
}
|
|
188
202
|
if (id === CORPUS_TRIGGER.adapterThrows) {
|
|
189
203
|
throw new Error("the corpus Adapter failed");
|
|
@@ -238,8 +252,10 @@ export async function createCorpusFramework(contractHash) {
|
|
|
238
252
|
: { transaction: failingCommit(fake.transaction) }),
|
|
239
253
|
});
|
|
240
254
|
const framework = await createFramework({
|
|
241
|
-
|
|
255
|
+
api: CORPUS_ENTRYPOINT,
|
|
242
256
|
adapter,
|
|
257
|
+
resources: CORPUS_RESOURCES,
|
|
258
|
+
application: CORPUS_APPLICATION_CONFIG,
|
|
243
259
|
client: CORPUS_CLIENT_CONFIG,
|
|
244
260
|
});
|
|
245
261
|
return { framework, adapterCalls: () => adapter.operations.length };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
* `runAvProtocolConformance(driver)`
|
|
3
|
+
* `runAvProtocolConformance(driver)` drives the protocol fixture corpus
|
|
4
4
|
* through one host.
|
|
5
5
|
*
|
|
6
6
|
* Every row a host answers is driven; rows whose `client.outcome` is
|
|
@@ -12,17 +12,17 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
|
12
12
|
* held to the row's `server` block:
|
|
13
13
|
*
|
|
14
14
|
* - `status`, exactly;
|
|
15
|
-
* - `code`: the body is a framework envelope carrying that code
|
|
15
|
+
* - `code`: the body is a framework envelope carrying that code or, for
|
|
16
16
|
* `null`, is none;
|
|
17
17
|
* - `causeKeys`: the exact sorted key set of `cause`;
|
|
18
18
|
* - `issues`: each issue's `code` and `path`, in order;
|
|
19
19
|
* - `adapterCalls`: the operations the corpus Adapter was handed, exactly;
|
|
20
20
|
* - `responseHeaders`: each named header present (names matched without
|
|
21
|
-
* regard to case) with exactly that value
|
|
21
|
+
* regard to case) with exactly that value; a host may add its own;
|
|
22
22
|
* - `bodyBytes`: the body text, exactly.
|
|
23
23
|
*
|
|
24
24
|
* Resolves one result per driven row, in corpus order, each with the
|
|
25
|
-
* mismatches found
|
|
25
|
+
* mismatches found (none when the host conforms). A driver that throws is a
|
|
26
26
|
* mismatch of its row, not a rejection of the run. Works with any test
|
|
27
27
|
* runner: assert on the results with whatever runner you use.
|
|
28
28
|
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AvFramework } 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<AvFramework>;
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvFramework } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
3
|
/**
|
|
4
4
|
* One host under test, as `runAvProtocolConformance` drives it: given the
|
|
@@ -7,7 +7,7 @@ import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/proto
|
|
|
7
7
|
* and body text, as an `AvProtocolResponse`). The run calls `close` once per
|
|
8
8
|
* row, after its one request.
|
|
9
9
|
*/
|
|
10
|
-
export type AvProtocolDriver = (framework:
|
|
10
|
+
export type AvProtocolDriver = (framework: AvFramework) => Promise<{
|
|
11
11
|
readonly send: (request: AvProtocolRequest) => Promise<AvProtocolResponse>;
|
|
12
12
|
readonly close: () => Promise<void>;
|
|
13
13
|
}>;
|