@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.
@@ -1,4 +1,4 @@
1
- import type { OperationCode, ValidationCode } from "@aventara/core";
1
+ import type { AvOperationCode, AvValidationCode } 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
@@ -25,11 +25,11 @@ export type AvProtocolFixture = {
25
25
  */
26
26
  readonly status: number;
27
27
  /** `null`: the host produces no framework envelope at all. */
28
- readonly code: OperationCode | null;
28
+ readonly code: AvOperationCode | null;
29
29
  /** The exact, sorted key set of `cause`. */
30
30
  readonly causeKeys?: readonly string[];
31
31
  readonly issues?: readonly {
32
- readonly code: ValidationCode;
32
+ readonly code: AvValidationCode;
33
33
  readonly path?: readonly (string | number)[];
34
34
  }[];
35
35
  /** Always stated: `0` is an assertion, not a default. */
@@ -47,6 +47,6 @@ export type AvProtocolFixture = {
47
47
  */
48
48
  readonly outcome: "resolve" | "framework-error" | "transport-error";
49
49
  /** Present iff `outcome` is `"framework-error"`. */
50
- readonly code?: OperationCode;
50
+ readonly code?: AvOperationCode;
51
51
  };
52
52
  };
@@ -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: ["arguments", "select", 1] }], 1),
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: ["operation"] }]),
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: ["operation"] }], 1),
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,11 +343,12 @@ 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)}`;
350
- const VALIDATION_FAILED = "Operation arguments failed framework validation.";
350
+ const RESOURCE_UNAVAILABLE = "The requested Resource is not available.";
351
+ const OPERATION_UNAVAILABLE = "The requested operation is not available on this Resource.";
351
352
  const absent = (label, request, status, code, cause) => ({
352
353
  label: `absent: ${label}`,
353
354
  request,
@@ -370,18 +371,18 @@ const absentRequest = (path, requestId, headers = {}, method = "POST") => {
370
371
  const base = corpusRequest("", {}, requestId);
371
372
  return { ...base, method, path, headers: { ...base.headers, ...headers } };
372
373
  };
373
- const unavailableOperation = (resource, operation) => ({
374
- message: VALIDATION_FAILED,
374
+ const unavailableOperation = (resource, operation, key) => ({
375
+ message: OPERATION_UNAVAILABLE,
375
376
  issues: [
376
377
  {
377
- path: ["operation"],
378
+ path: [key],
378
379
  code: "V1006",
379
380
  message: `Operation "${operation}" is not available on Resource "${resource}".`,
380
381
  },
381
382
  ],
382
383
  });
383
384
  const unavailableResource = (resource) => ({
384
- message: VALIDATION_FAILED,
385
+ message: RESOURCE_UNAVAILABLE,
385
386
  issues: [
386
387
  {
387
388
  code: "V1005",
@@ -391,7 +392,7 @@ const unavailableResource = (resource) => ({
391
392
  ],
392
393
  });
393
394
  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")),
395
+ absent("an unadvertised operation, current hash → 404 A2002", absentRequest("/_resources/books/delete/many", "corpus-absent-op"), 404, "A2002", unavailableOperation("books", "delete.many", "family")),
395
396
  absent("an unadvertised operation, stale hash → 409 A2005 (regenerate)", absentRequest("/_resources/books/delete/many", "corpus-absent-stale", {
396
397
  "Aventara-Contract-Hash": STALE_HASH,
397
398
  }), 409, "A2005", {
@@ -404,7 +405,7 @@ const ABSENCE = [
404
405
  "Aventara-Protocol-Version": "2",
405
406
  }), 400, "A2006", { message: "Aventara protocol version does not match the server." }),
406
407
  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")),
408
+ absent("a Resource with operations: {} → 404 A2002", absentRequest("/_resources/secrets/find/many", "corpus-absent-empty"), 404, "A2002", unavailableOperation("secrets", "find.many", "family")),
408
409
  absent("a client-hidden Resource → 404 A2001", absentRequest("/_resources/hiddenRes/find/many", "corpus-absent-hidden"), 404, "A2001", unavailableResource("hiddenRes")),
409
410
  absent('/_transactions under transactions "none" → 404 A2002 at ["operations"]', {
410
411
  ...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]),
@@ -476,6 +477,35 @@ const CREATE_ID = planNode("authors", "create.one", { data: { name: CORPUS_AUTHO
476
477
  const CREATE_ID_PRINT = CREATE_ID.fingerprint;
477
478
  const FIRST_NOBODY = planNode("authors", "find.first", { where: { id: CORPUS_NOBODY } }, "fp1:qNR9mD7_iVygNtmuuXjymw");
478
479
  const ARGUMENTS_FAILED = "Operation arguments failed framework validation.";
480
+ const TRANSACTION_REFERENCES = [
481
+ answered("transaction — a unique where and a filter reference an earlier step's result → 200 A1009", planRequest("corpus-tx-ref-where", [
482
+ CREATE_ID,
483
+ planNode("authors", "find.unique", { where: { id: ref(0, CREATE_ID_PRINT, "id") } }, "fp1:hKbxzChGakplMZvnpEa1_w"),
484
+ planNode("authors", "find.many", { where: { OR: [{ id: { equals: ref(0, CREATE_ID_PRINT, "id") } }] } }, "fp1:4LK9l_j8Skjk4iU_wSoRzQ"),
485
+ ]), 200, 3, {
486
+ data: [{ id: CORPUS_AUTHOR.id }, CORPUS_AUTHOR, [CORPUS_AUTHOR]],
487
+ code: "A1009",
488
+ cause: null,
489
+ }),
490
+ 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", [
491
+ CREATE_ID,
492
+ planNode("authors", "find.unique", { where: { id: ref(5, CREATE_ID_PRINT, "id") } }, "fp1:VS6Zk71avpYzuC90z8qzrQ"),
493
+ ]), 422, 0, {
494
+ data: null,
495
+ code: "A2007",
496
+ cause: {
497
+ message: ARGUMENTS_FAILED,
498
+ issues: [
499
+ {
500
+ code: "V1016",
501
+ path: ["operations", 1, "args", "where", "id", "$ref", "operation"],
502
+ message: "Transaction reference operation index is invalid.",
503
+ },
504
+ ],
505
+ operation: 1,
506
+ },
507
+ }),
508
+ ];
479
509
  const ARGUMENT_VALIDATION = [
480
510
  answered("argument: an unknown field in where → 422 A2004 / V1005", corpusRequest("find/many", { where: { nope: { equals: "x" } } }, "corpus-arg-field"), 422, 0, {
481
511
  data: null,
@@ -818,6 +848,7 @@ export const AvProtocolFixtures = [
818
848
  ...METHOD,
819
849
  ...ABSENCE,
820
850
  ...TRANSACTIONS,
851
+ ...TRANSACTION_REFERENCES,
821
852
  ...INTERNAL,
822
853
  ...CONFLICT,
823
854
  ...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
+ }