@aventara/testing 1.0.0-rc.0 → 1.0.0-rc.10

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.
@@ -8,6 +8,8 @@ export const inProcessDriver = async (framework) => {
8
8
  return protocol.encodeContract(request);
9
9
  case "/_transactions":
10
10
  return protocol.handleTransaction(request);
11
+ case "/_access/can":
12
+ return protocol.handleCan(request);
11
13
  default:
12
14
  return protocol.handleOperation(request);
13
15
  }
@@ -2,7 +2,8 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
2
2
  /**
3
3
  * The protocol hosted in process as a host that mounts one route per
4
4
  * `protocol.surface()` entry (each operation on `handleOperation`,
5
- * `/_transactions` on `handleTransaction`, `/_contract` on `encodeContract`,
5
+ * `/_transactions` on `handleTransaction`, `/_access/can` on `handleCan`,
6
+ * `/_contract` on `encodeContract`,
6
7
  * matched by method and path) and uses `protocol.answerAbsent` as the fallback
7
8
  * for everything it did not mount. `undefined` from the fallback is a path
8
9
  * outside the protocol, which this host answers with its own bodiless 404.
@@ -14,6 +14,8 @@ export const perRouteDriver = async (framework) => {
14
14
  return protocol.handleOperation(request);
15
15
  case "transactions":
16
16
  return protocol.handleTransaction(request);
17
+ case "can":
18
+ return protocol.handleCan(request);
17
19
  case "contract":
18
20
  return protocol.encodeContract(request);
19
21
  default:
@@ -1,6 +1,6 @@
1
1
  import type { AvProtocolDriver } from "./protocol-driver.js";
2
2
  /**
3
- * `runAvProtocolConformance(driver)` — drives the protocol fixture corpus
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 — or, for
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 — a host may add its own;
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 — none when the host conforms. A driver that throws is a
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
  */
@@ -1,4 +1,4 @@
1
- import type { Framework } from "@aventara/core";
1
+ import type { AvFramework } from "@aventara/core";
2
2
  /**
3
3
  * The server side of the protocol fixture corpus (`AvProtocolFixtures`), for
4
4
  * a client or host author who serves it through their own stack: a Framework
@@ -17,13 +17,15 @@ export declare const AvProtocolCorpus: {
17
17
  * pure function of the operation it is handed. `contractHash` picks which
18
18
  * corpus ClientContract it serves, as a row's `Aventara-Contract-Hash` does:
19
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.
20
+ * `/_transactions` absence rows send it), the one with the framework's
21
+ * `access` option for its hash (the `/_access/can` rows send it), the main one
22
+ * for anything else.
21
23
  */
22
- readonly framework: (contractHash?: string) => Promise<Framework>;
24
+ readonly framework: (contractHash?: string) => Promise<AvFramework>;
23
25
  /** Where the corpus Framework is mounted; every row's `path` is relative to it. */
24
26
  readonly entrypoint: string;
25
27
  /**
26
- * The hash of the main corpus ClientContract — what a client generated against
28
+ * The hash of the main corpus ClientContract: what a client generated against
27
29
  * it sends as `Aventara-Contract-Hash`, and what `GET /_contract` names.
28
30
  */
29
31
  readonly clientContractHash: string;
@@ -1,4 +1,4 @@
1
- import type { Framework } from "@aventara/core";
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: Framework) => Promise<{
10
+ export type AvProtocolDriver = (framework: AvFramework) => Promise<{
11
11
  readonly send: (request: AvProtocolRequest) => Promise<AvProtocolResponse>;
12
12
  readonly close: () => Promise<void>;
13
13
  }>;
@@ -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
  };
@@ -1,4 +1,4 @@
1
- import { CORPUS_AUTHOR, CORPUS_CLIENT_CONTRACT_DOCUMENT, CORPUS_CLIENT_CONTRACT_HASH, CORPUS_NOBODY, CORPUS_NONE_CLIENT_CONTRACT_HASH, CORPUS_TRIGGER, } from "./corpus-contract.fixture.js";
1
+ import { CORPUS_ACCESS_CLIENT_CONTRACT_HASH, CORPUS_AUTHOR, CORPUS_CLIENT_CONTRACT_DOCUMENT, CORPUS_CLIENT_CONTRACT_HASH, CORPUS_NOBODY, CORPUS_NONE_CLIENT_CONTRACT_HASH, CORPUS_TRIGGER, } from "./corpus-contract.fixture.js";
2
2
  const OPERATION_REQUEST = {
3
3
  method: "POST",
4
4
  path: "/_resources/authors/find/many",
@@ -347,7 +347,8 @@ const TRANSACTIONS = [
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,
@@ -371,7 +372,7 @@ const absentRequest = (path, requestId, headers = {}, method = "POST") => {
371
372
  return { ...base, method, path, headers: { ...base.headers, ...headers } };
372
373
  };
373
374
  const unavailableOperation = (resource, operation, key) => ({
374
- message: VALIDATION_FAILED,
375
+ message: OPERATION_UNAVAILABLE,
375
376
  issues: [
376
377
  {
377
378
  path: [key],
@@ -381,7 +382,7 @@ const unavailableOperation = (resource, operation, key) => ({
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",
@@ -836,6 +837,146 @@ const CORRELATION = [
836
837
  ["an id with a space", "a b"],
837
838
  ].map(([what, id]) => answered(`correlation: ${what} is not adopted, and never refused → 200 A1000`, corpusRequest("find/many", {}, id), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }, { "Content-Type": "application/json" })),
838
839
  ];
840
+ const canRequest = (requestId, questions, overrides = {}) => {
841
+ const base = corpusRequest("", { questions }, requestId);
842
+ return {
843
+ ...base,
844
+ path: "/_access/can",
845
+ headers: {
846
+ ...base.headers,
847
+ "Aventara-Contract-Hash": CORPUS_ACCESS_CLIENT_CONTRACT_HASH,
848
+ },
849
+ ...overrides,
850
+ };
851
+ };
852
+ const AUTHOR_ROW = { where: { id: CORPUS_AUTHOR.id } };
853
+ const QUESTIONS_INVALID = "Operation arguments failed framework validation.";
854
+ const ACCESS_QUESTIONS = [
855
+ answered("can: three questions in one request, one answer each → 200 A1000", canRequest("corpus-can-three", [
856
+ { resource: "authors", action: "update", ...AUTHOR_ROW },
857
+ { resource: "authors", action: "delete", ...AUTHOR_ROW },
858
+ { resource: "authors", action: "read" },
859
+ ]), 200, 1, { data: [true, false, true], code: "A1000", cause: null }),
860
+ answered("can: a row that does not exist answers false, never A2003", canRequest("corpus-can-missing", [
861
+ {
862
+ resource: "authors",
863
+ action: "update",
864
+ where: { id: CORPUS_NOBODY },
865
+ },
866
+ ]), 200, 1, { data: [false], code: "A1000", cause: null }),
867
+ answered("can: an operation the client is not offered answers false", canRequest("corpus-can-unoffered", [
868
+ { resource: "secrets", action: "read" },
869
+ { resource: "samples", action: "delete" },
870
+ ]), 200, 0, { data: [false, false], code: "A1000", cause: null }),
871
+ answered("can: an AvError the engine throws answers false", canRequest("corpus-can-engine-refuses", [
872
+ { resource: "books", action: "read" },
873
+ { resource: "authors", action: "read" },
874
+ ]), 200, 0, { data: [false, true], code: "A1000", cause: null }),
875
+ answered("can: no questions, no answers → 200 A1000", canRequest("corpus-can-none", []), 200, 0, { data: [], code: "A1000", cause: null }),
876
+ absent('/_access/can without access → 404 A2002 at ["questions"]', {
877
+ ...canRequest("corpus-absent-can", []),
878
+ headers: {
879
+ ...canRequest("corpus-absent-can", []).headers,
880
+ "Aventara-Contract-Hash": CORPUS_CLIENT_CONTRACT_HASH,
881
+ },
882
+ }, 404, "A2002", {
883
+ message: "Access questions are not available on the active Contract.",
884
+ issues: [
885
+ {
886
+ code: "V1006",
887
+ path: ["questions"],
888
+ message: "Access questions are not available on the active Contract.",
889
+ },
890
+ ],
891
+ }),
892
+ refused("method: GET on /_access/can → 405 A2012 with Allow: POST", canRequest("corpus-can-get", [], { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
893
+ answered("can: an unknown and a client-hidden Resource → 404 A2001 for the batch", canRequest("corpus-can-unknown", [
894
+ { resource: "authors", action: "read" },
895
+ { resource: "nope", action: "read" },
896
+ { resource: "hiddenRes", action: "read" },
897
+ ]), 404, 0, {
898
+ data: null,
899
+ code: "A2001",
900
+ cause: {
901
+ message: "The requested Resource is not available.",
902
+ issues: [
903
+ {
904
+ code: "V1005",
905
+ path: ["questions", 1, "resource"],
906
+ message: 'Resource "nope" is not available in the active Contract.',
907
+ },
908
+ {
909
+ code: "V1005",
910
+ path: ["questions", 2, "resource"],
911
+ message: 'Resource "hiddenRes" is not available in the active Contract.',
912
+ },
913
+ ],
914
+ },
915
+ }),
916
+ answered('can: a malformed question → 422 A2004 at ["questions", i, …]', canRequest("corpus-can-malformed", [
917
+ { resource: "authors", action: "refund" },
918
+ { resource: "authors", action: "read", where: { name: "Ada" } },
919
+ { resource: "authors", action: "create", ...AUTHOR_ROW },
920
+ ]), 422, 0, {
921
+ data: null,
922
+ code: "A2004",
923
+ cause: {
924
+ message: QUESTIONS_INVALID,
925
+ issues: [
926
+ {
927
+ code: "V1002",
928
+ path: ["questions", 0, "action"],
929
+ message: 'action must be one of "read", "create", "update" or "delete".',
930
+ },
931
+ {
932
+ code: "V1007",
933
+ path: ["questions", 1, "where"],
934
+ message: 'Unique selector must exactly match one active identifier on Resource "authors".',
935
+ },
936
+ {
937
+ code: "V1001",
938
+ path: ["questions", 2, "where"],
939
+ message: "A create question names no row: it takes no where.",
940
+ },
941
+ ],
942
+ },
943
+ }),
944
+ answered("can: more questions than maxListLimit → 422 A2009", canRequest("corpus-can-over-limit", Array.from({ length: 251 }, () => ({
945
+ resource: "authors",
946
+ action: "read",
947
+ }))), 422, 0, {
948
+ data: null,
949
+ code: "A2009",
950
+ cause: {
951
+ message: QUESTIONS_INVALID,
952
+ issues: [
953
+ {
954
+ code: "V1014",
955
+ path: ["questions"],
956
+ message: "Question count exceeds maxListLimit (250).",
957
+ },
958
+ ],
959
+ },
960
+ }),
961
+ answered("can: the engine throws → 500 A3000", canRequest("corpus-can-engine-throws", [
962
+ { resource: "samples", action: "read" },
963
+ ]), 500, 0, {
964
+ data: null,
965
+ code: "A3000",
966
+ cause: { message: "Internal framework error." },
967
+ }),
968
+ answered("can: the Adapter fails reading the row → 500 A3001", canRequest("corpus-can-adapter-throws", [
969
+ {
970
+ resource: "authors",
971
+ action: "update",
972
+ where: { id: CORPUS_TRIGGER.adapterThrows },
973
+ },
974
+ ]), 500, 1, {
975
+ data: null,
976
+ code: "A3001",
977
+ cause: { message: "Adapter execution failed." },
978
+ }),
979
+ ];
839
980
  export const AvProtocolFixtures = [
840
981
  ...CONTRACT_DISCOVERY,
841
982
  ...SUCCESS_STATUSES,
@@ -848,6 +989,7 @@ export const AvProtocolFixtures = [
848
989
  ...ABSENCE,
849
990
  ...TRANSACTIONS,
850
991
  ...TRANSACTION_REFERENCES,
992
+ ...ACCESS_QUESTIONS,
851
993
  ...INTERNAL,
852
994
  ...CONFLICT,
853
995
  ...WIRE_SCALARS,
@@ -4,7 +4,7 @@ import type { AvProtocolFixture } from "./protocol-fixture.js";
4
4
  * its bytes: `prefix + repeat × times + suffix`. The one oversized-body row
5
5
  * would otherwise be a megabyte of `x`.
6
6
  */
7
- export type ProtocolFixtureContentRecipe = {
7
+ type ProtocolFixtureContentRecipe = {
8
8
  readonly prefix?: string;
9
9
  readonly repeat: string;
10
10
  readonly times: number;
@@ -41,3 +41,4 @@ export declare function expandProtocolFixtureRow(row: ProtocolFixtureJsonRow): A
41
41
  * in corpus order, so a change to the corpus is a change to its own lines.
42
42
  */
43
43
  export declare function protocolFixturesJson(): Promise<string>;
44
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { AVENTARA_PROTOCOL_VERSION } from "@aventara/core/internal";
2
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";
3
+ import { CORPUS_ACCESS_CLIENT_CONTRACT_HASH, CORPUS_CLIENT_CONTRACT_HASH, CORPUS_ENTRYPOINT, CORPUS_NONE_CLIENT_CONTRACT_HASH, createCorpusFramework, } from "./corpus-contract.fixture.js";
4
4
  import { AvProtocolFixtures } from "./protocol-fixtures.js";
5
5
  const RECIPE_MINIMUM = 1024;
6
6
  function recipeOf(text) {
@@ -78,6 +78,10 @@ export async function protocolFixturesJson() {
78
78
  hash: CORPUS_NONE_CLIENT_CONTRACT_HASH,
79
79
  document: await contractDocument(CORPUS_NONE_CLIENT_CONTRACT_HASH),
80
80
  },
81
+ {
82
+ hash: CORPUS_ACCESS_CLIENT_CONTRACT_HASH,
83
+ document: await contractDocument(CORPUS_ACCESS_CLIENT_CONTRACT_HASH),
84
+ },
81
85
  ];
82
86
  const head = JSON.stringify({
83
87
  protocolVersion: AVENTARA_PROTOCOL_VERSION,