@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.
- package/CHANGELOG.md +544 -1
- package/README.md +23 -22
- package/dist/adapters/adapter-factory.d.ts +2 -2
- package/dist/adapters/fake-adapter.d.ts +16 -16
- package/dist/index.d.ts +2 -2
- package/dist/protocol/corpus-contract.fixture.d.ts +226 -51
- package/dist/protocol/corpus-contract.fixture.js +74 -35
- package/dist/protocol/in-process.driver.d.ts +2 -2
- package/dist/protocol/in-process.driver.js +2 -0
- package/dist/protocol/per-route.driver.d.ts +2 -1
- package/dist/protocol/per-route.driver.js +2 -0
- package/dist/protocol/protocol-conformance.d.ts +4 -4
- package/dist/protocol/protocol-corpus.d.ts +6 -4
- package/dist/protocol/protocol-driver.d.ts +2 -2
- package/dist/protocol/protocol-fixture.d.ts +4 -4
- package/dist/protocol/protocol-fixtures.js +146 -4
- package/dist/protocol/protocol-fixtures.json-codec.d.ts +2 -1
- package/dist/protocol/protocol-fixtures.json-codec.js +5 -1
- package/fixtures/protocol-v1.json +113 -100
- package/package.json +3 -3
|
@@ -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`, `/
|
|
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)`
|
|
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
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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<
|
|
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
|
|
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 {
|
|
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
|
}>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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:
|
|
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?:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,
|