@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
|
@@ -322,20 +322,20 @@ const TRANSACTIONS = [
|
|
|
322
322
|
]), 422, "A2004", [{ code: "V1019", path: ["operations", 1, "resource"] }], 1),
|
|
323
323
|
...["scope", "origin"].map((key) => invalid(`transaction — a plan node carrying "${key}" cannot smuggle an origin → 422 A2004 / V1001`, planRequest(`corpus-tx-smuggle-${key}`, [
|
|
324
324
|
{ ...AUTHORS_FIND_MANY, [key]: "application" },
|
|
325
|
-
]), 422, "A2004", [{ code: "V1001", path: [key] }], 0)),
|
|
325
|
+
]), 422, "A2004", [{ code: "V1001", path: ["operations", 0, key] }], 0)),
|
|
326
326
|
invalid("a client-hidden field — single route", corpusRequest("find/many", { select: ["id", "secret"] }, "corpus-r1-field"), 422, "A2004", [{ code: "V1005", path: ["arguments", "select", 1] }]),
|
|
327
327
|
invalid("a client-hidden field — /_transactions", planRequest("corpus-r1-field-tx", [
|
|
328
328
|
AUTHORS_FIND_MANY,
|
|
329
329
|
planNode("authors", "find.many", { select: ["id", "secret"] }, "fp1:1t8vSfPuohv1N5MIX8ccNw"),
|
|
330
|
-
]), 422, "A2004", [{ code: "V1005", path: ["
|
|
330
|
+
]), 422, "A2004", [{ code: "V1005", path: ["operations", 1, "args", "select", 1] }], 1),
|
|
331
331
|
invalid("a client-restricted variant — single route", {
|
|
332
332
|
...corpusRequest("", {}, "corpus-r1-variant"),
|
|
333
333
|
path: "/_resources/secrets/find/many",
|
|
334
|
-
}, 404, "A2002", [{ code: "V1006", path: ["
|
|
334
|
+
}, 404, "A2002", [{ code: "V1006", path: ["family"] }]),
|
|
335
335
|
invalid("a client-restricted variant — /_transactions", planRequest("corpus-r1-variant-tx", [
|
|
336
336
|
AUTHORS_FIND_MANY,
|
|
337
337
|
planNode("secrets", "find.many", {}, "fp1:KegI5OcquDHi8z39VH11EQ"),
|
|
338
|
-
]), 404, "A2002", [{ code: "V1006", path: ["
|
|
338
|
+
]), 404, "A2002", [{ code: "V1006", path: ["operations", 1, "family"] }], 1),
|
|
339
339
|
invalid("a client-hidden Resource — single route", {
|
|
340
340
|
...corpusRequest("", {}, "corpus-r1-resource"),
|
|
341
341
|
path: "/_resources/hiddenRes/find/many",
|
|
@@ -343,7 +343,7 @@ const TRANSACTIONS = [
|
|
|
343
343
|
invalid("a client-hidden Resource — /_transactions", planRequest("corpus-r1-resource-tx", [
|
|
344
344
|
AUTHORS_FIND_MANY,
|
|
345
345
|
planNode("hiddenRes", "find.many", {}, "fp1:UY6pLVLKM2LaIMnfQa0zGw"),
|
|
346
|
-
]), 404, "A2001", [{ code: "V1005", path: ["resource"] }], 1),
|
|
346
|
+
]), 404, "A2001", [{ code: "V1005", path: ["operations", 1, "resource"] }], 1),
|
|
347
347
|
refused("method: GET on /_transactions → 405 A2012 with Allow: POST", planRequest("corpus-tx-get", [AUTHORS_FIND_MANY], { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
|
|
348
348
|
];
|
|
349
349
|
const STALE_HASH = `sha256:${"0".repeat(64)}`;
|
|
@@ -370,11 +370,11 @@ const absentRequest = (path, requestId, headers = {}, method = "POST") => {
|
|
|
370
370
|
const base = corpusRequest("", {}, requestId);
|
|
371
371
|
return { ...base, method, path, headers: { ...base.headers, ...headers } };
|
|
372
372
|
};
|
|
373
|
-
const unavailableOperation = (resource, operation) => ({
|
|
373
|
+
const unavailableOperation = (resource, operation, key) => ({
|
|
374
374
|
message: VALIDATION_FAILED,
|
|
375
375
|
issues: [
|
|
376
376
|
{
|
|
377
|
-
path: [
|
|
377
|
+
path: [key],
|
|
378
378
|
code: "V1006",
|
|
379
379
|
message: `Operation "${operation}" is not available on Resource "${resource}".`,
|
|
380
380
|
},
|
|
@@ -391,7 +391,7 @@ const unavailableResource = (resource) => ({
|
|
|
391
391
|
],
|
|
392
392
|
});
|
|
393
393
|
const ABSENCE = [
|
|
394
|
-
absent("an unadvertised operation, current hash → 404 A2002", absentRequest("/_resources/books/delete/many", "corpus-absent-op"), 404, "A2002", unavailableOperation("books", "delete.many")),
|
|
394
|
+
absent("an unadvertised operation, current hash → 404 A2002", absentRequest("/_resources/books/delete/many", "corpus-absent-op"), 404, "A2002", unavailableOperation("books", "delete.many", "family")),
|
|
395
395
|
absent("an unadvertised operation, stale hash → 409 A2005 (regenerate)", absentRequest("/_resources/books/delete/many", "corpus-absent-stale", {
|
|
396
396
|
"Aventara-Contract-Hash": STALE_HASH,
|
|
397
397
|
}), 409, "A2005", {
|
|
@@ -404,7 +404,7 @@ const ABSENCE = [
|
|
|
404
404
|
"Aventara-Protocol-Version": "2",
|
|
405
405
|
}), 400, "A2006", { message: "Aventara protocol version does not match the server." }),
|
|
406
406
|
absent("an unknown Resource → 404 A2001", absentRequest("/_resources/nope/find/many", "corpus-absent-resource"), 404, "A2001", unavailableResource("nope")),
|
|
407
|
-
absent("a Resource with operations: {} → 404 A2002", absentRequest("/_resources/secrets/find/many", "corpus-absent-empty"), 404, "A2002", unavailableOperation("secrets", "find.many")),
|
|
407
|
+
absent("a Resource with operations: {} → 404 A2002", absentRequest("/_resources/secrets/find/many", "corpus-absent-empty"), 404, "A2002", unavailableOperation("secrets", "find.many", "family")),
|
|
408
408
|
absent("a client-hidden Resource → 404 A2001", absentRequest("/_resources/hiddenRes/find/many", "corpus-absent-hidden"), 404, "A2001", unavailableResource("hiddenRes")),
|
|
409
409
|
absent('/_transactions under transactions "none" → 404 A2002 at ["operations"]', {
|
|
410
410
|
...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]),
|
|
@@ -476,6 +476,35 @@ const CREATE_ID = planNode("authors", "create.one", { data: { name: CORPUS_AUTHO
|
|
|
476
476
|
const CREATE_ID_PRINT = CREATE_ID.fingerprint;
|
|
477
477
|
const FIRST_NOBODY = planNode("authors", "find.first", { where: { id: CORPUS_NOBODY } }, "fp1:qNR9mD7_iVygNtmuuXjymw");
|
|
478
478
|
const ARGUMENTS_FAILED = "Operation arguments failed framework validation.";
|
|
479
|
+
const TRANSACTION_REFERENCES = [
|
|
480
|
+
answered("transaction — a unique where and a filter reference an earlier step's result → 200 A1009", planRequest("corpus-tx-ref-where", [
|
|
481
|
+
CREATE_ID,
|
|
482
|
+
planNode("authors", "find.unique", { where: { id: ref(0, CREATE_ID_PRINT, "id") } }, "fp1:hKbxzChGakplMZvnpEa1_w"),
|
|
483
|
+
planNode("authors", "find.many", { where: { OR: [{ id: { equals: ref(0, CREATE_ID_PRINT, "id") } }] } }, "fp1:4LK9l_j8Skjk4iU_wSoRzQ"),
|
|
484
|
+
]), 200, 3, {
|
|
485
|
+
data: [{ id: CORPUS_AUTHOR.id }, CORPUS_AUTHOR, [CORPUS_AUTHOR]],
|
|
486
|
+
code: "A1009",
|
|
487
|
+
cause: null,
|
|
488
|
+
}),
|
|
489
|
+
answered("argument: a plan reference in a where to an operation index no earlier step has → 422 A2007 / V1016", planRequest("corpus-arg-ref-where-index", [
|
|
490
|
+
CREATE_ID,
|
|
491
|
+
planNode("authors", "find.unique", { where: { id: ref(5, CREATE_ID_PRINT, "id") } }, "fp1:VS6Zk71avpYzuC90z8qzrQ"),
|
|
492
|
+
]), 422, 0, {
|
|
493
|
+
data: null,
|
|
494
|
+
code: "A2007",
|
|
495
|
+
cause: {
|
|
496
|
+
message: ARGUMENTS_FAILED,
|
|
497
|
+
issues: [
|
|
498
|
+
{
|
|
499
|
+
code: "V1016",
|
|
500
|
+
path: ["operations", 1, "args", "where", "id", "$ref", "operation"],
|
|
501
|
+
message: "Transaction reference operation index is invalid.",
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
operation: 1,
|
|
505
|
+
},
|
|
506
|
+
}),
|
|
507
|
+
];
|
|
479
508
|
const ARGUMENT_VALIDATION = [
|
|
480
509
|
answered("argument: an unknown field in where → 422 A2004 / V1005", corpusRequest("find/many", { where: { nope: { equals: "x" } } }, "corpus-arg-field"), 422, 0, {
|
|
481
510
|
data: null,
|
|
@@ -818,6 +847,7 @@ export const AvProtocolFixtures = [
|
|
|
818
847
|
...METHOD,
|
|
819
848
|
...ABSENCE,
|
|
820
849
|
...TRANSACTIONS,
|
|
850
|
+
...TRANSACTION_REFERENCES,
|
|
821
851
|
...INTERNAL,
|
|
822
852
|
...CONFLICT,
|
|
823
853
|
...WIRE_SCALARS,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AvProtocolFixture } from "./protocol-fixture.js";
|
|
2
|
+
/**
|
|
3
|
+
* A long run of one character in a request body, spelled as a recipe instead of
|
|
4
|
+
* its bytes: `prefix + repeat × times + suffix`. The one oversized-body row
|
|
5
|
+
* would otherwise be a megabyte of `x`.
|
|
6
|
+
*/
|
|
7
|
+
export type ProtocolFixtureContentRecipe = {
|
|
8
|
+
readonly prefix?: string;
|
|
9
|
+
readonly repeat: string;
|
|
10
|
+
readonly times: number;
|
|
11
|
+
readonly suffix?: string;
|
|
12
|
+
};
|
|
13
|
+
/** A row as the JSON file holds it: a raw body's content may be a recipe. */
|
|
14
|
+
export type ProtocolFixtureJsonRow = Omit<AvProtocolFixture, "request"> & {
|
|
15
|
+
readonly request: Omit<AvProtocolFixture["request"], "body"> & {
|
|
16
|
+
readonly body: {
|
|
17
|
+
readonly kind: "raw";
|
|
18
|
+
readonly content: string | ProtocolFixtureContentRecipe;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "parsed";
|
|
21
|
+
readonly value: unknown;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** The file: the protocol version, the corpus's mount point, its ClientContracts, every row. */
|
|
26
|
+
export type ProtocolFixturesJson = {
|
|
27
|
+
readonly protocolVersion: number;
|
|
28
|
+
readonly entrypoint: string;
|
|
29
|
+
readonly clientContractHash: string;
|
|
30
|
+
/** Each ClientContract a row targets: its hash and its exact `GET /_contract` bytes. */
|
|
31
|
+
readonly contracts: readonly {
|
|
32
|
+
readonly hash: string;
|
|
33
|
+
readonly document: string;
|
|
34
|
+
}[];
|
|
35
|
+
readonly rows: readonly ProtocolFixtureJsonRow[];
|
|
36
|
+
};
|
|
37
|
+
/** A row of the file, expanded back to the TS row it was written from. */
|
|
38
|
+
export declare function expandProtocolFixtureRow(row: ProtocolFixtureJsonRow): AvProtocolFixture;
|
|
39
|
+
/**
|
|
40
|
+
* `fixtures/protocol-v1.json`, as text: one line per ClientContract and per row,
|
|
41
|
+
* in corpus order, so a change to the corpus is a change to its own lines.
|
|
42
|
+
*/
|
|
43
|
+
export declare function protocolFixturesJson(): Promise<string>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AVENTARA_PROTOCOL_VERSION } from "@aventara/core/internal";
|
|
2
|
+
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
|
+
import { CORPUS_CLIENT_CONTRACT_HASH, CORPUS_ENTRYPOINT, CORPUS_NONE_CLIENT_CONTRACT_HASH, createCorpusFramework, } from "./corpus-contract.fixture.js";
|
|
4
|
+
import { AvProtocolFixtures } from "./protocol-fixtures.js";
|
|
5
|
+
const RECIPE_MINIMUM = 1024;
|
|
6
|
+
function recipeOf(text) {
|
|
7
|
+
let best = { start: 0, length: 0 };
|
|
8
|
+
let start = 0;
|
|
9
|
+
for (let index = 1; index <= text.length; index += 1) {
|
|
10
|
+
if (index === text.length || text[index] !== text[start]) {
|
|
11
|
+
if (index - start > best.length) {
|
|
12
|
+
best = { start, length: index - start };
|
|
13
|
+
}
|
|
14
|
+
start = index;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (best.length < RECIPE_MINIMUM) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const prefix = text.slice(0, best.start);
|
|
21
|
+
const suffix = text.slice(best.start + best.length);
|
|
22
|
+
return {
|
|
23
|
+
...(prefix === "" ? {} : { prefix }),
|
|
24
|
+
repeat: text[best.start],
|
|
25
|
+
times: best.length,
|
|
26
|
+
...(suffix === "" ? {} : { suffix }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function encodeRow(row) {
|
|
30
|
+
const { body } = row.request;
|
|
31
|
+
if (body.kind === "parsed") {
|
|
32
|
+
return { ...row, request: { ...row.request, body } };
|
|
33
|
+
}
|
|
34
|
+
if (typeof body.content !== "string") {
|
|
35
|
+
throw new Error(`protocol fixture "${row.label}": a body of bytes has no JSON spelling`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...row,
|
|
39
|
+
request: {
|
|
40
|
+
...row.request,
|
|
41
|
+
body: { kind: "raw", content: recipeOf(body.content) ?? body.content },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function expandProtocolFixtureRow(row) {
|
|
46
|
+
const { body } = row.request;
|
|
47
|
+
if (body.kind === "parsed" || typeof body.content === "string") {
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
const { prefix = "", repeat, times, suffix = "" } = body.content;
|
|
51
|
+
return {
|
|
52
|
+
...row,
|
|
53
|
+
request: {
|
|
54
|
+
...row.request,
|
|
55
|
+
body: { kind: "raw", content: prefix + repeat.repeat(times) + suffix },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function contractDocument(contractHash) {
|
|
60
|
+
const { framework } = await createCorpusFramework(contractHash);
|
|
61
|
+
return AvProtocol.bind(framework).encodeContract({
|
|
62
|
+
method: "GET",
|
|
63
|
+
path: "/_contract",
|
|
64
|
+
headers: {},
|
|
65
|
+
body: { kind: "raw", content: "" },
|
|
66
|
+
}).body;
|
|
67
|
+
}
|
|
68
|
+
export async function protocolFixturesJson() {
|
|
69
|
+
if (AVENTARA_PROTOCOL_VERSION !== 1) {
|
|
70
|
+
throw new Error(`protocol-v1.json describes protocol version 1, not ${AVENTARA_PROTOCOL_VERSION}`);
|
|
71
|
+
}
|
|
72
|
+
const contracts = [
|
|
73
|
+
{
|
|
74
|
+
hash: CORPUS_CLIENT_CONTRACT_HASH,
|
|
75
|
+
document: await contractDocument(CORPUS_CLIENT_CONTRACT_HASH),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
hash: CORPUS_NONE_CLIENT_CONTRACT_HASH,
|
|
79
|
+
document: await contractDocument(CORPUS_NONE_CLIENT_CONTRACT_HASH),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
const head = JSON.stringify({
|
|
83
|
+
protocolVersion: AVENTARA_PROTOCOL_VERSION,
|
|
84
|
+
entrypoint: CORPUS_ENTRYPOINT,
|
|
85
|
+
clientContractHash: CORPUS_CLIENT_CONTRACT_HASH,
|
|
86
|
+
}).slice(0, -1);
|
|
87
|
+
const lines = (items) => items.map((item) => JSON.stringify(item)).join(",\n");
|
|
88
|
+
return `${head},"contracts":[\n${lines(contracts)}\n],"rows":[\n${lines(AvProtocolFixtures.map(encodeRow))}\n]}\n`;
|
|
89
|
+
}
|