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

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,3 +1,4 @@
1
+ import { AvProtocol, type AvProtocolResponse } from "@aventara/core/protocol";
1
2
  import type { AvProtocolDriver } from "./protocol-driver.js";
2
3
  /**
3
4
  * The protocol hosted in process, with no HTTP stack: `/_contract` goes to
@@ -6,3 +7,5 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
6
7
  * `protocol.handleOperation`, and the answer is written back as it is.
7
8
  */
8
9
  export declare const inProcessDriver: AvProtocolDriver;
10
+ /** An Operation Set's answer as a host writes it: a streamed one's lines, in order, as its body text. */
11
+ export declare function written(answer: Awaited<ReturnType<ReturnType<typeof AvProtocol.bind>["handleAll"]>>): Promise<AvProtocolResponse>;
@@ -10,6 +10,8 @@ export const inProcessDriver = async (framework) => {
10
10
  return protocol.handleTransaction(request);
11
11
  case "/_access/can":
12
12
  return protocol.handleCan(request);
13
+ case "/_all":
14
+ return written(await protocol.handleAll(request));
13
15
  default:
14
16
  return protocol.handleOperation(request);
15
17
  }
@@ -17,3 +19,13 @@ export const inProcessDriver = async (framework) => {
17
19
  close: async () => undefined,
18
20
  };
19
21
  };
22
+ export async function written(answer) {
23
+ if (typeof answer.body === "string") {
24
+ return answer;
25
+ }
26
+ let body = "";
27
+ for await (const line of answer.body) {
28
+ body += line;
29
+ }
30
+ return { status: answer.status, headers: answer.headers, body };
31
+ }
@@ -3,7 +3,7 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
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
5
  * `/_transactions` on `handleTransaction`, `/_access/can` on `handleCan`,
6
- * `/_contract` on `encodeContract`,
6
+ * `/_all` on `handleAll`, `/_contract` on `encodeContract`,
7
7
  * matched by method and path) and uses `protocol.answerAbsent` as the fallback
8
8
  * for everything it did not mount. `undefined` from the fallback is a path
9
9
  * outside the protocol, which this host answers with its own bodiless 404.
@@ -1,4 +1,5 @@
1
1
  import { AvProtocol } from "@aventara/core/protocol";
2
+ import { written } from "./in-process.driver.js";
2
3
  const HOST_NOT_FOUND = Object.freeze({
3
4
  status: 404,
4
5
  headers: Object.freeze({}),
@@ -16,6 +17,8 @@ export const perRouteDriver = async (framework) => {
16
17
  return protocol.handleTransaction(request);
17
18
  case "can":
18
19
  return protocol.handleCan(request);
20
+ case "all":
21
+ return written(await protocol.handleAll(request));
19
22
  case "contract":
20
23
  return protocol.encodeContract(request);
21
24
  default:
@@ -8,8 +8,9 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
8
8
  * answer, so they are skipped. Each driven row runs against a fresh corpus
9
9
  * Framework, through a fresh driver session, with one request (the corpus
10
10
  * Framework whose ClientContract hash the row's `Aventara-Contract-Hash`
11
- * names; the main corpus contract for any other hash), and its answer is
12
- * held to the row's `server` block:
11
+ * names, or a cached read's `v` when it sends no such header; the main corpus
12
+ * contract for any other hash), and its answer is held to the row's `server`
13
+ * block:
13
14
  *
14
15
  * - `status`, exactly;
15
16
  * - `code`: the body is a framework envelope carrying that code or, for
@@ -70,7 +70,11 @@ function mismatchesOf(server, response, adapterCalls) {
70
70
  }
71
71
  function contractHashOf(request) {
72
72
  const value = Object.entries(request.headers).find(([name]) => name.toLowerCase() === "aventara-contract-hash")?.[1];
73
- return typeof value === "string" ? value : undefined;
73
+ if (typeof value === "string") {
74
+ return value;
75
+ }
76
+ const v = new URLSearchParams(request.query ?? "").get("v");
77
+ return v === null ? undefined : v;
74
78
  }
75
79
  function parsedBody(text) {
76
80
  try {
@@ -18,8 +18,9 @@ export declare const AvProtocolCorpus: {
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
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
+ * `access` option for its hash (the `/_access/can` rows send it), the one with
22
+ * cached reads for its hash (the cached-read rows send it as their query's
23
+ * `v`), the main one for anything else.
23
24
  */
24
25
  readonly framework: (contractHash?: string) => Promise<AvFramework>;
25
26
  /** Where the corpus Framework is mounted; every row's `path` is relative to it. */
@@ -1,4 +1,4 @@
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";
1
+ import { CORPUS_ACCESS_CLIENT_CONTRACT_HASH, CORPUS_AUTHOR, CORPUS_CACHE_CLIENT_CONTRACT_HASH, 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",
@@ -212,6 +212,7 @@ const contractServed = (label, requestId, headers = {}) => ({
212
212
  ...answeredHeaders(requestId),
213
213
  ETag: CORPUS_ETAG,
214
214
  "Cache-Control": "no-cache",
215
+ "X-Content-Type-Options": "nosniff",
215
216
  },
216
217
  bodyBytes: CORPUS_CLIENT_CONTRACT_DOCUMENT,
217
218
  },
@@ -228,6 +229,7 @@ const contractNotModified = (label, requestId, ifNoneMatch) => ({
228
229
  "Aventara-Request-Id": requestId,
229
230
  ETag: CORPUS_ETAG,
230
231
  "Cache-Control": "no-cache",
232
+ "X-Content-Type-Options": "nosniff",
231
233
  },
232
234
  bodyBytes: "",
233
235
  },
@@ -977,6 +979,340 @@ const ACCESS_QUESTIONS = [
977
979
  cause: { message: "Adapter execution failed." },
978
980
  }),
979
981
  ];
982
+ const setEntry = (resource, operation, args = {}) => {
983
+ const [family, variant] = operation.split(".");
984
+ return { resource, family, variant, args };
985
+ };
986
+ const setRequest = (requestId, operations, headers = {}, overrides = {}) => {
987
+ const base = corpusRequest("", { operations }, requestId);
988
+ return {
989
+ ...base,
990
+ path: "/_all",
991
+ headers: { ...base.headers, ...headers },
992
+ ...overrides,
993
+ };
994
+ };
995
+ const countEntries = (count) => Object.fromEntries(Array.from({ length: count }, (_, index) => [
996
+ `k${index}`,
997
+ setEntry("authors", "find.count"),
998
+ ]));
999
+ const NDJSON = "application/x-ndjson";
1000
+ const SET_INVALID = "Operation Set failed framework validation.";
1001
+ const OVER_MAX_ALL = {
1002
+ data: null,
1003
+ code: "A2009",
1004
+ cause: {
1005
+ message: "Operation count exceeds the active Contract limit.",
1006
+ issues: [
1007
+ {
1008
+ code: "V1014",
1009
+ path: ["operations"],
1010
+ message: "Operation count exceeds maxAll (10).",
1011
+ },
1012
+ ],
1013
+ },
1014
+ };
1015
+ const AUTHOR_LIST = {
1016
+ data: [CORPUS_AUTHOR],
1017
+ code: "A1000",
1018
+ cause: null,
1019
+ };
1020
+ const AUTHOR_MISSING = {
1021
+ data: null,
1022
+ code: "A2003",
1023
+ cause: { message: NO_MATCH },
1024
+ };
1025
+ const OPERATION_SETS = [
1026
+ answered("all: reads and writes in one request → 200 A1000, each key its single route's envelope", setRequest("corpus-all-mixed", {
1027
+ list: setEntry("authors", "find.many"),
1028
+ total: setEntry("authors", "find.count"),
1029
+ missing: setEntry("authors", "find.unique", {
1030
+ where: { id: CORPUS_NOBODY },
1031
+ }),
1032
+ created: setEntry("authors", "create.one", { data: DATA }),
1033
+ denied: setEntry("authors", "find.first", {
1034
+ where: { id: CORPUS_TRIGGER.guardDenies },
1035
+ }),
1036
+ hidden: setEntry("hiddenRes", "find.many"),
1037
+ }), 200, 4, {
1038
+ data: {
1039
+ list: AUTHOR_LIST,
1040
+ total: { data: 2, code: "A1000", cause: null },
1041
+ missing: AUTHOR_MISSING,
1042
+ created: { data: CORPUS_AUTHOR, code: "A1002", cause: null },
1043
+ denied: {
1044
+ data: null,
1045
+ code: "A4001",
1046
+ cause: { message: "Operation denied by a framework guard." },
1047
+ },
1048
+ hidden: {
1049
+ data: null,
1050
+ code: "A2001",
1051
+ cause: {
1052
+ message: RESOURCE_UNAVAILABLE,
1053
+ issues: [
1054
+ {
1055
+ code: "V1005",
1056
+ path: ["resource"],
1057
+ message: 'Resource "hiddenRes" is not available in the active Contract.',
1058
+ },
1059
+ ],
1060
+ },
1061
+ },
1062
+ },
1063
+ code: "A1000",
1064
+ cause: null,
1065
+ }),
1066
+ {
1067
+ label: "all: __proto__ and constructor are plain keys",
1068
+ request: {
1069
+ ...setRequest("corpus-all-hostile-keys", {}),
1070
+ body: {
1071
+ kind: "raw",
1072
+ content: '{"operations":{"__proto__":{"resource":"authors","family":"find","variant":"count","args":{}},"constructor":{"resource":"authors","family":"find","variant":"count","args":{}}}}',
1073
+ },
1074
+ },
1075
+ server: {
1076
+ status: 200,
1077
+ code: "A1000",
1078
+ adapterCalls: 2,
1079
+ responseHeaders: answeredHeaders("corpus-all-hostile-keys"),
1080
+ bodyBytes: '{"data":{"__proto__":{"data":2,"code":"A1000","cause":null},"constructor":{"data":2,"code":"A1000","cause":null}},"code":"A1000","cause":null}',
1081
+ },
1082
+ client: { outcome: "resolve" },
1083
+ },
1084
+ {
1085
+ label: "all: streamed (Accept: application/x-ndjson) → 200, one line per entry in settle order",
1086
+ request: setRequest("corpus-all-streamed", {
1087
+ list: setEntry("authors", "find.many"),
1088
+ missing: setEntry("authors", "find.unique", {
1089
+ where: { id: CORPUS_NOBODY },
1090
+ }),
1091
+ }, { Accept: NDJSON }),
1092
+ server: {
1093
+ status: 200,
1094
+ code: null,
1095
+ adapterCalls: 2,
1096
+ responseHeaders: {
1097
+ "Content-Type": NDJSON,
1098
+ "Cache-Control": "no-store",
1099
+ "X-Accel-Buffering": "no",
1100
+ "Aventara-Request-Id": "corpus-all-streamed",
1101
+ },
1102
+ bodyBytes: `${JSON.stringify({ key: "missing", envelope: AUTHOR_MISSING })}\n${JSON.stringify({ key: "list", envelope: AUTHOR_LIST })}\n`,
1103
+ },
1104
+ client: { outcome: "resolve" },
1105
+ },
1106
+ answered("all: refused whole while streamed is plain JSON at its status (more than maxAll → 422 A2009)", setRequest("corpus-all-streamed-refused", countEntries(11), {
1107
+ Accept: NDJSON,
1108
+ }), 422, 0, OVER_MAX_ALL),
1109
+ answered("all: more entries than limits.maxAll → 422 A2009 / V1014", setRequest("corpus-all-over-max", countEntries(11)), 422, 0, OVER_MAX_ALL),
1110
+ answered("all: no entries → 422 A2004 / V1004", setRequest("corpus-all-empty", {}), 422, 0, {
1111
+ data: null,
1112
+ code: "A2004",
1113
+ cause: {
1114
+ message: SET_INVALID,
1115
+ issues: [
1116
+ {
1117
+ code: "V1004",
1118
+ path: ["operations"],
1119
+ message: "Value outside accepted range.",
1120
+ },
1121
+ ],
1122
+ },
1123
+ }),
1124
+ answered("all: a key that is no identifier → 422 A2004 / V1001", setRequest("corpus-all-bad-key", {
1125
+ "a-b": setEntry("authors", "find.count"),
1126
+ }), 422, 0, {
1127
+ data: null,
1128
+ code: "A2004",
1129
+ cause: {
1130
+ message: SET_INVALID,
1131
+ issues: [
1132
+ {
1133
+ code: "V1001",
1134
+ path: ["operations"],
1135
+ message: "Every operation key must be an identifier.",
1136
+ },
1137
+ ],
1138
+ },
1139
+ }),
1140
+ answered("all: an entry with a fifth member → 422 A2004 / V1001 at it", setRequest("corpus-all-entry-member", {
1141
+ a: { ...setEntry("authors", "find.count"), extra: 1 },
1142
+ }), 422, 0, {
1143
+ data: null,
1144
+ code: "A2004",
1145
+ cause: {
1146
+ message: SET_INVALID,
1147
+ issues: [
1148
+ {
1149
+ code: "V1001",
1150
+ path: ["operations", "a", "extra"],
1151
+ message: 'Property "extra" is not valid here.',
1152
+ },
1153
+ ],
1154
+ },
1155
+ }),
1156
+ answered("all: a body member beside operations → 422 A2004 / V1001 at it", {
1157
+ ...setRequest("corpus-all-body-member", {}),
1158
+ body: {
1159
+ kind: "raw",
1160
+ content: JSON.stringify({
1161
+ operations: { a: setEntry("authors", "find.count") },
1162
+ extra: 1,
1163
+ }),
1164
+ },
1165
+ }, 422, 0, {
1166
+ data: null,
1167
+ code: "A2004",
1168
+ cause: {
1169
+ message: SET_INVALID,
1170
+ issues: [
1171
+ {
1172
+ code: "V1001",
1173
+ path: ["extra"],
1174
+ message: 'Property "extra" is not valid here.',
1175
+ },
1176
+ ],
1177
+ },
1178
+ }),
1179
+ refused("all: a stale hash → 409 A2005, nothing runs", setRequest("corpus-all-stale", { a: setEntry("authors", "find.count") }, { "Aventara-Contract-Hash": STALE_HASH }), 409, "A2005", STALE_CLIENT),
1180
+ refused("method: GET on /_all → 405 A2012 with Allow: POST", setRequest("corpus-all-get", { a: setEntry("authors", "find.count") }, {}, { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
1181
+ ];
1182
+ const CACHE_V = encodeURIComponent(CORPUS_CACHE_CLIENT_CONTRACT_HASH);
1183
+ const plainQ = (args) => encodeURIComponent(JSON.stringify(args));
1184
+ const DEFLATED_WHERE_A1 = "q1Yqz0gtSlWyqlbKTFGyUko0VKqtBQA";
1185
+ const DEFLATE_BOMB = `7cEBDQAwCAMwK89k3A06CN4RQttO5ecB${"A".repeat(2582)}DnZBY`;
1186
+ const cachedRead = (route, query, requestId, extra = {}, method = "GET") => ({
1187
+ method,
1188
+ path: `/_resources/${route}`,
1189
+ ...(query === undefined ? {} : { query }),
1190
+ headers: { "Aventara-Request-Id": requestId, ...extra },
1191
+ body: { kind: "raw", content: "" },
1192
+ });
1193
+ const cacheQuery = (args) => `q=${plainQ(args)}&v=${CACHE_V}`;
1194
+ const GET_HEADERS = {
1195
+ "Referrer-Policy": "no-referrer",
1196
+ "X-Content-Type-Options": "nosniff",
1197
+ };
1198
+ const GET_REFUSED = { "Cache-Control": "no-store", ...GET_HEADERS };
1199
+ const USER_CACHE = {
1200
+ "Cache-Control": "private, max-age=60",
1201
+ Vary: "Authorization, Cookie",
1202
+ ...GET_HEADERS,
1203
+ };
1204
+ const AUTHOR_LIST_ETAG = '"Hw1Lk7KqP84xBk693Mz4B2LG8SHbThNf7TUw48xZUGI"';
1205
+ const AUTHOR_ETAG = '"bY0J9tlkShjFaZpC9KaS3xX3YaoRzKji-HAb69ASAsQ"';
1206
+ const EVERYONE_CACHE = {
1207
+ "Cache-Control": "public, max-age=300",
1208
+ ...GET_HEADERS,
1209
+ };
1210
+ const BOOK_LIST_ETAG = '"boybbH17GgkJBSByRCwm0xJj3tbWWrPd2O0BPrDCNSo"';
1211
+ const BOOK_LIST = {
1212
+ data: [{ authorId: CORPUS_AUTHOR.id, id: "b1", label: "book:b1" }],
1213
+ code: "A1000",
1214
+ cause: null,
1215
+ };
1216
+ const AUTHOR_FOUND = {
1217
+ data: CORPUS_AUTHOR,
1218
+ code: "A1000",
1219
+ cause: null,
1220
+ };
1221
+ const CACHED_READS = [
1222
+ answered('cache: GET for "user", plain q → 200 private, Vary, ETag', cachedRead("authors/find/many", cacheQuery({}), "corpus-cache-user"), 200, 1, AUTHOR_LIST, {
1223
+ ...answeredHeaders("corpus-cache-user"),
1224
+ ...USER_CACHE,
1225
+ ETag: AUTHOR_LIST_ETAG,
1226
+ }),
1227
+ answered("cache: GET, compressed q → the plain form's answer", cachedRead("authors/find/unique", `q=${DEFLATED_WHERE_A1}&v=${CACHE_V}`, "corpus-cache-deflated"), 200, 1, AUTHOR_FOUND, {
1228
+ ...answeredHeaders("corpus-cache-deflated"),
1229
+ ...USER_CACHE,
1230
+ ETag: AUTHOR_ETAG,
1231
+ }),
1232
+ {
1233
+ label: "cache: If-None-Match its ETag → 304, no body, the same validators",
1234
+ request: cachedRead("authors/find/many", cacheQuery({}), "corpus-cache-304", { "If-None-Match": AUTHOR_LIST_ETAG }),
1235
+ server: {
1236
+ status: 304,
1237
+ code: null,
1238
+ adapterCalls: 1,
1239
+ responseHeaders: {
1240
+ "Aventara-Request-Id": "corpus-cache-304",
1241
+ ...USER_CACHE,
1242
+ ETag: AUTHOR_LIST_ETAG,
1243
+ },
1244
+ bodyBytes: "",
1245
+ },
1246
+ client: { outcome: "resolve" },
1247
+ },
1248
+ answered('cache: GET for "everyone" → 200 public, no Vary, no request id', cachedRead("books/find/many", cacheQuery({}), "corpus-cache-everyone"), 200, 1, BOOK_LIST, {
1249
+ "Content-Type": "application/json",
1250
+ ...EVERYONE_CACHE,
1251
+ ETag: BOOK_LIST_ETAG,
1252
+ }),
1253
+ {
1254
+ label: "cache: If-None-Match weak W/ on a public answer → 304",
1255
+ request: cachedRead("books/find/many", cacheQuery({}), "corpus-cache-public-304", { "If-None-Match": `W/${BOOK_LIST_ETAG}` }),
1256
+ server: {
1257
+ status: 304,
1258
+ code: null,
1259
+ adapterCalls: 1,
1260
+ responseHeaders: { ...EVERYONE_CACHE, ETag: BOOK_LIST_ETAG },
1261
+ bodyBytes: "",
1262
+ },
1263
+ client: { outcome: "resolve" },
1264
+ },
1265
+ answered('cache: GET for "anonymous", no Cookie or Authorization → public', cachedRead("authors/find/first", cacheQuery({}), "corpus-cache-anonymous"), 200, 1, AUTHOR_FOUND, {
1266
+ "Content-Type": "application/json",
1267
+ "Cache-Control": "public, max-age=30",
1268
+ Vary: "Authorization, Cookie",
1269
+ ...GET_HEADERS,
1270
+ ETag: AUTHOR_ETAG,
1271
+ }),
1272
+ answered('cache: GET for "anonymous" with a Cookie → private', cachedRead("authors/find/first", cacheQuery({}), "corpus-cache-signed-in", {
1273
+ Cookie: "session=s1",
1274
+ }), 200, 1, AUTHOR_FOUND, {
1275
+ ...answeredHeaders("corpus-cache-signed-in"),
1276
+ "Cache-Control": "private, max-age=30",
1277
+ Vary: "Authorization, Cookie",
1278
+ ...GET_HEADERS,
1279
+ ETag: AUTHOR_ETAG,
1280
+ }),
1281
+ refused("cache: a query parameter beside q and v → 400 A2000, no-store", cachedRead("authors/find/many", `${cacheQuery({})}&utm=1`, "corpus-cache-extra-param"), 400, "A2000", MALFORMED, GET_REFUSED),
1282
+ refused("cache: q that is no JSON object → 400 A2000, no-store", cachedRead("authors/find/many", cacheQuery([1]), "corpus-cache-q-list"), 400, "A2000", MALFORMED, GET_REFUSED),
1283
+ refused("cache: a compressed q inflating past maxRequestBytes → 413 A2010, no-store", cachedRead("authors/find/many", `q=${DEFLATE_BOMB}&v=${CACHE_V}`, "corpus-cache-bomb"), 413, "A2010", "The request body exceeds the maximum request size.", GET_REFUSED),
1284
+ refused("cache: a stale v → 409 A2005, no-store", cachedRead("authors/find/many", `q=${plainQ({})}&v=${encodeURIComponent(STALE_HASH)}`, "corpus-cache-stale"), 409, "A2005", STALE_CLIENT, GET_REFUSED),
1285
+ {
1286
+ ...refused("cache: a unique miss → 404 A2003, no-store", cachedRead("authors/find/unique", cacheQuery({ where: { id: CORPUS_NOBODY } }), "corpus-cache-miss"), 404, "A2003", NO_MATCH, GET_REFUSED),
1287
+ server: {
1288
+ status: 404,
1289
+ code: "A2003",
1290
+ causeKeys: ["message"],
1291
+ adapterCalls: 1,
1292
+ responseHeaders: {
1293
+ ...answeredHeaders("corpus-cache-miss"),
1294
+ ...GET_REFUSED,
1295
+ },
1296
+ bodyBytes: JSON.stringify({
1297
+ data: null,
1298
+ code: "A2003",
1299
+ cause: { message: NO_MATCH },
1300
+ }),
1301
+ },
1302
+ },
1303
+ refused("cache: the pipelines run on a GET: a guard refusal → 403 A4001, no-store", cachedRead("authors/find/many", cacheQuery({ where: { id: CORPUS_TRIGGER.guardDenies } }), "corpus-cache-denied"), 403, "A4001", "Operation denied by a framework guard.", GET_REFUSED),
1304
+ refused("method: GET on a write → 405 A2012 with Allow: POST, no-store", cachedRead("authors/create/one", cacheQuery({ data: DATA }), "corpus-cache-get-write"), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST", ...GET_REFUSED }),
1305
+ refused("method: GET on a read with no cache stage → 405 A2012 with Allow: POST, no-store", cachedRead("samples/find/unique", cacheQuery({ where: { id: "s1" } }), "corpus-cache-get-uncached"), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST", ...GET_REFUSED }),
1306
+ refused("method: PUT on a cached read → 405 A2012 with Allow: GET, POST", {
1307
+ ...corpusRequest("find/many", {}, "corpus-cache-put", { method: "PUT" }),
1308
+ headers: {
1309
+ "Content-Type": "application/json",
1310
+ "Aventara-Protocol-Version": "1",
1311
+ "Aventara-Contract-Hash": CORPUS_CACHE_CLIENT_CONTRACT_HASH,
1312
+ "Aventara-Request-Id": "corpus-cache-put",
1313
+ },
1314
+ }, 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "GET, POST" }),
1315
+ ];
980
1316
  export const AvProtocolFixtures = [
981
1317
  ...CONTRACT_DISCOVERY,
982
1318
  ...SUCCESS_STATUSES,
@@ -990,6 +1326,8 @@ export const AvProtocolFixtures = [
990
1326
  ...TRANSACTIONS,
991
1327
  ...TRANSACTION_REFERENCES,
992
1328
  ...ACCESS_QUESTIONS,
1329
+ ...OPERATION_SETS,
1330
+ ...CACHED_READS,
993
1331
  ...INTERNAL,
994
1332
  ...CONFLICT,
995
1333
  ...WIRE_SCALARS,
@@ -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_ACCESS_CLIENT_CONTRACT_HASH, 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_CACHE_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) {
@@ -82,6 +82,10 @@ export async function protocolFixturesJson() {
82
82
  hash: CORPUS_ACCESS_CLIENT_CONTRACT_HASH,
83
83
  document: await contractDocument(CORPUS_ACCESS_CLIENT_CONTRACT_HASH),
84
84
  },
85
+ {
86
+ hash: CORPUS_CACHE_CLIENT_CONTRACT_HASH,
87
+ document: await contractDocument(CORPUS_CACHE_CLIENT_CONTRACT_HASH),
88
+ },
85
89
  ];
86
90
  const head = JSON.stringify({
87
91
  protocolVersion: AVENTARA_PROTOCOL_VERSION,