@stratal/testing 0.0.27 → 0.1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +562 -0
  2. package/README.md +137 -36
  3. package/dist/database/index.d.mts +2 -2
  4. package/dist/database/index.mjs +2 -2
  5. package/dist/database-Rojs69r3.mjs +453 -0
  6. package/dist/database-Rojs69r3.mjs.map +1 -0
  7. package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
  8. package/dist/feature-flags/index.mjs +1 -1
  9. package/dist/{feature-flags-BiLhfSGh.mjs → feature-flags-CZ1a4M2g.mjs} +2 -2
  10. package/dist/{feature-flags-BiLhfSGh.mjs.map → feature-flags-CZ1a4M2g.mjs.map} +1 -1
  11. package/dist/index-B8Mw1Dxk.d.mts +175 -0
  12. package/dist/index-B8Mw1Dxk.d.mts.map +1 -0
  13. package/dist/{index-CrHzUDKX.d.mts → index-D7FM6EKp.d.mts} +21 -3
  14. package/dist/index-D7FM6EKp.d.mts.map +1 -0
  15. package/dist/index-qgWNJRdC.d.mts.map +1 -1
  16. package/dist/index.d.mts +105 -46
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +218 -108
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/mocks/cloudflare-workers.d.mts +24 -0
  21. package/dist/mocks/cloudflare-workers.d.mts.map +1 -0
  22. package/dist/mocks/cloudflare-workers.mjs +28 -0
  23. package/dist/mocks/cloudflare-workers.mjs.map +1 -0
  24. package/dist/mocks/index.d.mts +2 -2
  25. package/dist/mocks/index.mjs +2 -2
  26. package/dist/mocks/noop-rate-limiter-store.d.mts +1 -3
  27. package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
  28. package/dist/mocks/zenstack-language.d.mts +26 -29
  29. package/dist/mocks/zenstack-language.d.mts.map +1 -1
  30. package/dist/mocks/zenstack-language.mjs +1 -1
  31. package/dist/mocks/zenstack-language.mjs.map +1 -1
  32. package/dist/storage/index.d.mts +1 -1
  33. package/dist/storage/index.mjs +1 -1
  34. package/dist/{storage-DhoxWqyF.mjs → storage-BiGamMA8.mjs} +73 -6
  35. package/dist/storage-BiGamMA8.mjs.map +1 -0
  36. package/dist/test-worker-exports-dCRARYEc.d.mts +144 -0
  37. package/dist/test-worker-exports-dCRARYEc.d.mts.map +1 -0
  38. package/dist/test-workers-cache-D02Pt_dE.mjs +183 -0
  39. package/dist/test-workers-cache-D02Pt_dE.mjs.map +1 -0
  40. package/dist/vitest-plugin/index.d.mts +14 -28
  41. package/dist/vitest-plugin/index.d.mts.map +1 -1
  42. package/dist/vitest-plugin/index.mjs +32 -20
  43. package/dist/vitest-plugin/index.mjs.map +1 -1
  44. package/package.json +30 -21
  45. package/dist/database-B02eYKhE.mjs +0 -334
  46. package/dist/database-B02eYKhE.mjs.map +0 -1
  47. package/dist/index-BIr5nLof.d.mts +0 -122
  48. package/dist/index-BIr5nLof.d.mts.map +0 -1
  49. package/dist/index-CrHzUDKX.d.mts.map +0 -1
  50. package/dist/storage-DhoxWqyF.mjs.map +0 -1
  51. package/dist/test-email-provider-B7cjj97-.d.mts +0 -21
  52. package/dist/test-email-provider-B7cjj97-.d.mts.map +0 -1
  53. package/dist/test-email-provider-Dr-nhE0x.mjs +0 -34
  54. package/dist/test-email-provider-Dr-nhE0x.mjs.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import { IRateLimiterStore } from "stratal/rate-limiter";
2
-
3
2
  //#region src/mocks/noop-rate-limiter-store.d.ts
4
3
  /**
5
4
  * Rate-limit store that never persists, so every read misses and every
@@ -11,11 +10,10 @@ import { IRateLimiterStore } from "stratal/rate-limiter";
11
10
  * this store via the framework bridge). Suites that want to test limiting
12
11
  * behavior can override `RATE_LIMITER_TOKENS.Store` back to a real store.
13
12
  */
14
- declare class NoopRateLimiterStore implements IRateLimiterStore {
13
+ export declare class NoopRateLimiterStore implements IRateLimiterStore {
15
14
  get<T = unknown>(_key: string): Promise<T | null>;
16
15
  set<T = unknown>(_key: string, _value: T, _ttlSeconds: number): Promise<void>;
17
16
  delete(_key: string): Promise<void>;
18
17
  }
19
18
  //#endregion
20
- export { NoopRateLimiterStore };
21
19
  //# sourceMappingURL=noop-rate-limiter-store.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"noop-rate-limiter-store.d.mts","names":[],"sources":["../../src/mocks/noop-rate-limiter-store.ts"],"mappings":";;;;;AAYA;;;;;;;;cAAa,oBAAA,YAAgC,iBAAA;EAC3C,GAAA,cAAiB,IAAA,WAAe,OAAA,CAAQ,CAAA;EAIxC,GAAA,cAAiB,IAAA,UAAc,MAAA,EAAQ,CAAA,EAAG,WAAA,WAAsB,OAAA;EAKhE,MAAA,CAAO,IAAA,WAAe,OAAA;AAAA"}
1
+ {"version":3,"file":"noop-rate-limiter-store.d.mts","names":[],"sources":["../../src/mocks/noop-rate-limiter-store.ts"],"mappings":";;;;;;;;;;;;qBAYa,gCAAgC;EAC3C,IAAI,aAAa,eAAe,QAAQ;EAIxC,IAAI,aAAa,cAAc,QAAQ,GAAG,sBAAsB;EAKhE,OAAO,eAAe"}
@@ -6,43 +6,40 @@
6
6
  * incompatible with Cloudflare Workers runtime. These exports are only used
7
7
  * for schema generation (CLI functionality), not runtime authentication.
8
8
  *
9
- * This mock provides stub implementations to allow tests to run in vitest-pool-workers.
9
+ * This mock provides stub implementations to allow tests to run in the Workers Vitest integration.
10
10
  */
11
- declare const formatDocument: (content: string) => string;
12
- declare const loadDocument: (_path: string) => {
11
+ export declare const formatDocument: (content: string) => string;
12
+ export declare const loadDocument: (_path: string) => {
13
13
  success: boolean;
14
14
  errors: string[];
15
15
  };
16
- declare class ZModelCodeGenerator {
16
+ export declare class ZModelCodeGenerator {
17
17
  generate(_model: unknown): string;
18
18
  }
19
- declare const isDataModel: (_item: unknown) => boolean;
20
- declare const isAttribute: (_item: unknown) => boolean;
21
- declare const isAbstractDeclaration: (_item: unknown) => boolean;
22
- declare const isExpression: (_item: unknown) => boolean;
23
- declare const isLiteralExpr: (_item: unknown) => boolean;
24
- declare const isArrayExpr: (_item: unknown) => boolean;
25
- declare const isAttributeArg: (_item: unknown) => boolean;
26
- declare const isBinaryExpr: (_item: unknown) => boolean;
27
- declare const isBooleanLiteral: (_item: unknown) => boolean;
28
- declare const isConfigArrayExpr: (_item: unknown) => boolean;
29
- declare const isConfigField: (_item: unknown) => boolean;
30
- declare const isDataField: (_item: unknown) => boolean;
31
- declare const isDataFieldAttribute: (_item: unknown) => boolean;
32
- declare const isDataFieldType: (_item: unknown) => boolean;
33
- declare const isDataModelAttribute: (_item: unknown) => boolean;
34
- declare const hasAttribute: (_node: unknown, _name?: string) => boolean;
19
+ export declare const isDataModel: (_item: unknown) => boolean;
20
+ export declare const isAttribute: (_item: unknown) => boolean;
21
+ export declare const isAbstractDeclaration: (_item: unknown) => boolean;
22
+ export declare const isExpression: (_item: unknown) => boolean;
23
+ export declare const isLiteralExpr: (_item: unknown) => boolean;
24
+ export declare const isArrayExpr: (_item: unknown) => boolean;
25
+ export declare const isAttributeArg: (_item: unknown) => boolean;
26
+ export declare const isBinaryExpr: (_item: unknown) => boolean;
27
+ export declare const isBooleanLiteral: (_item: unknown) => boolean;
28
+ export declare const isConfigArrayExpr: (_item: unknown) => boolean;
29
+ export declare const isConfigField: (_item: unknown) => boolean;
30
+ export declare const isDataField: (_item: unknown) => boolean;
31
+ export declare const isDataFieldAttribute: (_item: unknown) => boolean;
32
+ export declare const isDataFieldType: (_item: unknown) => boolean;
33
+ export declare const isDataModelAttribute: (_item: unknown) => boolean;
34
+ export declare const hasAttribute: (_node: unknown, _name?: string) => boolean;
35
35
  declare const _default: {
36
- formatDocument: (content: string) => string;
37
- loadDocument: (_path: string) => {
38
- success: boolean;
39
- errors: string[];
40
- };
36
+ formatDocument: typeof formatDocument;
37
+ loadDocument: typeof loadDocument;
41
38
  ZModelCodeGenerator: typeof ZModelCodeGenerator;
42
- isDataModel: (_item: unknown) => boolean;
43
- isAttribute: (_item: unknown) => boolean;
44
- hasAttribute: (_node: unknown, _name?: string) => boolean;
39
+ isDataModel: typeof isDataModel;
40
+ isAttribute: typeof isAttribute;
41
+ hasAttribute: typeof hasAttribute;
45
42
  };
46
43
  //#endregion
47
- export { ZModelCodeGenerator, _default as default, formatDocument, hasAttribute, isAbstractDeclaration, isArrayExpr, isAttribute, isAttributeArg, isBinaryExpr, isBooleanLiteral, isConfigArrayExpr, isConfigField, isDataField, isDataFieldAttribute, isDataFieldType, isDataModel, isDataModelAttribute, isExpression, isLiteralExpr, loadDocument };
44
+ export { _default as default };
48
45
  //# sourceMappingURL=zenstack-language.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zenstack-language.d.mts","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"mappings":";;AAWA;;;;AAA8C;AAE9C;;;cAFa,cAAA,GAAkB,OAAe;AAAA,cAEjC,YAAA,GAAgB,KAAA;;;;cAKhB,mBAAA;EACZ,QAAA,CAAS,MAAe;AAAA;AAAA,cAMZ,WAAA,GAAe,KAAc;AAAA,cAC7B,WAAA,GAAe,KAAc;AAAA,cAC7B,qBAAA,GAAyB,KAAc;AAAA,cACvC,YAAA,GAAgB,KAAc;AAAA,cAC9B,aAAA,GAAiB,KAAc;AAAA,cAC/B,WAAA,GAAe,KAAc;AAAA,cAC7B,cAAA,GAAkB,KAAc;AAAA,cAChC,YAAA,GAAgB,KAAc;AAAA,cAC9B,gBAAA,GAAoB,KAAc;AAAA,cAClC,iBAAA,GAAqB,KAAc;AAAA,cACnC,aAAA,GAAiB,KAAc;AAAA,cAC/B,WAAA,GAAe,KAAc;AAAA,cAC7B,oBAAA,GAAwB,KAAc;AAAA,cACtC,eAAA,GAAmB,KAAc;AAAA,cACjC,oBAAA,GAAwB,KAAc;AAAA,cAGtC,YAAA,GAAgB,KAAA,WAAgB,KAAc;AAAA,cAAmB,QAAA;;;;;;;;;iCAAnC,KAAA;AAAA"}
1
+ {"version":3,"file":"zenstack-language.d.mts","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"mappings":";;;;;;;;;;qBAWa,iBAAc;qBAEd,eAAY;EACxB;EACA;;qBAGY;EACZ,SAAS;;qBAMG,cAAW;qBACX,cAAW;qBACX,wBAAqB;qBACrB,eAAY;qBACZ,gBAAa;qBACb,cAAW;qBACX,iBAAc;qBACd,eAAY;qBACZ,mBAAgB;qBAChB,oBAAiB;qBACjB,gBAAa;qBACb,cAAW;qBACX,uBAAoB;qBACpB,kBAAe;qBACf,uBAAoB;qBAGpB,eAAY,gBAAkB"}
@@ -6,7 +6,7 @@
6
6
  * incompatible with Cloudflare Workers runtime. These exports are only used
7
7
  * for schema generation (CLI functionality), not runtime authentication.
8
8
  *
9
- * This mock provides stub implementations to allow tests to run in vitest-pool-workers.
9
+ * This mock provides stub implementations to allow tests to run in the Workers Vitest integration.
10
10
  */
11
11
  const formatDocument = (content) => content;
12
12
  const loadDocument = (_path) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"zenstack-language.mjs","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"sourcesContent":["/**\n * Mock for @zenstackhq/language package\n *\n * The @zenstackhq/language package depends on vscode-languageserver which is\n * incompatible with Cloudflare Workers runtime. These exports are only used\n * for schema generation (CLI functionality), not runtime authentication.\n *\n * This mock provides stub implementations to allow tests to run in vitest-pool-workers.\n */\n\n// Main exports from @zenstackhq/language\nexport const formatDocument = (content: string) => content\n\nexport const loadDocument = (_path: string) => ({\n\tsuccess: false,\n\terrors: ['Mock: loadDocument not available in test environment'],\n})\n\nexport class ZModelCodeGenerator {\n\tgenerate(_model: unknown): string {\n\t\treturn ''\n\t}\n}\n\n// AST exports from @zenstackhq/language/ast\nexport const isDataModel = (_item: unknown): boolean => false\nexport const isAttribute = (_item: unknown): boolean => false\nexport const isAbstractDeclaration = (_item: unknown): boolean => false\nexport const isExpression = (_item: unknown): boolean => false\nexport const isLiteralExpr = (_item: unknown): boolean => false\nexport const isArrayExpr = (_item: unknown): boolean => false\nexport const isAttributeArg = (_item: unknown): boolean => false\nexport const isBinaryExpr = (_item: unknown): boolean => false\nexport const isBooleanLiteral = (_item: unknown): boolean => false\nexport const isConfigArrayExpr = (_item: unknown): boolean => false\nexport const isConfigField = (_item: unknown): boolean => false\nexport const isDataField = (_item: unknown): boolean => false\nexport const isDataFieldAttribute = (_item: unknown): boolean => false\nexport const isDataFieldType = (_item: unknown): boolean => false\nexport const isDataModelAttribute = (_item: unknown): boolean => false\n\n// Utils exports from @zenstackhq/language/utils\nexport const hasAttribute = (_node: unknown, _name?: string): boolean => false\n\nexport default {\n\tformatDocument,\n\tloadDocument,\n\tZModelCodeGenerator,\n\tisDataModel,\n\tisAttribute,\n\thasAttribute,\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAa,kBAAkB,YAAoB;AAEnD,MAAa,gBAAgB,WAAmB;CAC/C,SAAS;CACT,QAAQ,CAAC,sDAAsD;AAChE;AAEA,IAAa,sBAAb,MAAiC;CAChC,SAAS,QAAyB;EACjC,OAAO;CACR;AACD;AAGA,MAAa,eAAe,UAA4B;AACxD,MAAa,eAAe,UAA4B;AACxD,MAAa,yBAAyB,UAA4B;AAClE,MAAa,gBAAgB,UAA4B;AACzD,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,kBAAkB,UAA4B;AAC3D,MAAa,gBAAgB,UAA4B;AACzD,MAAa,oBAAoB,UAA4B;AAC7D,MAAa,qBAAqB,UAA4B;AAC9D,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,wBAAwB,UAA4B;AACjE,MAAa,mBAAmB,UAA4B;AAC5D,MAAa,wBAAwB,UAA4B;AAGjE,MAAa,gBAAgB,OAAgB,UAA4B;AAEzE,IAAA,4BAAe;CACd;CACA;CACA;CACA;CACA;CACA;AACD"}
1
+ {"version":3,"file":"zenstack-language.mjs","names":[],"sources":["../../src/mocks/zenstack-language.ts"],"sourcesContent":["/**\n * Mock for @zenstackhq/language package\n *\n * The @zenstackhq/language package depends on vscode-languageserver which is\n * incompatible with Cloudflare Workers runtime. These exports are only used\n * for schema generation (CLI functionality), not runtime authentication.\n *\n * This mock provides stub implementations to allow tests to run in the Workers Vitest integration.\n */\n\n// Main exports from @zenstackhq/language\nexport const formatDocument = (content: string) => content\n\nexport const loadDocument = (_path: string) => ({\n\tsuccess: false,\n\terrors: ['Mock: loadDocument not available in test environment'],\n})\n\nexport class ZModelCodeGenerator {\n\tgenerate(_model: unknown): string {\n\t\treturn ''\n\t}\n}\n\n// AST exports from @zenstackhq/language/ast\nexport const isDataModel = (_item: unknown): boolean => false\nexport const isAttribute = (_item: unknown): boolean => false\nexport const isAbstractDeclaration = (_item: unknown): boolean => false\nexport const isExpression = (_item: unknown): boolean => false\nexport const isLiteralExpr = (_item: unknown): boolean => false\nexport const isArrayExpr = (_item: unknown): boolean => false\nexport const isAttributeArg = (_item: unknown): boolean => false\nexport const isBinaryExpr = (_item: unknown): boolean => false\nexport const isBooleanLiteral = (_item: unknown): boolean => false\nexport const isConfigArrayExpr = (_item: unknown): boolean => false\nexport const isConfigField = (_item: unknown): boolean => false\nexport const isDataField = (_item: unknown): boolean => false\nexport const isDataFieldAttribute = (_item: unknown): boolean => false\nexport const isDataFieldType = (_item: unknown): boolean => false\nexport const isDataModelAttribute = (_item: unknown): boolean => false\n\n// Utils exports from @zenstackhq/language/utils\nexport const hasAttribute = (_node: unknown, _name?: string): boolean => false\n\nexport default {\n\tformatDocument,\n\tloadDocument,\n\tZModelCodeGenerator,\n\tisDataModel,\n\tisAttribute,\n\thasAttribute,\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAa,kBAAkB,YAAoB;AAEnD,MAAa,gBAAgB,WAAmB;CAC/C,SAAS;CACT,QAAQ,CAAC,sDAAsD;AAChE;AAEA,IAAa,sBAAb,MAAiC;CAChC,SAAS,QAAyB;EACjC,OAAO;CACR;AACD;AAGA,MAAa,eAAe,UAA4B;AACxD,MAAa,eAAe,UAA4B;AACxD,MAAa,yBAAyB,UAA4B;AAClE,MAAa,gBAAgB,UAA4B;AACzD,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,kBAAkB,UAA4B;AAC3D,MAAa,gBAAgB,UAA4B;AACzD,MAAa,oBAAoB,UAA4B;AAC7D,MAAa,qBAAqB,UAA4B;AAC9D,MAAa,iBAAiB,UAA4B;AAC1D,MAAa,eAAe,UAA4B;AACxD,MAAa,wBAAwB,UAA4B;AACjE,MAAa,mBAAmB,UAA4B;AAC5D,MAAa,wBAAwB,UAA4B;AAGjE,MAAa,gBAAgB,OAAgB,UAA4B;AAEzE,IAAA,4BAAe;CACd;CACA;CACA;CACA;CACA;CACA;AACD"}
@@ -1,2 +1,2 @@
1
- import { n as StoredFile, t as FakeStorageService } from "../index-CrHzUDKX.mjs";
1
+ import { n as StoredFile, t as FakeStorageService } from "../index-D7FM6EKp.mjs";
2
2
  export { FakeStorageService, type StoredFile };
@@ -1,2 +1,2 @@
1
- import { t as FakeStorageService } from "../storage-DhoxWqyF.mjs";
1
+ import { t as FakeStorageService } from "../storage-BiGamMA8.mjs";
2
2
  export { FakeStorageService };
@@ -1,8 +1,8 @@
1
- import { t as __decorate } from "./decorate-B7nr7eBl.mjs";
1
+ import { t as __decorate } from "./decorate-RQD1h28J.mjs";
2
2
  import { FileNotFoundError, STORAGE_TOKENS, StorageService } from "stratal/storage";
3
3
  import { Transient, inject } from "stratal/di";
4
4
  import { expect } from "vitest";
5
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateParam.js
5
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
6
6
  function __decorateParam(paramIndex, decorator) {
7
7
  return function(target, key) {
8
8
  decorator(target, key, paramIndex);
@@ -10,6 +10,23 @@ function __decorateParam(paramIndex, decorator) {
10
10
  }
11
11
  //#endregion
12
12
  //#region src/storage/fake-storage.service.ts
13
+ /**
14
+ * Objects per `list` page when a test does not ask for a size.
15
+ *
16
+ * One, so that any listing of two or more objects truncates and a caller who skips the `cursor`
17
+ * loop fails here rather than against a real bucket.
18
+ */
19
+ const FAKE_LIST_PAGE_SIZE = 1;
20
+ /** Shared by `head` and `list` so a listed object and a headed one cannot drift apart. */
21
+ function toHeadResult(path, file) {
22
+ return {
23
+ path,
24
+ size: file.size,
25
+ contentType: file.mimeType,
26
+ uploadedAt: file.uploadedAt,
27
+ metadata: file.metadata
28
+ };
29
+ }
13
30
  let FakeStorageService = class FakeStorageService extends StorageService {
14
31
  storageManager;
15
32
  options;
@@ -73,6 +90,56 @@ let FakeStorageService = class FakeStorageService extends StorageService {
73
90
  return Promise.resolve(this.files.has(path));
74
91
  }
75
92
  /**
93
+ * Delete many files from fake storage
94
+ */
95
+ async deleteMany(paths, disk) {
96
+ this.resolveDisk(disk);
97
+ for (const path of paths) this.files.delete(path);
98
+ return Promise.resolve();
99
+ }
100
+ /**
101
+ * Read a file's metadata from fake storage, without its contents
102
+ */
103
+ async head(path, disk) {
104
+ this.resolveDisk(disk);
105
+ const file = this.files.get(path);
106
+ if (!file) return Promise.resolve(null);
107
+ return Promise.resolve(toHeadResult(path, file));
108
+ }
109
+ /**
110
+ * List files in fake storage, one page at a time
111
+ *
112
+ * Truncates after ONE object unless `limit` says otherwise. Real R2 pages at 1000, but no
113
+ * fixture stores 1000 files — matching that number would return everything in one page and let
114
+ * a caller who never follows `cursor` pass every test, then undercount against a real bucket.
115
+ * Paging at one means any listing of two or more objects exercises the loop.
116
+ *
117
+ * Pass `limit` to widen the page when a test is asserting page contents rather than the loop.
118
+ */
119
+ async list(options = {}, disk) {
120
+ this.resolveDisk(disk);
121
+ const prefix = options.prefix ?? "";
122
+ const matching = Array.from(this.files.keys()).filter((path) => path.startsWith(prefix)).sort();
123
+ const start = options.cursor === void 0 ? 0 : matching.indexOf(options.cursor);
124
+ if (start < 0) return Promise.reject(/* @__PURE__ */ new Error(`Unknown storage list cursor: ${options.cursor ?? ""}`));
125
+ const limit = options.limit ?? FAKE_LIST_PAGE_SIZE;
126
+ const page = matching.slice(start, start + limit);
127
+ const next = matching[start + limit];
128
+ return Promise.resolve({
129
+ objects: page.map((path) => {
130
+ const object = toHeadResult(path, this.files.get(path));
131
+ if (options.includeMetadata) return object;
132
+ return {
133
+ ...object,
134
+ contentType: void 0,
135
+ metadata: void 0
136
+ };
137
+ }),
138
+ truncated: next !== void 0,
139
+ cursor: next
140
+ });
141
+ }
142
+ /**
76
143
  * Generate a fake presigned download URL
77
144
  */
78
145
  getPresignedDownloadUrl(path, expiresIn) {
@@ -96,7 +163,7 @@ let FakeStorageService = class FakeStorageService extends StorageService {
96
163
  async chunkedUpload(body, path, options, disk) {
97
164
  const content = await this.bodyToUint8Array(body);
98
165
  const size = options.size ?? content.length;
99
- return this.upload(body, path, {
166
+ return this.upload(content, path, {
100
167
  ...options,
101
168
  size
102
169
  }, disk);
@@ -170,7 +237,7 @@ let FakeStorageService = class FakeStorageService extends StorageService {
170
237
  };
171
238
  }
172
239
  async bodyToUint8Array(body) {
173
- if (!body) return new Uint8Array(0);
240
+ if (!body) return /* @__PURE__ */ new Uint8Array(0);
174
241
  if (body instanceof Uint8Array) return body;
175
242
  if (body instanceof ArrayBuffer) return new Uint8Array(body);
176
243
  if (typeof body === "string") return new TextEncoder().encode(body);
@@ -180,7 +247,7 @@ let FakeStorageService = class FakeStorageService extends StorageService {
180
247
  }
181
248
  if (body instanceof ReadableStream) return new Uint8Array(await new Response(body).arrayBuffer());
182
249
  if (body instanceof FormData || body instanceof URLSearchParams) return new Uint8Array(await new Response(body).arrayBuffer());
183
- return new Uint8Array(0);
250
+ return /* @__PURE__ */ new Uint8Array(0);
184
251
  }
185
252
  };
186
253
  FakeStorageService = __decorate([
@@ -191,4 +258,4 @@ FakeStorageService = __decorate([
191
258
  //#endregion
192
259
  export { FakeStorageService as t };
193
260
 
194
- //# sourceMappingURL=storage-DhoxWqyF.mjs.map
261
+ //# sourceMappingURL=storage-BiGamMA8.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-BiGamMA8.mjs","names":[],"sources":["../src/storage/fake-storage.service.ts"],"sourcesContent":["import { Transient, inject } from 'stratal/di'\nimport {\n FileNotFoundError,\n STORAGE_TOKENS,\n type StorageManagerService,\n StorageService,\n type StreamingBlobPayloadInputTypes,\n type DownloadResult,\n type HeadResult,\n type ListOptions,\n type ListResult,\n type PresignedUrlResult,\n type StorageConfig,\n type UploadOptions,\n type UploadResult,\n} from 'stratal/storage'\nimport { expect } from 'vitest'\n\n/**\n * Objects per `list` page when a test does not ask for a size.\n *\n * One, so that any listing of two or more objects truncates and a caller who skips the `cursor`\n * loop fails here rather than against a real bucket.\n */\nconst FAKE_LIST_PAGE_SIZE = 1\n\n/** Shared by `head` and `list` so a listed object and a headed one cannot drift apart. */\nfunction toHeadResult(path: string, file: StoredFile): HeadResult {\n return {\n path,\n size: file.size,\n contentType: file.mimeType,\n uploadedAt: file.uploadedAt,\n metadata: file.metadata,\n }\n}\n\n/**\n * Stored file representation in memory\n */\nexport interface StoredFile {\n content: Uint8Array\n mimeType: string\n size: number\n metadata?: Record<string, string>\n uploadedAt: Date\n}\n\n/**\n * FakeStorageService\n *\n * In-memory storage implementation for testing.\n * Registered by default in TestingModuleBuilder.\n *\n * Similar to Laravel's Storage::fake() - stores files in memory\n * and provides assertion helpers for testing.\n *\n * @example\n * ```typescript\n * // Access via TestingModule\n * module.storage.assertExists('path/to/file.pdf')\n * module.storage.assertMissing('deleted/file.pdf')\n * module.storage.clear() // Reset between tests\n * ```\n */\n@Transient(STORAGE_TOKENS.StorageService)\nexport class FakeStorageService extends StorageService {\n private files = new Map<string, StoredFile>()\n\n constructor(\n @inject(STORAGE_TOKENS.StorageManager)\n protected readonly storageManager: StorageManagerService,\n @inject(STORAGE_TOKENS.Options)\n protected readonly options: StorageConfig\n ) {\n super(storageManager, options)\n }\n\n /**\n * Upload content to fake storage\n */\n async upload(\n body: StreamingBlobPayloadInputTypes,\n relativePath: string,\n options: UploadOptions,\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const diskName = this.resolveDisk(disk)\n\n this.files.set(relativePath, {\n content,\n mimeType: options.mimeType ?? 'application/octet-stream',\n size: options.size,\n metadata: options.metadata,\n uploadedAt: new Date(),\n })\n\n return {\n path: relativePath,\n disk: diskName,\n fullPath: relativePath,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n /**\n * Download a file from fake storage\n */\n download(path: string): Promise<DownloadResult> {\n const file = this.files.get(path)\n\n if (!file) {\n return Promise.reject(new FileNotFoundError(path))\n }\n\n return Promise.resolve({\n toStream: () => new ReadableStream({\n start(controller) {\n controller.enqueue(file.content)\n controller.close()\n },\n }),\n toString: () => Promise.resolve(new TextDecoder().decode(file.content)),\n toArrayBuffer: () => Promise.resolve(file.content),\n contentType: file.mimeType,\n size: file.size,\n metadata: file.metadata,\n })\n }\n\n /**\n * Delete a file from fake storage\n */\n delete(path: string): Promise<void> {\n this.files.delete(path)\n return Promise.resolve()\n }\n\n /**\n * Check if a file exists in fake storage\n */\n exists(path: string): Promise<boolean> {\n return Promise.resolve(this.files.has(path))\n }\n\n /**\n * Delete many files from fake storage\n */\n // `async` so an unconfigured disk arrives as a rejected promise, as it does from the real\n // service. Thrown synchronously it would slip past a caller that chains `.catch()` without\n // awaiting — a difference in error surface is still a difference the fake exists to avoid.\n async deleteMany(paths: string[], disk?: string): Promise<void> {\n this.resolveDisk(disk)\n\n for (const path of paths) {\n this.files.delete(path)\n }\n\n return Promise.resolve()\n }\n\n /**\n * Read a file's metadata from fake storage, without its contents\n */\n async head(path: string, disk?: string): Promise<HeadResult | null> {\n this.resolveDisk(disk)\n\n const file = this.files.get(path)\n if (!file) {\n return Promise.resolve(null)\n }\n\n return Promise.resolve(toHeadResult(path, file))\n }\n\n /**\n * List files in fake storage, one page at a time\n *\n * Truncates after ONE object unless `limit` says otherwise. Real R2 pages at 1000, but no\n * fixture stores 1000 files — matching that number would return everything in one page and let\n * a caller who never follows `cursor` pass every test, then undercount against a real bucket.\n * Paging at one means any listing of two or more objects exercises the loop.\n *\n * Pass `limit` to widen the page when a test is asserting page contents rather than the loop.\n */\n async list(options: ListOptions = {}, disk?: string): Promise<ListResult> {\n this.resolveDisk(disk)\n\n const prefix = options.prefix ?? ''\n // Sorted so pagination is deterministic — an unordered Map iteration would make `cursor`\n // meaningless and the page boundaries unrepeatable between runs.\n const matching = Array.from(this.files.keys())\n .filter((path) => path.startsWith(prefix))\n .sort()\n\n const start = options.cursor === undefined ? 0 : matching.indexOf(options.cursor)\n if (start < 0) {\n return Promise.reject(new Error(`Unknown storage list cursor: ${options.cursor ?? ''}`))\n }\n\n const limit = options.limit ?? FAKE_LIST_PAGE_SIZE\n const page = matching.slice(start, start + limit)\n const next = matching[start + limit]\n\n return Promise.resolve({\n objects: page.map((path) => {\n const object = toHeadResult(path, this.files.get(path)!)\n if (options.includeMetadata) {\n return object\n }\n\n // Dropped unless asked for, because R2 omits them unless asked for. A fake that always\n // returned them would let a caller read `contentType` off a listing in tests and find it\n // undefined against a real bucket.\n return { ...object, contentType: undefined, metadata: undefined }\n }),\n truncated: next !== undefined,\n cursor: next,\n })\n }\n\n /**\n * Generate a fake presigned download URL\n */\n getPresignedDownloadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'GET', expiresIn))\n }\n\n /**\n * Generate a fake presigned upload URL\n */\n getPresignedUploadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'PUT', expiresIn))\n }\n\n /**\n * Generate a fake presigned delete URL\n */\n getPresignedDeleteUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'DELETE', expiresIn))\n }\n\n /**\n * Chunked upload (same as regular upload for fake)\n */\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number },\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const size = options.size ?? content.length\n\n // Reuse the already-materialized bytes rather than `body`: a `ReadableStream`\n // is single-use, so reading it here and again inside `upload` would throw\n // \"ReadableStream is disturbed\". `content` is a Uint8Array and safe to re-read.\n return this.upload(content, path, { ...options, size }, disk)\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Test Assertion Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Assert that a file exists at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file does not exist\n */\n assertExists(path: string): void {\n expect(\n this.files.has(path),\n `Expected file to exist at: ${path}\\nStored files: ${this.getStoredPaths().join(', ') || '(none)'}`\n ).toBe(true)\n }\n\n /**\n * Assert that a file does NOT exist at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file exists\n */\n assertMissing(path: string): void {\n expect(\n this.files.has(path),\n `Expected file NOT to exist at: ${path}`\n ).toBe(false)\n }\n\n /**\n * Assert storage is empty\n *\n * @throws AssertionError if any files exist\n */\n assertEmpty(): void {\n expect(\n this.files.size,\n `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(', ')}`\n ).toBe(0)\n }\n\n /**\n * Assert storage has exactly N files\n *\n * @param count - Expected number of files\n * @throws AssertionError if count doesn't match\n */\n assertCount(count: number): void {\n expect(\n this.files.size,\n `Expected ${count} files in storage but found ${this.files.size}`\n ).toBe(count)\n }\n\n /**\n * Get all stored files (for inspection)\n */\n getStoredFiles(): Map<string, StoredFile> {\n return new Map(this.files)\n }\n\n /**\n * Get all stored file paths\n */\n getStoredPaths(): string[] {\n return Array.from(this.files.keys())\n }\n\n /**\n * Get a specific file by path\n */\n getFile(path: string): StoredFile | undefined {\n return this.files.get(path)\n }\n\n /**\n * Clear all stored files (call in beforeEach for test isolation)\n */\n clear(): void {\n this.files.clear()\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Private Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n private createPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn = 300\n ): PresignedUrlResult {\n const expiresAt = new Date(Date.now() + expiresIn * 1000)\n\n return {\n url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,\n expiresIn,\n expiresAt,\n method,\n }\n }\n\n private async bodyToUint8Array(body: StreamingBlobPayloadInputTypes | null | undefined): Promise<Uint8Array> {\n if (!body) {\n return new Uint8Array(0)\n }\n\n if (body instanceof Uint8Array) {\n return body\n }\n\n if (body instanceof ArrayBuffer) {\n return new Uint8Array(body)\n }\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body)\n }\n\n if (body instanceof Blob) {\n const buffer = await body.arrayBuffer()\n return new Uint8Array(buffer)\n }\n\n if (body instanceof ReadableStream) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n // FormData or URLSearchParams - convert via Response\n if (body instanceof FormData || body instanceof URLSearchParams) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n return new Uint8Array(0)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,MAAM,sBAAsB;;AAG5B,SAAS,aAAa,MAAc,MAA8B;CAChE,OAAO;EACL;EACA,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,UAAU,KAAK;CACjB;AACF;AA+BO,IAAM,qBAAN,MAAM,2BAA2B,eAAe;CAKhC;CAEA;CANrB,wBAAgB,IAAI,IAAwB;CAE5C,YACE,gBAEA,SAEA;EACA,MAAM,gBAAgB,OAAO;EAJV,KAAA,iBAAA;EAEA,KAAA,UAAA;CAGrB;;;;CAKA,MAAM,OACJ,MACA,cACA,SACA,MACuB;EACvB,MAAM,UAAU,MAAM,KAAK,iBAAiB,IAAI;EAChD,MAAM,WAAW,KAAK,YAAY,IAAI;EAEtC,KAAK,MAAM,IAAI,cAAc;GAC3B;GACA,UAAU,QAAQ,YAAY;GAC9B,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,4BAAY,IAAI,KAAK;EACvB,CAAC;EAED,OAAO;GACL,MAAM;GACN,MAAM;GACN,UAAU;GACV,MAAM,QAAQ;GACd,UAAU,QAAQ,YAAY;GAC9B,4BAAY,IAAI,KAAK;EACvB;CACF;;;;CAKA,SAAS,MAAuC;EAC9C,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI;EAEhC,IAAI,CAAC,MACH,OAAO,QAAQ,OAAO,IAAI,kBAAkB,IAAI,CAAC;EAGnD,OAAO,QAAQ,QAAQ;GACrB,gBAAgB,IAAI,eAAe,EACjC,MAAM,YAAY;IAChB,WAAW,QAAQ,KAAK,OAAO;IAC/B,WAAW,MAAM;GACnB,EACF,CAAC;GACD,gBAAgB,QAAQ,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC;GACtE,qBAAqB,QAAQ,QAAQ,KAAK,OAAO;GACjD,aAAa,KAAK;GAClB,MAAM,KAAK;GACX,UAAU,KAAK;EACjB,CAAC;CACH;;;;CAKA,OAAO,MAA6B;EAClC,KAAK,MAAM,OAAO,IAAI;EACtB,OAAO,QAAQ,QAAQ;CACzB;;;;CAKA,OAAO,MAAgC;EACrC,OAAO,QAAQ,QAAQ,KAAK,MAAM,IAAI,IAAI,CAAC;CAC7C;;;;CAQA,MAAM,WAAW,OAAiB,MAA8B;EAC9D,KAAK,YAAY,IAAI;EAErB,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,OAAO,IAAI;EAGxB,OAAO,QAAQ,QAAQ;CACzB;;;;CAKA,MAAM,KAAK,MAAc,MAA2C;EAClE,KAAK,YAAY,IAAI;EAErB,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI;EAChC,IAAI,CAAC,MACH,OAAO,QAAQ,QAAQ,IAAI;EAG7B,OAAO,QAAQ,QAAQ,aAAa,MAAM,IAAI,CAAC;CACjD;;;;;;;;;;;CAYA,MAAM,KAAK,UAAuB,CAAC,GAAG,MAAoC;EACxE,KAAK,YAAY,IAAI;EAErB,MAAM,SAAS,QAAQ,UAAU;EAGjC,MAAM,WAAW,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC,CAAC,CAC3C,QAAQ,SAAS,KAAK,WAAW,MAAM,CAAC,CAAC,CACzC,KAAK;EAER,MAAM,QAAQ,QAAQ,WAAW,KAAA,IAAY,IAAI,SAAS,QAAQ,QAAQ,MAAM;EAChF,IAAI,QAAQ,GACV,OAAO,QAAQ,uBAAO,IAAI,MAAM,gCAAgC,QAAQ,UAAU,IAAI,CAAC;EAGzF,MAAM,QAAQ,QAAQ,SAAS;EAC/B,MAAM,OAAO,SAAS,MAAM,OAAO,QAAQ,KAAK;EAChD,MAAM,OAAO,SAAS,QAAQ;EAE9B,OAAO,QAAQ,QAAQ;GACrB,SAAS,KAAK,KAAK,SAAS;IAC1B,MAAM,SAAS,aAAa,MAAM,KAAK,MAAM,IAAI,IAAI,CAAE;IACvD,IAAI,QAAQ,iBACV,OAAO;IAMT,OAAO;KAAE,GAAG;KAAQ,aAAa,KAAA;KAAW,UAAU,KAAA;IAAU;GAClE,CAAC;GACD,WAAW,SAAS,KAAA;GACpB,QAAQ;EACV,CAAC;CACH;;;;CAKA,wBACE,MACA,WAC6B;EAC7B,OAAO,QAAQ,QAAQ,KAAK,mBAAmB,MAAM,OAAO,SAAS,CAAC;CACxE;;;;CAKA,sBACE,MACA,WAC6B;EAC7B,OAAO,QAAQ,QAAQ,KAAK,mBAAmB,MAAM,OAAO,SAAS,CAAC;CACxE;;;;CAKA,sBACE,MACA,WAC6B;EAC7B,OAAO,QAAQ,QAAQ,KAAK,mBAAmB,MAAM,UAAU,SAAS,CAAC;CAC3E;;;;CAKA,MAAM,cACJ,MACA,MACA,SACA,MACuB;EACvB,MAAM,UAAU,MAAM,KAAK,iBAAiB,IAAI;EAChD,MAAM,OAAO,QAAQ,QAAQ,QAAQ;EAKrC,OAAO,KAAK,OAAO,SAAS,MAAM;GAAE,GAAG;GAAS;EAAK,GAAG,IAAI;CAC9D;;;;;;;CAYA,aAAa,MAAoB;EAC/B,OACE,KAAK,MAAM,IAAI,IAAI,GACnB,8BAA8B,KAAK,kBAAkB,KAAK,eAAe,CAAC,CAAC,KAAK,IAAI,KAAK,UAC3F,CAAC,CAAC,KAAK,IAAI;CACb;;;;;;;CAQA,cAAc,MAAoB;EAChC,OACE,KAAK,MAAM,IAAI,IAAI,GACnB,kCAAkC,MACpC,CAAC,CAAC,KAAK,KAAK;CACd;;;;;;CAOA,cAAoB;EAClB,OACE,KAAK,MAAM,MACX,0CAA0C,KAAK,MAAM,KAAK,UAAU,KAAK,eAAe,CAAC,CAAC,KAAK,IAAI,GACrG,CAAC,CAAC,KAAK,CAAC;CACV;;;;;;;CAQA,YAAY,OAAqB;EAC/B,OACE,KAAK,MAAM,MACX,YAAY,MAAM,8BAA8B,KAAK,MAAM,MAC7D,CAAC,CAAC,KAAK,KAAK;CACd;;;;CAKA,iBAA0C;EACxC,OAAO,IAAI,IAAI,KAAK,KAAK;CAC3B;;;;CAKA,iBAA2B;EACzB,OAAO,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC;CACrC;;;;CAKA,QAAQ,MAAsC;EAC5C,OAAO,KAAK,MAAM,IAAI,IAAI;CAC5B;;;;CAKA,QAAc;EACZ,KAAK,MAAM,MAAM;CACnB;CAMA,mBACE,MACA,QACA,YAAY,KACQ;EACpB,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,YAAY,GAAI;EAExD,OAAO;GACL,KAAK,6BAA6B,KAAK,UAAU,OAAO,WAAW,UAAU,YAAY;GACzF;GACA;GACA;EACF;CACF;CAEA,MAAc,iBAAiB,MAA8E;EAC3G,IAAI,CAAC,MACH,uBAAO,IAAI,WAAW,CAAC;EAGzB,IAAI,gBAAgB,YAClB,OAAO;EAGT,IAAI,gBAAgB,aAClB,OAAO,IAAI,WAAW,IAAI;EAG5B,IAAI,OAAO,SAAS,UAClB,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;EAGtC,IAAI,gBAAgB,MAAM;GACxB,MAAM,SAAS,MAAM,KAAK,YAAY;GACtC,OAAO,IAAI,WAAW,MAAM;EAC9B;EAEA,IAAI,gBAAgB,gBAClB,OAAO,IAAI,WAAW,MAAM,IAAI,SAAS,IAAI,CAAC,CAAC,YAAY,CAAC;EAI9D,IAAI,gBAAgB,YAAY,gBAAgB,iBAC9C,OAAO,IAAI,WAAW,MAAM,IAAI,SAAS,IAAI,CAAC,CAAC,YAAY,CAAC;EAG9D,uBAAO,IAAI,WAAW,CAAC;CACzB;AACF;;CAtVC,UAAU,eAAe,cAAc;CAKnC,gBAAA,GAAA,OAAO,eAAe,cAAc,CAAA;CAEpC,gBAAA,GAAA,OAAO,eAAe,OAAO,CAAA"}
@@ -0,0 +1,144 @@
1
+ import { PurgeSpec, WorkersCache } from "stratal/response-cache";
2
+ import { EmailBatchSendResult, EmailSendResult, IEmailProvider, ResolvedEmailMessage } from "stratal/email";
3
+ //#region src/mocks/test-email-provider.d.ts
4
+ /**
5
+ * In-memory email provider for tests.
6
+ *
7
+ * The sync queue provider runs `EmailConsumer` inline on dispatch, which would
8
+ * otherwise open a real SMTP connection from the test worker. The testing
9
+ * module builder installs this provider by default (overridable via
10
+ * `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every
11
+ * message so tests can assert on what was sent.
12
+ */
13
+ declare class TestEmailProvider implements IEmailProvider {
14
+ /** Every message handed to the provider, in send order. */
15
+ readonly sent: ResolvedEmailMessage[];
16
+ send(message: ResolvedEmailMessage): Promise<EmailSendResult>;
17
+ sendBatch(messages: ResolvedEmailMessage[]): Promise<EmailBatchSendResult>;
18
+ }
19
+ //#endregion
20
+ //#region src/mocks/test-workers-cache.d.ts
21
+ /**
22
+ * In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.
23
+ *
24
+ * Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,
25
+ * which would otherwise leave every `@Cacheable` route in a test serving
26
+ * uncached and stamped `private, no-store` — so a spec asserting the headers
27
+ * the route declares would read the ones an unconfigured runtime produces. The
28
+ * testing module builder installs this by default so those routes are testable
29
+ * with zero configuration: `@Cacheable` responses carry real
30
+ * `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges are recorded
31
+ * rather than skipped.
32
+ *
33
+ * Every spec passed to `purge()` is recorded, in call order, for assertion
34
+ * via `module.cache.purges`. Purges always succeed — this stub only proves an
35
+ * app's cache configuration is correct, not Workers Caching's own failure
36
+ * modes (that's covered inside `stratal` itself, against a mocked
37
+ * `WorkersCache`). Opt out with `Test.createTestingModule({ cache: false })`
38
+ * to reproduce a runtime where Workers Caching is genuinely unconfigured —
39
+ * e.g. to test the `ResponseCacheConfigError` boot guard.
40
+ */
41
+ declare class TestWorkersCache implements WorkersCache {
42
+ /** Every spec passed to `purge()`, in call order. */
43
+ readonly purges: PurgeSpec[];
44
+ purge(spec: PurgeSpec): Promise<{
45
+ success: boolean;
46
+ }>;
47
+ }
48
+ //#endregion
49
+ //#region src/mocks/test-worker-exports.d.ts
50
+ /** One `ctx.exports.<Name>.fetch()` the gateway made, recorded for assertion. */
51
+ interface LoopbackCall {
52
+ /** The export name the gateway dispatched to — `gateway: { entrypoint }`. */
53
+ entrypoint: string;
54
+ method: string;
55
+ url: string;
56
+ /** The resolved partitions the gateway placed in `ctx.props`. */
57
+ props: Record<string, unknown>;
58
+ }
59
+ /** Runs a loopback in cached mode. Installed by the testing module builder. */
60
+ type LoopbackHandler = (request: Request, props: Record<string, unknown>) => Promise<Response>;
61
+ /**
62
+ * In-memory stand-in for `ctx.exports` — the loopback bindings a Worker has to
63
+ * its own top-level exports.
64
+ *
65
+ * Neither Miniflare nor workerd populates `ExecutionContext.exports` for a
66
+ * `Test.createTestingModule()` app, so without this every consumer that adopts
67
+ * `ResponseCacheModule.forRoot({ gateway: { entrypoint } })` would have their
68
+ * whole suite fail on the first request: the gateway's boot verification
69
+ * throws `ResponseCacheConfigError` the moment it cannot reach the configured
70
+ * entrypoint. That is the same usability defect {@link TestWorkersCache} fixes
71
+ * for `ctx.cache`, and it is fixed the same way — installed by default, opt out
72
+ * with `Test.createTestingModule({ cache: false })`.
73
+ *
74
+ * The stub is not inert. A dispatched request is re-run through the *same*
75
+ * Hono app with an unmarked execution context carrying the gateway's chosen
76
+ * `props` — exactly what the real cached entrypoint does — so a partitioned
77
+ * route is genuinely exercised end to end rather than short-circuited. Every
78
+ * call is recorded, in order, for assertion via `module.gateway.loopbacks`,
79
+ * mirroring `module.cache.purges`:
80
+ *
81
+ * ```typescript
82
+ * await module.http.get('/dashboard').withHeaders({ ... }).send()
83
+ * expect(module.gateway.loopbacks).toEqual([
84
+ * { entrypoint: 'Cached', method: 'GET', url: '...', props: { user: 'u-1' } },
85
+ * ])
86
+ * ```
87
+ *
88
+ * An empty `loopbacks` after a request to a partitioned route is itself the
89
+ * assertion for the fail-closed cases — an unresolved partition, a non-GET, a
90
+ * route with no `partitionBy` — all of which must run inline.
91
+ *
92
+ * Purges forwarded over RPC (`ctx.exports.<Name>.purge(spec)`) land on the same
93
+ * {@link TestWorkersCache} as a direct `ctx.cache.purge()`, so
94
+ * `module.cache.purges` reads identically whether or not a gateway is
95
+ * configured.
96
+ */
97
+ declare class TestWorkerExports {
98
+ private readonly cache;
99
+ /** Every loopback dispatch, in call order. */
100
+ readonly loopbacks: LoopbackCall[];
101
+ private handler;
102
+ private readonly stubs;
103
+ constructor(cache: TestWorkersCache | undefined);
104
+ /**
105
+ * Install the function that actually serves a loopback.
106
+ *
107
+ * Set after the `Application` exists, because the handler needs its Hono
108
+ * app — while the execution context carrying this stub has to be built
109
+ * before it, to be passed to the constructor.
110
+ *
111
+ * @internal
112
+ */
113
+ setHandler(handler: LoopbackHandler): void;
114
+ /**
115
+ * The object to install as `ctx.exports`.
116
+ *
117
+ * A `Proxy` rather than a fixed record because the export name is the
118
+ * consumer's to choose — the framework only ever sees the string in
119
+ * `gateway: { entrypoint }`. Answering for every name keeps a typo'd
120
+ * entrypoint from being caught here, in a stub, instead of in the
121
+ * consumer's real Wrangler config where it matters; that trade is
122
+ * deliberate, since a suite failing on a name the test never mentions is
123
+ * far more confusing than a deploy-time error.
124
+ *
125
+ * A typo is meant to be caught earlier still: `gateway.entrypoint` is typed
126
+ * as `CachedEntrypointName`, so once the consumer has run `wrangler types` a
127
+ * wrong name is a compile error at the config site — before this stub, and
128
+ * before deploy. This Proxy is the runtime fallback for projects without
129
+ * generated types, not the primary guard.
130
+ */
131
+ get context(): Record<string, unknown>;
132
+ /**
133
+ * A loopback binding, shaped like the real one:
134
+ * `Fetcher<T> & ((opts: { props }) => Fetcher<T>)`. Both the callable form
135
+ * (which is how `ctx.props` is chosen) and the direct form are supported,
136
+ * so whichever the framework uses is genuinely exercised.
137
+ */
138
+ private stubFor;
139
+ private dispatch;
140
+ private purge;
141
+ }
142
+ //#endregion
143
+ export { TestEmailProvider as i, TestWorkerExports as n, TestWorkersCache as r, LoopbackCall as t };
144
+ //# sourceMappingURL=test-worker-exports-dCRARYEc.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-worker-exports-dCRARYEc.d.mts","names":[],"sources":["../src/mocks/test-email-provider.ts","../src/mocks/test-workers-cache.ts","../src/mocks/test-worker-exports.ts"],"mappings":";;;;;;;;;;;;cAgBa,6BAA6B;;WAEhC,MAAM;EAEf,KAAK,SAAS,uBAAuB,QAAQ;EAKvC,UAAU,UAAU,yBAAyB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;cCH/C,4BAA4B;;WAE9B,QAAQ;EAEjB,MAAM,MAAM,YAAY;IAAU;;;;;;UCtBnB;;EAEf;EACA;EACA;;EAEA,OAAO;;;KAIG,mBACV,SAAS,SACT,OAAO,4BACJ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA;mBAOkB;;WALpB,WAAW;UAEZ;mBACS;EAEjB,YAA6B,OAAO;;;;;;;;;;EAWpC,WAAW,SAAS;;;;;;;;;;;;;;;;;;MAqBhB,WAAW;;;;;;;UAmBP;UAkBM;UAiBA"}
@@ -0,0 +1,183 @@
1
+ //#region src/mocks/test-email-provider.ts
2
+ /**
3
+ * In-memory email provider for tests.
4
+ *
5
+ * The sync queue provider runs `EmailConsumer` inline on dispatch, which would
6
+ * otherwise open a real SMTP connection from the test worker. The testing
7
+ * module builder installs this provider by default (overridable via
8
+ * `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every
9
+ * message so tests can assert on what was sent.
10
+ */
11
+ var TestEmailProvider = class {
12
+ /** Every message handed to the provider, in send order. */
13
+ sent = [];
14
+ send(message) {
15
+ this.sent.push(message);
16
+ return Promise.resolve({
17
+ messageId: `test-${this.sent.length}`,
18
+ accepted: true
19
+ });
20
+ }
21
+ async sendBatch(messages) {
22
+ const results = await Promise.all(messages.map((message) => this.send(message)));
23
+ return {
24
+ total: results.length,
25
+ successful: results.length,
26
+ failed: 0,
27
+ results
28
+ };
29
+ }
30
+ };
31
+ //#endregion
32
+ //#region src/mocks/test-worker-exports.ts
33
+ /**
34
+ * In-memory stand-in for `ctx.exports` — the loopback bindings a Worker has to
35
+ * its own top-level exports.
36
+ *
37
+ * Neither Miniflare nor workerd populates `ExecutionContext.exports` for a
38
+ * `Test.createTestingModule()` app, so without this every consumer that adopts
39
+ * `ResponseCacheModule.forRoot({ gateway: { entrypoint } })` would have their
40
+ * whole suite fail on the first request: the gateway's boot verification
41
+ * throws `ResponseCacheConfigError` the moment it cannot reach the configured
42
+ * entrypoint. That is the same usability defect {@link TestWorkersCache} fixes
43
+ * for `ctx.cache`, and it is fixed the same way — installed by default, opt out
44
+ * with `Test.createTestingModule({ cache: false })`.
45
+ *
46
+ * The stub is not inert. A dispatched request is re-run through the *same*
47
+ * Hono app with an unmarked execution context carrying the gateway's chosen
48
+ * `props` — exactly what the real cached entrypoint does — so a partitioned
49
+ * route is genuinely exercised end to end rather than short-circuited. Every
50
+ * call is recorded, in order, for assertion via `module.gateway.loopbacks`,
51
+ * mirroring `module.cache.purges`:
52
+ *
53
+ * ```typescript
54
+ * await module.http.get('/dashboard').withHeaders({ ... }).send()
55
+ * expect(module.gateway.loopbacks).toEqual([
56
+ * { entrypoint: 'Cached', method: 'GET', url: '...', props: { user: 'u-1' } },
57
+ * ])
58
+ * ```
59
+ *
60
+ * An empty `loopbacks` after a request to a partitioned route is itself the
61
+ * assertion for the fail-closed cases — an unresolved partition, a non-GET, a
62
+ * route with no `partitionBy` — all of which must run inline.
63
+ *
64
+ * Purges forwarded over RPC (`ctx.exports.<Name>.purge(spec)`) land on the same
65
+ * {@link TestWorkersCache} as a direct `ctx.cache.purge()`, so
66
+ * `module.cache.purges` reads identically whether or not a gateway is
67
+ * configured.
68
+ */
69
+ var TestWorkerExports = class {
70
+ cache;
71
+ /** Every loopback dispatch, in call order. */
72
+ loopbacks = [];
73
+ handler;
74
+ stubs = /* @__PURE__ */ new Map();
75
+ constructor(cache) {
76
+ this.cache = cache;
77
+ }
78
+ /**
79
+ * Install the function that actually serves a loopback.
80
+ *
81
+ * Set after the `Application` exists, because the handler needs its Hono
82
+ * app — while the execution context carrying this stub has to be built
83
+ * before it, to be passed to the constructor.
84
+ *
85
+ * @internal
86
+ */
87
+ setHandler(handler) {
88
+ this.handler = handler;
89
+ }
90
+ /**
91
+ * The object to install as `ctx.exports`.
92
+ *
93
+ * A `Proxy` rather than a fixed record because the export name is the
94
+ * consumer's to choose — the framework only ever sees the string in
95
+ * `gateway: { entrypoint }`. Answering for every name keeps a typo'd
96
+ * entrypoint from being caught here, in a stub, instead of in the
97
+ * consumer's real Wrangler config where it matters; that trade is
98
+ * deliberate, since a suite failing on a name the test never mentions is
99
+ * far more confusing than a deploy-time error.
100
+ *
101
+ * A typo is meant to be caught earlier still: `gateway.entrypoint` is typed
102
+ * as `CachedEntrypointName`, so once the consumer has run `wrangler types` a
103
+ * wrong name is a compile error at the config site — before this stub, and
104
+ * before deploy. This Proxy is the runtime fallback for projects without
105
+ * generated types, not the primary guard.
106
+ */
107
+ get context() {
108
+ return new Proxy({}, {
109
+ get: (_target, property) => typeof property === "string" ? this.stubFor(property) : void 0,
110
+ has: () => true,
111
+ ownKeys: () => [...this.stubs.keys()],
112
+ getOwnPropertyDescriptor: () => ({
113
+ enumerable: true,
114
+ configurable: true
115
+ })
116
+ });
117
+ }
118
+ /**
119
+ * A loopback binding, shaped like the real one:
120
+ * `Fetcher<T> & ((opts: { props }) => Fetcher<T>)`. Both the callable form
121
+ * (which is how `ctx.props` is chosen) and the direct form are supported,
122
+ * so whichever the framework uses is genuinely exercised.
123
+ */
124
+ stubFor(entrypoint) {
125
+ const existing = this.stubs.get(entrypoint);
126
+ if (existing) return existing;
127
+ const bound = (props) => ({
128
+ fetch: (request) => this.dispatch(entrypoint, request, props),
129
+ purge: (spec) => this.purge(spec)
130
+ });
131
+ const stub = Object.assign((options) => bound(options?.props ?? {}), bound({}));
132
+ this.stubs.set(entrypoint, stub);
133
+ return stub;
134
+ }
135
+ async dispatch(entrypoint, request, props) {
136
+ this.loopbacks.push({
137
+ entrypoint,
138
+ method: request.method,
139
+ url: request.url,
140
+ props
141
+ });
142
+ if (!this.handler) throw new Error("[stratal:testing] A gateway loopback was dispatched before the testing module finished compiling. This is a bug in @stratal/testing — please report it.");
143
+ return this.handler(request, props);
144
+ }
145
+ async purge(spec) {
146
+ await this.cache?.purge(spec);
147
+ return { success: true };
148
+ }
149
+ };
150
+ //#endregion
151
+ //#region src/mocks/test-workers-cache.ts
152
+ /**
153
+ * In-memory stand-in for Cloudflare Workers Caching's `ctx.cache`.
154
+ *
155
+ * Neither Miniflare nor workerd populates `ExecutionContext.cache` locally,
156
+ * which would otherwise leave every `@Cacheable` route in a test serving
157
+ * uncached and stamped `private, no-store` — so a spec asserting the headers
158
+ * the route declares would read the ones an unconfigured runtime produces. The
159
+ * testing module builder installs this by default so those routes are testable
160
+ * with zero configuration: `@Cacheable` responses carry real
161
+ * `Cache-Control`/`Cache-Tag` headers, and `@PurgesCache` purges are recorded
162
+ * rather than skipped.
163
+ *
164
+ * Every spec passed to `purge()` is recorded, in call order, for assertion
165
+ * via `module.cache.purges`. Purges always succeed — this stub only proves an
166
+ * app's cache configuration is correct, not Workers Caching's own failure
167
+ * modes (that's covered inside `stratal` itself, against a mocked
168
+ * `WorkersCache`). Opt out with `Test.createTestingModule({ cache: false })`
169
+ * to reproduce a runtime where Workers Caching is genuinely unconfigured —
170
+ * e.g. to test the `ResponseCacheConfigError` boot guard.
171
+ */
172
+ var TestWorkersCache = class {
173
+ /** Every spec passed to `purge()`, in call order. */
174
+ purges = [];
175
+ purge(spec) {
176
+ this.purges.push(spec);
177
+ return Promise.resolve({ success: true });
178
+ }
179
+ };
180
+ //#endregion
181
+ export { TestWorkerExports as n, TestEmailProvider as r, TestWorkersCache as t };
182
+
183
+ //# sourceMappingURL=test-workers-cache-D02Pt_dE.mjs.map