@ddd-ts-kit/testing 0.8.0 → 0.9.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 (70) hide show
  1. package/README.md +1 -0
  2. package/dist/contracts/file-storage/arrangement-suite.d.ts +9 -0
  3. package/dist/contracts/file-storage/arrangement-suite.d.ts.map +1 -0
  4. package/dist/contracts/file-storage/arrangement-suite.js +51 -0
  5. package/dist/contracts/file-storage/arrangement-suite.js.map +1 -0
  6. package/dist/contracts/file-storage/checksum-of.d.ts +3 -0
  7. package/dist/contracts/file-storage/checksum-of.d.ts.map +1 -0
  8. package/dist/contracts/file-storage/checksum-of.js +10 -0
  9. package/dist/contracts/file-storage/checksum-of.js.map +1 -0
  10. package/dist/contracts/file-storage/chunks-of.d.ts +3 -0
  11. package/dist/contracts/file-storage/chunks-of.d.ts.map +1 -0
  12. package/dist/contracts/file-storage/chunks-of.js +5 -0
  13. package/dist/contracts/file-storage/chunks-of.js.map +1 -0
  14. package/dist/contracts/file-storage/content-suite.d.ts +12 -0
  15. package/dist/contracts/file-storage/content-suite.d.ts.map +1 -0
  16. package/dist/contracts/file-storage/content-suite.js +110 -0
  17. package/dist/contracts/file-storage/content-suite.js.map +1 -0
  18. package/dist/contracts/file-storage/contents.d.ts +7 -0
  19. package/dist/contracts/file-storage/contents.d.ts.map +1 -0
  20. package/dist/contracts/file-storage/contents.js +7 -0
  21. package/dist/contracts/file-storage/contents.js.map +1 -0
  22. package/dist/contracts/file-storage/file-storage-contract-options.d.ts +17 -0
  23. package/dist/contracts/file-storage/file-storage-contract-options.d.ts.map +1 -0
  24. package/dist/contracts/file-storage/file-storage-contract-options.js +2 -0
  25. package/dist/contracts/file-storage/file-storage-contract-options.js.map +1 -0
  26. package/dist/contracts/file-storage/file-storage-suite.d.ts +14 -0
  27. package/dist/contracts/file-storage/file-storage-suite.d.ts.map +1 -0
  28. package/dist/contracts/file-storage/file-storage-suite.js +30 -0
  29. package/dist/contracts/file-storage/file-storage-suite.js.map +1 -0
  30. package/dist/contracts/file-storage/follow-served.d.ts +9 -0
  31. package/dist/contracts/file-storage/follow-served.d.ts.map +1 -0
  32. package/dist/contracts/file-storage/follow-served.js +28 -0
  33. package/dist/contracts/file-storage/follow-served.js.map +1 -0
  34. package/dist/contracts/file-storage/follow-with-fetch.d.ts +5 -0
  35. package/dist/contracts/file-storage/follow-with-fetch.d.ts.map +1 -0
  36. package/dist/contracts/file-storage/follow-with-fetch.js +14 -0
  37. package/dist/contracts/file-storage/follow-with-fetch.js.map +1 -0
  38. package/dist/contracts/file-storage/followed-response.d.ts +7 -0
  39. package/dist/contracts/file-storage/followed-response.d.ts.map +1 -0
  40. package/dist/contracts/file-storage/followed-response.js +2 -0
  41. package/dist/contracts/file-storage/followed-response.js.map +1 -0
  42. package/dist/contracts/file-storage/large-content.d.ts +3 -0
  43. package/dist/contracts/file-storage/large-content.d.ts.map +1 -0
  44. package/dist/contracts/file-storage/large-content.js +8 -0
  45. package/dist/contracts/file-storage/large-content.js.map +1 -0
  46. package/dist/contracts/file-storage/read-all.d.ts +3 -0
  47. package/dist/contracts/file-storage/read-all.d.ts.map +1 -0
  48. package/dist/contracts/file-storage/read-all.js +9 -0
  49. package/dist/contracts/file-storage/read-all.js.map +1 -0
  50. package/dist/contracts/file-storage/signed-links-suite.d.ts +11 -0
  51. package/dist/contracts/file-storage/signed-links-suite.d.ts.map +1 -0
  52. package/dist/contracts/file-storage/signed-links-suite.js +93 -0
  53. package/dist/contracts/file-storage/signed-links-suite.js.map +1 -0
  54. package/dist/contracts/file-storage/signed-request.d.ts +8 -0
  55. package/dist/contracts/file-storage/signed-request.d.ts.map +1 -0
  56. package/dist/contracts/file-storage/signed-request.js +2 -0
  57. package/dist/contracts/file-storage/signed-request.js.map +1 -0
  58. package/dist/contracts/file-storage/tampered.d.ts +3 -0
  59. package/dist/contracts/file-storage/tampered.d.ts.map +1 -0
  60. package/dist/contracts/file-storage/tampered.js +5 -0
  61. package/dist/contracts/file-storage/tampered.js.map +1 -0
  62. package/dist/contracts/file-storage-contract.d.ts +11 -0
  63. package/dist/contracts/file-storage-contract.d.ts.map +1 -0
  64. package/dist/contracts/file-storage-contract.js +16 -0
  65. package/dist/contracts/file-storage-contract.js.map +1 -0
  66. package/dist/contracts.d.ts +6 -0
  67. package/dist/contracts.d.ts.map +1 -1
  68. package/dist/contracts.js +3 -0
  69. package/dist/contracts.js.map +1 -1
  70. package/package.json +4 -4
package/README.md CHANGED
@@ -442,6 +442,7 @@ repositoryContract(runner, { ...options, name: 'DrizzleOrderRepository', createR
442
442
  | `loggerContract` | any `Logger`: every level with its message and fields of every shape, level and message that no field forges, children that add their fields and nest, fields read at each entry, an entry below the level dropped without reading a field, `isLevelEnabled` answering what is written (for a child too), no throw on a circular field or a getter that throws, at the top of the fields included; an `Error` passed as a field written as its description (`type`, `code` of a `DomainError`, `message`, own `stack`, `cause`, eight errors deep at most), its message replaced when the logger scrubs unless it is a `DomainError`; every entry in the sink once `flush()` resolves, then no throw from a log call or a second flush. `createLogger({ level, fields, scrubMessages })` answers the logger and `lines()`, what its sink received as `{ level, message, fields }`. No capability flag: `InMemoryLogger` takes the same three options and passes the whole suite |
443
443
  | `criteriaContract` | any translator of criteria, the in-memory evaluator included: a table of criteria and the ids each answers, in order, over eight fixed rows. Every operator on every type it applies to, decimals as numbers, instants to the millisecond and across zones, case and the characters a `LIKE` would trip on, nulls under each operator, `and`, `or`, `not` and what a negation does to a null, sorts with ties and nulls in both directions, offset pages with their total, every sort walked by cursor one and two rows at a time, the four kinds of access scope, and hostile values. `createReadModel(rows)` answers `{ search(scopedCriteria) }` over exactly those rows, read with `criteriaContractDefinition` |
444
444
  | `readModelStoreContract` | any `ReadModelTable` under `TableReadModelStore`: rows of every type round trip, the current row handed to a later event, an event applied once, never one older than the row, order by event id at the same instant and by version when given, concurrent writes ending on the latest, deletions that last, listing by criteria and scope, and `ProjectionRebuild`: rebuilt from the source alone, no event lost while it exports (on a row already copied and on one not yet), stale and new events after the swap, deletions during and before it, a failed export leaving the table as it was, the table still itself after two rebuilds. `createTable()` answers `{ table, unitOfWork }`; `SourceOfTruth` is the fake of the source |
445
+ | `fileStorageContract` | any `FileStorage`: content round trip with size, media type and checksum, several mebibytes streamed, a write beyond `maxBytes` refused leaving the previous content, keys that could leave their place refused, deletion, copy, move, listing by prefix, signed download and upload links that expire, refuse a changed signature or another media type, and are never signed for more than an hour, a download served as an attachment even without a name. `follow` sends a request to a link: `fetch` by default, `followServed(storage)` for a storage the application serves |
445
446
  | `rateLimiterContract` | any `RateLimiter`: limit, `remaining` and `retryAfterMs`, refused calls not counted, sliding window, isolation of keys, concurrent calls |
446
447
 
447
448
  The cache, lock and rate limiter suites also take `elapse(ms)`, which makes time pass for the
@@ -0,0 +1,9 @@
1
+ import { FileStorageSuite } from './file-storage-suite.js';
2
+ /** Files are copied, moved and listed by key, without reading them through the application. */
3
+ export declare class ArrangementSuite extends FileStorageSuite {
4
+ protected tests(): void;
5
+ private copies;
6
+ private moves;
7
+ private lists;
8
+ }
9
+ //# sourceMappingURL=arrangement-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrangement-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/arrangement-suite.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,+FAA+F;AAC/F,qBAAa,gBAAiB,SAAQ,gBAAgB;IACpD,SAAS,CAAC,KAAK,IAAI,IAAI,CAItB;IAED,OAAO,CAAC,MAAM;IAkBd,OAAO,CAAC,KAAK;IAkBb,OAAO,CAAC,KAAK;CAoBd"}
@@ -0,0 +1,51 @@
1
+ import { PDF, TEXT } from './contents.js';
2
+ import { FileStorageSuite } from './file-storage-suite.js';
3
+ /** Files are copied, moved and listed by key, without reading them through the application. */
4
+ export class ArrangementSuite extends FileStorageSuite {
5
+ tests() {
6
+ this.copies();
7
+ this.moves();
8
+ this.lists();
9
+ }
10
+ copies() {
11
+ this.it('copies a file to another key, the original staying', async () => {
12
+ await this.write('staging/documents/a.pdf', PDF);
13
+ await this.subject.copy('staging/documents/a.pdf', 'documents/a.pdf');
14
+ this.expect(await this.read('documents/a.pdf')).toEqual(PDF);
15
+ this.expect(await this.read('staging/documents/a.pdf')).toEqual(PDF);
16
+ this.expect((await this.subject.stat('documents/a.pdf'))?.mediaType).toBe('application/pdf');
17
+ });
18
+ this.it('refuses to copy a file that is not there', async () => {
19
+ this.expect(await this.codeOf(() => this.subject.copy('documents/missing.pdf', 'documents/b.pdf'))).toBe('FILE_NOT_FOUND');
20
+ });
21
+ }
22
+ moves() {
23
+ this.it('moves a file to another key, nothing staying behind', async () => {
24
+ await this.write('staging/documents/a.txt', TEXT, 'text/plain');
25
+ await this.subject.move('staging/documents/a.txt', 'documents/a.txt');
26
+ this.expect(await this.read('documents/a.txt')).toEqual(TEXT);
27
+ this.expect((await this.subject.stat('documents/a.txt'))?.mediaType).toBe('text/plain');
28
+ this.expect(await this.subject.stat('staging/documents/a.txt')).toBe(undefined);
29
+ });
30
+ this.it('refuses to move a file that is not there', async () => {
31
+ this.expect(await this.codeOf(() => this.subject.move('documents/missing.pdf', 'documents/b.pdf'))).toBe('FILE_NOT_FOUND');
32
+ });
33
+ }
34
+ lists() {
35
+ this.it('lists the files under a prefix, nested ones included, and no other', async () => {
36
+ await this.write('staging/a.pdf', PDF);
37
+ await this.write('staging/deep/er/b.pdf', TEXT);
38
+ await this.write('documents/c.pdf', PDF);
39
+ const listed = [];
40
+ for await (const file of this.subject.list('staging/')) {
41
+ listed.push([file.key, file.size, file.lastModified instanceof Date]);
42
+ }
43
+ listed.sort((left, right) => String(left[0]).localeCompare(String(right[0])));
44
+ this.expect(listed).toEqual([
45
+ ['staging/a.pdf', PDF.length, true],
46
+ ['staging/deep/er/b.pdf', TEXT.length, true],
47
+ ]);
48
+ });
49
+ }
50
+ }
51
+ //# sourceMappingURL=arrangement-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrangement-suite.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/arrangement-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,+FAA+F;AAC/F,MAAM,OAAO,gBAAiB,SAAQ,gBAAgB;IAC1C,KAAK;QACb,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;YACvE,MAAM,IAAI,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;YAEjD,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,yBAAyB,EAAE,iBAAiB,CAAC,CAAC;YAEtE,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC7D,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACrE,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC/F,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;YAC7D,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,CACtG,gBAAgB,CACjB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK;QACX,IAAI,CAAC,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;YACxE,MAAM,IAAI,CAAC,KAAK,CAAC,yBAAyB,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;YAEhE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,yBAAyB,EAAE,iBAAiB,CAAC,CAAC;YAEtE,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC9D,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACxF,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;YAC7D,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,CACtG,gBAAgB,CACjB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK;QACX,IAAI,CAAC,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;YACvF,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;YACvC,MAAM,IAAI,CAAC,KAAK,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAC;YAChD,MAAM,IAAI,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;YAEzC,MAAM,MAAM,GAAG,EAAE,CAAC;YAElB,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBACvD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,YAAY,IAAI,CAAC,CAAC,CAAC;YACxE,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAE9E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;gBAC1B,CAAC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC;gBACnC,CAAC,uBAAuB,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;aAC7C,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ /** The checksum a connector answers, computed by the suite from what it wrote. */
2
+ export declare function checksumOf(chunks: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<string>;
3
+ //# sourceMappingURL=checksum-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksum-of.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/checksum-of.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1G"}
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** The checksum a connector answers, computed by the suite from what it wrote. */
3
+ export async function checksumOf(chunks) {
4
+ const hash = createHash('sha256');
5
+ for await (const chunk of chunks) {
6
+ hash.update(chunk);
7
+ }
8
+ return `sha256:${hash.digest('hex')}`;
9
+ }
10
+ //# sourceMappingURL=checksum-of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksum-of.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/checksum-of.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAwD;IACvF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAElC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACxC,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** A content of a test as the stream a connector receives. */
2
+ export declare function chunksOf(bytes: Uint8Array): AsyncIterable<Uint8Array>;
3
+ //# sourceMappingURL=chunks-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks-of.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/chunks-of.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,wBAAuB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAAC,UAAU,CAAC,CAE5E"}
@@ -0,0 +1,5 @@
1
+ /** A content of a test as the stream a connector receives. */
2
+ export async function* chunksOf(bytes) {
3
+ yield bytes;
4
+ }
5
+ //# sourceMappingURL=chunks-of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks-of.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/chunks-of.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,QAAQ,CAAC,KAAiB;IAC/C,MAAM,KAAK,CAAC;AACd,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { FileStorageSuite } from './file-storage-suite.js';
2
+ /** Content goes in and out as streams, intact, measured and refused beyond its limit; keys stay inside the storage. */
3
+ export declare class ContentSuite extends FileStorageSuite {
4
+ protected tests(): void;
5
+ private roundTrips;
6
+ private streamsLarge;
7
+ private overwrites;
8
+ private limits;
9
+ private keys;
10
+ private removes;
11
+ }
12
+ //# sourceMappingURL=content-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/content-suite.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAI3D,uHAAuH;AACvH,qBAAa,YAAa,SAAQ,gBAAgB;IAChD,SAAS,CAAC,KAAK,IAAI,IAAI,CAOtB;IAED,OAAO,CAAC,UAAU;IA0BlB,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,MAAM;IA+Bd,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,OAAO;CAehB"}
@@ -0,0 +1,110 @@
1
+ import { checksumOf } from './checksum-of.js';
2
+ import { chunksOf } from './chunks-of.js';
3
+ import { LARGE_SIZE, PDF, TEXT } from './contents.js';
4
+ import { largeContent } from './large-content.js';
5
+ import { FileStorageSuite } from './file-storage-suite.js';
6
+ const INVALID_KEYS = ['', '../escape', '/absolute', 'a//b', '.hidden', 'a/../b', 'a/./b', 'with space', 'tail/'];
7
+ /** Content goes in and out as streams, intact, measured and refused beyond its limit; keys stay inside the storage. */
8
+ export class ContentSuite extends FileStorageSuite {
9
+ tests() {
10
+ this.roundTrips();
11
+ this.streamsLarge();
12
+ this.overwrites();
13
+ this.limits();
14
+ this.keys();
15
+ this.removes();
16
+ }
17
+ roundTrips() {
18
+ this.it('gives back what was written, with its size, media type and checksum', async () => {
19
+ const stored = await this.subject.put('documents/report.pdf', chunksOf(PDF), { mediaType: 'Application/PDF' });
20
+ const metadata = await this.subject.stat('documents/report.pdf');
21
+ const checksum = await checksumOf([PDF]);
22
+ this.expect(stored).toEqual({
23
+ key: 'documents/report.pdf',
24
+ size: PDF.length,
25
+ mediaType: 'application/pdf',
26
+ checksum,
27
+ });
28
+ this.expect(await this.read('documents/report.pdf')).toEqual(PDF);
29
+ this.expect([metadata?.key, metadata?.size, metadata?.mediaType]).toEqual([
30
+ 'documents/report.pdf',
31
+ PDF.length,
32
+ 'application/pdf',
33
+ ]);
34
+ this.expect(metadata?.checksum === undefined || metadata.checksum === checksum).toBe(true);
35
+ this.expect(metadata?.lastModified instanceof Date).toBe(true);
36
+ });
37
+ }
38
+ streamsLarge() {
39
+ this.it('streams a content of several mebibytes, chunk after chunk', async () => {
40
+ const stored = await this.subject.put('exports/large.bin', largeContent(), {
41
+ mediaType: 'application/octet-stream',
42
+ });
43
+ const content = await this.subject.get('exports/large.bin');
44
+ let read = 0;
45
+ for await (const chunk of content?.stream ?? []) {
46
+ read += Buffer.byteLength(chunk instanceof Uint8Array ? chunk : String(chunk));
47
+ }
48
+ this.expect([stored.size, stored.checksum, read]).toEqual([
49
+ LARGE_SIZE,
50
+ await checksumOf(largeContent()),
51
+ LARGE_SIZE,
52
+ ]);
53
+ });
54
+ }
55
+ overwrites() {
56
+ this.it('replaces the content written again under a key', async () => {
57
+ await this.write('notes/today.txt', PDF);
58
+ await this.write('notes/today.txt', TEXT, 'text/plain');
59
+ this.expect(await this.read('notes/today.txt')).toEqual(TEXT);
60
+ this.expect((await this.subject.stat('notes/today.txt'))?.mediaType).toBe('text/plain');
61
+ });
62
+ this.it('answers undefined for a key nothing is stored under', async () => {
63
+ this.expect(await this.subject.get('documents/missing.pdf')).toBe(undefined);
64
+ this.expect(await this.subject.stat('documents/missing.pdf')).toBe(undefined);
65
+ });
66
+ }
67
+ limits() {
68
+ this.it('refuses a content beyond its limit, and leaves what the key held before', async () => {
69
+ await this.write('documents/report.pdf', TEXT, 'text/plain');
70
+ const code = await this.codeOf(() => this.subject.put('documents/report.pdf', largeContent(), { mediaType: 'application/pdf', maxBytes: 1_000_000 }));
71
+ this.expect(code).toBe('FILE_TOO_LARGE');
72
+ this.expect(await this.read('documents/report.pdf')).toEqual(TEXT);
73
+ });
74
+ this.it('leaves nothing under a new key whose content was refused', async () => {
75
+ const code = await this.codeOf(() => this.subject.put('documents/new.pdf', chunksOf(PDF), { mediaType: 'application/pdf', maxBytes: 10 }));
76
+ this.expect(code).toBe('FILE_TOO_LARGE');
77
+ this.expect(await this.subject.stat('documents/new.pdf')).toBe(undefined);
78
+ });
79
+ this.it('accepts a content of exactly its limit', async () => {
80
+ const stored = await this.subject.put('documents/exact.pdf', chunksOf(PDF), {
81
+ mediaType: 'application/pdf',
82
+ maxBytes: PDF.length,
83
+ });
84
+ this.expect(stored.size).toBe(PDF.length);
85
+ });
86
+ }
87
+ keys() {
88
+ this.it('refuses a key that could leave its place, before touching anything', async () => {
89
+ for (const key of INVALID_KEYS) {
90
+ this.expect(await this.codeOf(() => this.write(key, PDF))).toBe('INVALID_FILE_KEY');
91
+ this.expect(await this.codeOf(() => this.subject.get(key))).toBe('INVALID_FILE_KEY');
92
+ this.expect(await this.codeOf(() => this.subject.stat(key))).toBe('INVALID_FILE_KEY');
93
+ this.expect(await this.codeOf(() => this.subject.delete(key))).toBe('INVALID_FILE_KEY');
94
+ }
95
+ });
96
+ }
97
+ removes() {
98
+ this.it('forgets a deleted file, and only that one', async () => {
99
+ await this.write('documents/a.pdf', PDF);
100
+ await this.write('documents/b.pdf', PDF);
101
+ await this.subject.delete('documents/a.pdf');
102
+ this.expect(await this.subject.stat('documents/a.pdf')).toBe(undefined);
103
+ this.expect(await this.read('documents/b.pdf')).toEqual(PDF);
104
+ });
105
+ this.it('accepts the deletion of a file that is not there', async () => {
106
+ this.expect(await this.codeOf(() => this.subject.delete('documents/missing.pdf'))).toBe('resolved');
107
+ });
108
+ }
109
+ }
110
+ //# sourceMappingURL=content-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-suite.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/content-suite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** The contents the suites write: a small PDF, a line of text, and the size of a large one. */
2
+ export declare const PDF: Uint8Array<ArrayBuffer>;
3
+ export declare const TEXT: Uint8Array<ArrayBuffer>;
4
+ /** Six mebibytes, enough to cross the part size of a multipart upload. */
5
+ export declare const LARGE_SIZE: number;
6
+ export declare const LARGE_CHUNK = 65536;
7
+ //# sourceMappingURL=contents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/contents.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,eAAO,MAAM,GAAG,yBAAwF,CAAC;AAEzG,eAAO,MAAM,IAAI,yBAAkE,CAAC;AAEpF,0EAA0E;AAC1E,eAAO,MAAM,UAAU,QAAoB,CAAC;AAE5C,eAAO,MAAM,WAAW,QAAS,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** The contents the suites write: a small PDF, a line of text, and the size of a large one. */
2
+ export const PDF = new Uint8Array(Buffer.from('%PDF-1.7\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n'));
3
+ export const TEXT = new Uint8Array(Buffer.from('Appointment notes, plain text.\n'));
4
+ /** Six mebibytes, enough to cross the part size of a multipart upload. */
5
+ export const LARGE_SIZE = 6 * 1_024 * 1_024;
6
+ export const LARGE_CHUNK = 65_536;
7
+ //# sourceMappingURL=contents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contents.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/contents.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC,CAAC;AAEzG,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC,CAAC;AAEpF,0EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC;AAE5C,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { FileStorage } from '@ddd-ts-kit/kernel';
2
+ import type { Awaitable } from '../awaitable.js';
3
+ import type { FollowedResponse } from './followed-response.js';
4
+ import type { SignedRequest } from './signed-request.js';
5
+ export interface FileStorageContractOptions {
6
+ readonly name: string;
7
+ /** A storage that holds nothing; called before each test. */
8
+ createStorage(): Awaitable<FileStorage>;
9
+ /** Makes the time of the signed links pass: advances their clock, or really waits. */
10
+ elapse(ms: number): Awaitable<void>;
11
+ /**
12
+ * Sends a request to a signed link. Default: `fetch`, for a storage that answers its own links.
13
+ * A storage the application serves passes `followServed(storage)`.
14
+ */
15
+ follow?(request: SignedRequest): Promise<FollowedResponse>;
16
+ }
17
+ //# sourceMappingURL=file-storage-contract-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-contract-options.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/file-storage-contract-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,aAAa,IAAI,SAAS,CAAC,WAAW,CAAC,CAAC;IACxC,sFAAsF;IACtF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC5D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=file-storage-contract-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-contract-options.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/file-storage-contract-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import { type FileStorage } from '@ddd-ts-kit/kernel';
2
+ import { ContractSuite } from '../contract-suite.js';
3
+ import type { ContractRunner } from '../contract-runner.js';
4
+ import type { FileStorageContractOptions } from './file-storage-contract-options.js';
5
+ /** What the suites of the file storage contract share: the options, and reading back what was written. */
6
+ export declare abstract class FileStorageSuite extends ContractSuite<FileStorage> {
7
+ protected readonly options: FileStorageContractOptions;
8
+ constructor(runner: ContractRunner, options: FileStorageContractOptions);
9
+ protected write(key: string, bytes: Uint8Array, mediaType?: string): Promise<void>;
10
+ protected read(key: string): Promise<Uint8Array | undefined>;
11
+ /** The code of the `FileError` the work rejects with, `resolved` when it does not, the message of another error. */
12
+ protected codeOf(work: () => Promise<unknown>): Promise<string>;
13
+ }
14
+ //# sourceMappingURL=file-storage-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/file-storage-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,0GAA0G;AAC1G,8BAAsB,gBAAiB,SAAQ,aAAa,CAAC,WAAW,CAAC;IAGrE,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,0BAA0B;IAFxD,YACE,MAAM,EAAE,cAAc,EACH,OAAO,EAAE,0BAA0B,EAGvD;IAED,UAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,SAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElG;IAED,UAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAIjE;IAED,oHAAoH;IACpH,UAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAQpE;CACF"}
@@ -0,0 +1,30 @@
1
+ import { FileError } from '@ddd-ts-kit/kernel';
2
+ import { ContractSuite } from '../contract-suite.js';
3
+ import { chunksOf } from './chunks-of.js';
4
+ import { readAll } from './read-all.js';
5
+ /** What the suites of the file storage contract share: the options, and reading back what was written. */
6
+ export class FileStorageSuite extends ContractSuite {
7
+ options;
8
+ constructor(runner, options) {
9
+ super(runner, () => options.createStorage());
10
+ this.options = options;
11
+ }
12
+ async write(key, bytes, mediaType = 'application/pdf') {
13
+ await this.subject.put(key, chunksOf(bytes), { mediaType });
14
+ }
15
+ async read(key) {
16
+ const content = await this.subject.get(key);
17
+ return content === undefined ? undefined : readAll(content.stream);
18
+ }
19
+ /** The code of the `FileError` the work rejects with, `resolved` when it does not, the message of another error. */
20
+ async codeOf(work) {
21
+ try {
22
+ await work();
23
+ return 'resolved';
24
+ }
25
+ catch (error) {
26
+ return FileError.is(error) ? error.code : String(error);
27
+ }
28
+ }
29
+ }
30
+ //# sourceMappingURL=file-storage-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-suite.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/file-storage-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,0GAA0G;AAC1G,MAAM,OAAgB,gBAAiB,SAAQ,aAA0B;IAGlD,OAAO;IAF5B,YACE,MAAsB,EACH,OAAmC;QAEtD,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;uBAF1B,OAAO;IAG5B,CAAC;IAES,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,KAAiB,EAAE,SAAS,GAAG,iBAAiB;QACjF,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9D,CAAC;IAES,KAAK,CAAC,IAAI,CAAC,GAAW;QAC9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAE5C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,CAAC;IAED,oHAAoH;IAC1G,KAAK,CAAC,MAAM,CAAC,IAA4B;QACjD,IAAI,CAAC;YACH,MAAM,IAAI,EAAE,CAAC;YAEb,OAAO,UAAU,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,SAAS,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import { type ServedFileStorage } from '@ddd-ts-kit/kernel';
2
+ import type { FollowedResponse } from './followed-response.js';
3
+ import type { SignedRequest } from './signed-request.js';
4
+ /**
5
+ * Follows a link of a storage the application serves, without a server: the token is the `token`
6
+ * parameter of the query, answered the way `signedFileRoutes` of `@ddd-ts-kit/http` answers it.
7
+ */
8
+ export declare function followServed(storage: ServedFileStorage): (request: SignedRequest) => Promise<FollowedResponse>;
9
+ //# sourceMappingURL=follow-served.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-served.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/follow-served.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAiB9G"}
@@ -0,0 +1,28 @@
1
+ import { Readable } from 'node:stream';
2
+ import { contentDisposition } from '@ddd-ts-kit/kernel';
3
+ import { readAll } from './read-all.js';
4
+ /**
5
+ * Follows a link of a storage the application serves, without a server: the token is the `token`
6
+ * parameter of the query, answered the way `signedFileRoutes` of `@ddd-ts-kit/http` answers it.
7
+ */
8
+ export function followServed(storage) {
9
+ return async (request) => {
10
+ const token = new URL(request.url).searchParams.get('token') ?? '';
11
+ if (request.method === 'GET') {
12
+ const download = await storage.openSigned(token);
13
+ return download === undefined
14
+ ? refused()
15
+ : { status: 200, headers: dispositionOf(download.fileName), body: await readAll(download.stream) };
16
+ }
17
+ const body = Readable.from([Buffer.from(request.body ?? new Uint8Array())]);
18
+ const stored = await storage.acceptSigned(token, body, request.headers?.['content-type'] ?? '');
19
+ return stored === undefined ? refused() : { status: 200, headers: {}, body: new Uint8Array() };
20
+ };
21
+ }
22
+ function refused() {
23
+ return { status: 403, headers: {}, body: new Uint8Array() };
24
+ }
25
+ function dispositionOf(fileName) {
26
+ return { 'content-disposition': contentDisposition(fileName) };
27
+ }
28
+ //# sourceMappingURL=follow-served.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-served.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/follow-served.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAA0B,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAGxC;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAA0B;IACrD,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAEnE,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAEjD,OAAO,QAAQ,KAAK,SAAS;gBAC3B,CAAC,CAAC,OAAO,EAAE;gBACX,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACvG,CAAC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC;QAEhG,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,EAAE,CAAC;IACjG,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO;IACd,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,EAAE,CAAC;AAC9D,CAAC;AAED,SAAS,aAAa,CAAC,QAA4B;IACjD,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { FollowedResponse } from './followed-response.js';
2
+ import type { SignedRequest } from './signed-request.js';
3
+ /** Follows a link over the network: for a storage that answers its own links, an object store. */
4
+ export declare function followWithFetch(request: SignedRequest): Promise<FollowedResponse>;
5
+ //# sourceMappingURL=follow-with-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-with-fetch.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/follow-with-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,kGAAkG;AAClG,wBAAsB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYvF"}
@@ -0,0 +1,14 @@
1
+ /** Follows a link over the network: for a storage that answers its own links, an object store. */
2
+ export async function followWithFetch(request) {
3
+ const response = await fetch(request.url, {
4
+ method: request.method,
5
+ headers: { ...request.headers },
6
+ ...(request.body === undefined ? {} : { body: request.body }),
7
+ });
8
+ return {
9
+ status: response.status,
10
+ headers: Object.fromEntries(response.headers.entries()),
11
+ body: new Uint8Array(await response.arrayBuffer()),
12
+ };
13
+ }
14
+ //# sourceMappingURL=follow-with-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follow-with-fetch.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/follow-with-fetch.ts"],"names":[],"mappings":"AAGA,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAsB;IAC1D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE;QAC/B,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;KAC9D,CAAC,CAAC;IAEH,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACvD,IAAI,EAAE,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;KACnD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** What a signed link answered; header names in lower case. */
2
+ export interface FollowedResponse {
3
+ readonly status: number;
4
+ readonly headers: Readonly<Record<string, string>>;
5
+ readonly body: Uint8Array;
6
+ }
7
+ //# sourceMappingURL=followed-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"followed-response.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/followed-response.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=followed-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"followed-response.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/followed-response.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ /** Six mebibytes in chunks of 64 KiB, each filled with its rank: never held whole. */
2
+ export declare function largeContent(): AsyncIterable<Uint8Array>;
3
+ //# sourceMappingURL=large-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"large-content.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/large-content.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,wBAAuB,YAAY,IAAI,aAAa,CAAC,UAAU,CAAC,CAI/D"}
@@ -0,0 +1,8 @@
1
+ import { LARGE_CHUNK, LARGE_SIZE } from './contents.js';
2
+ /** Six mebibytes in chunks of 64 KiB, each filled with its rank: never held whole. */
3
+ export async function* largeContent() {
4
+ for (let offset = 0; offset < LARGE_SIZE; offset += LARGE_CHUNK) {
5
+ yield new Uint8Array(LARGE_CHUNK).fill(offset / LARGE_CHUNK);
6
+ }
7
+ }
8
+ //# sourceMappingURL=large-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"large-content.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/large-content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAExD,sFAAsF;AACtF,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,YAAY;IACjC,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE,MAAM,IAAI,WAAW,EAAE,CAAC;QAChE,MAAM,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** The whole content of a stream: for the small files of a test, never in an adapter. */
2
+ export declare function readAll(stream: AsyncIterable<unknown>): Promise<Uint8Array>;
3
+ //# sourceMappingURL=read-all.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-all.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/read-all.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAQjF"}
@@ -0,0 +1,9 @@
1
+ /** The whole content of a stream: for the small files of a test, never in an adapter. */
2
+ export async function readAll(stream) {
3
+ const chunks = [];
4
+ for await (const chunk of stream) {
5
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk instanceof Uint8Array ? chunk : String(chunk)));
6
+ }
7
+ return new Uint8Array(Buffer.concat(chunks));
8
+ }
9
+ //# sourceMappingURL=read-all.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-all.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/read-all.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAA8B;IAC1D,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjH,CAAC;IAED,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { FileStorageSuite } from './file-storage-suite.js';
2
+ /** A signed link grants one action on one file, for a short time, to whoever holds it unchanged. */
3
+ export declare class SignedLinksSuite extends FileStorageSuite {
4
+ protected tests(): void;
5
+ private downloads;
6
+ private uploads;
7
+ private refuses;
8
+ private textUpload;
9
+ private follow;
10
+ }
11
+ //# sourceMappingURL=signed-links-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-links-suite.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/signed-links-suite.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,oGAAoG;AACpG,qBAAa,gBAAiB,SAAQ,gBAAgB;IACpD,SAAS,CAAC,KAAK,IAAI,IAAI,CAItB;IAED,OAAO,CAAC,SAAS;IAmCjB,OAAO,CAAC,OAAO;IAiCf,OAAO,CAAC,OAAO;IA2Bf,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,MAAM;CAGf"}
@@ -0,0 +1,93 @@
1
+ import { MAX_SIGNED_URL_SECONDS } from '@ddd-ts-kit/kernel';
2
+ import { PDF, TEXT } from './contents.js';
3
+ import { tampered } from './tampered.js';
4
+ import { followWithFetch } from './follow-with-fetch.js';
5
+ import { FileStorageSuite } from './file-storage-suite.js';
6
+ /** A signed link grants one action on one file, for a short time, to whoever holds it unchanged. */
7
+ export class SignedLinksSuite extends FileStorageSuite {
8
+ tests() {
9
+ this.downloads();
10
+ this.uploads();
11
+ this.refuses();
12
+ }
13
+ downloads() {
14
+ this.it('downloads the file through its link, under the name it was signed with', async () => {
15
+ await this.write('documents/report.pdf', PDF);
16
+ const link = await this.subject.signedDownloadUrl('documents/report.pdf', {
17
+ expiresInSeconds: 300,
18
+ fileName: 'report.pdf',
19
+ });
20
+ const longer = await this.subject.signedDownloadUrl('documents/report.pdf', { expiresInSeconds: 600 });
21
+ const response = await this.follow({ url: link.url, method: 'GET' });
22
+ this.expect([response.status, response.body]).toEqual([200, PDF]);
23
+ this.expect(response.headers['content-disposition']?.includes('report.pdf')).toBe(true);
24
+ this.expect(Math.abs(longer.expiresAt.getTime() - link.expiresAt.getTime() - 300_000) < 5_000).toBe(true);
25
+ });
26
+ this.it('serves a link signed without a name as an attachment all the same, never inline', async () => {
27
+ await this.write('documents/report.pdf', PDF);
28
+ const link = await this.subject.signedDownloadUrl('documents/report.pdf', { expiresInSeconds: 300 });
29
+ const response = await this.follow({ url: link.url, method: 'GET' });
30
+ this.expect(response.headers['content-disposition']?.startsWith('attachment')).toBe(true);
31
+ });
32
+ this.it('refuses a download link once it has expired', async () => {
33
+ await this.write('documents/report.pdf', PDF);
34
+ const link = await this.subject.signedDownloadUrl('documents/report.pdf', { expiresInSeconds: 1 });
35
+ await this.options.elapse(2_100);
36
+ this.expect((await this.follow({ url: link.url, method: 'GET' })).status >= 400).toBe(true);
37
+ });
38
+ }
39
+ uploads() {
40
+ this.it('stores what is sent through an upload link, with the media type it was signed for', async () => {
41
+ const upload = await this.textUpload();
42
+ const response = await this.follow({
43
+ url: upload.url,
44
+ method: upload.method,
45
+ headers: upload.headers,
46
+ body: TEXT,
47
+ });
48
+ const metadata = await this.subject.stat('staging/documents/notes.txt');
49
+ this.expect(response.status < 300).toBe(true);
50
+ this.expect([metadata?.size, metadata?.mediaType]).toEqual([TEXT.length, 'text/plain']);
51
+ this.expect(await this.read('staging/documents/notes.txt')).toEqual(TEXT);
52
+ });
53
+ this.it('refuses an upload sent with another media type than the one signed', async () => {
54
+ const upload = await this.textUpload();
55
+ const response = await this.follow({
56
+ url: upload.url,
57
+ method: 'PUT',
58
+ headers: { 'content-type': 'text/html' },
59
+ body: TEXT,
60
+ });
61
+ this.expect(response.status >= 400).toBe(true);
62
+ this.expect(await this.subject.stat('staging/documents/notes.txt')).toBe(undefined);
63
+ });
64
+ }
65
+ refuses() {
66
+ this.it('refuses a link whose signature was changed', async () => {
67
+ await this.write('documents/report.pdf', PDF);
68
+ const link = await this.subject.signedDownloadUrl('documents/report.pdf', { expiresInSeconds: 300 });
69
+ this.expect((await this.follow({ url: tampered(link.url), method: 'GET' })).status >= 400).toBe(true);
70
+ });
71
+ this.it('refuses to sign a link for longer than the maximum, or for no time', async () => {
72
+ for (const expiresInSeconds of [MAX_SIGNED_URL_SECONDS + 1, 0, 1.5]) {
73
+ const code = await this.codeOf(() => this.subject.signedDownloadUrl('documents/report.pdf', { expiresInSeconds }));
74
+ this.expect(code).toBe('INVALID_FILE_URL_EXPIRY');
75
+ }
76
+ });
77
+ this.it('refuses to sign a link for a key that could leave its place', async () => {
78
+ const code = await this.codeOf(() => this.subject.signedUploadUrl('../outside', { expiresInSeconds: 60, mediaType: 'text/plain', maxBytes: 1_000 }));
79
+ this.expect(code).toBe('INVALID_FILE_KEY');
80
+ });
81
+ }
82
+ textUpload() {
83
+ return this.subject.signedUploadUrl('staging/documents/notes.txt', {
84
+ expiresInSeconds: 300,
85
+ mediaType: 'text/plain',
86
+ maxBytes: 1_000,
87
+ });
88
+ }
89
+ follow(request) {
90
+ return (this.options.follow ?? followWithFetch)(request);
91
+ }
92
+ }
93
+ //# sourceMappingURL=signed-links-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-links-suite.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/signed-links-suite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /** A request to a signed link, as a client would send it. */
2
+ export interface SignedRequest {
3
+ readonly url: string;
4
+ readonly method: 'GET' | 'PUT';
5
+ readonly headers?: Readonly<Record<string, string>>;
6
+ readonly body?: Uint8Array;
7
+ }
8
+ //# sourceMappingURL=signed-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-request.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/signed-request.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;CAC5B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signed-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-request.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/signed-request.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ /** The link with its last character changed: the end of the signature, for every connector of the kit. */
2
+ export declare function tampered(url: string): string;
3
+ //# sourceMappingURL=tampered.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tampered.d.ts","sourceRoot":"","sources":["../../../src/contracts/file-storage/tampered.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5C"}
@@ -0,0 +1,5 @@
1
+ /** The link with its last character changed: the end of the signature, for every connector of the kit. */
2
+ export function tampered(url) {
3
+ return `${url.slice(0, -1)}${url.endsWith('A') ? 'B' : 'A'}`;
4
+ }
5
+ //# sourceMappingURL=tampered.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tampered.js","sourceRoot":"","sources":["../../../src/contracts/file-storage/tampered.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ContractRunner } from './contract-runner.js';
2
+ import type { FileStorageContractOptions } from './file-storage/file-storage-contract-options.js';
3
+ /**
4
+ * What every connector of the kernel `FileStorage` port guarantees, the in-memory one included:
5
+ * content streams in and out intact and measured, a refused write leaves nothing, keys never
6
+ * leave their place, copies, moves and listings by key, and short signed links that grant one
7
+ * action. A connector of your own (Google Cloud Storage, Azure Blob Storage) is done when it
8
+ * passes this suite against the real service or its emulator.
9
+ */
10
+ export declare function fileStorageContract(runner: ContractRunner, options: FileStorageContractOptions): void;
11
+ //# sourceMappingURL=file-storage-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-contract.d.ts","sourceRoot":"","sources":["../../src/contracts/file-storage-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAC;AAGlG;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAIrG"}
@@ -0,0 +1,16 @@
1
+ import { ArrangementSuite } from './file-storage/arrangement-suite.js';
2
+ import { ContentSuite } from './file-storage/content-suite.js';
3
+ import { SignedLinksSuite } from './file-storage/signed-links-suite.js';
4
+ /**
5
+ * What every connector of the kernel `FileStorage` port guarantees, the in-memory one included:
6
+ * content streams in and out intact and measured, a refused write leaves nothing, keys never
7
+ * leave their place, copies, moves and listings by key, and short signed links that grant one
8
+ * action. A connector of your own (Google Cloud Storage, Azure Blob Storage) is done when it
9
+ * passes this suite against the real service or its emulator.
10
+ */
11
+ export function fileStorageContract(runner, options) {
12
+ new ContentSuite(runner, options).describe(`${options.name} (file storage contract: content)`);
13
+ new ArrangementSuite(runner, options).describe(`${options.name} (file storage contract: copy, move, list)`);
14
+ new SignedLinksSuite(runner, options).describe(`${options.name} (file storage contract: signed links)`);
15
+ }
16
+ //# sourceMappingURL=file-storage-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-storage-contract.js","sourceRoot":"","sources":["../../src/contracts/file-storage-contract.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAsB,EAAE,OAAmC;IAC7F,IAAI,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,mCAAmC,CAAC,CAAC;IAC/F,IAAI,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,4CAA4C,CAAC,CAAC;IAC5G,IAAI,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,wCAAwC,CAAC,CAAC;AAC1G,CAAC"}
@@ -8,6 +8,12 @@ export { criteriaContractDefinition } from './contracts/criteria/criteria-contra
8
8
  export type { CriteriaContractRow } from './contracts/criteria/criteria-contract-row.js';
9
9
  export { criteriaContractRows } from './contracts/criteria/criteria-contract-rows.js';
10
10
  export { eventBusContract, type EventBusContractOptions, type EventBusUnderContract, } from './contracts/event-bus-contract.js';
11
+ export { fileStorageContract } from './contracts/file-storage-contract.js';
12
+ export type { FileStorageContractOptions } from './contracts/file-storage/file-storage-contract-options.js';
13
+ export { followServed } from './contracts/file-storage/follow-served.js';
14
+ export { followWithFetch } from './contracts/file-storage/follow-with-fetch.js';
15
+ export type { FollowedResponse } from './contracts/file-storage/followed-response.js';
16
+ export type { SignedRequest } from './contracts/file-storage/signed-request.js';
11
17
  export { jobSchedulerContract, type JobSchedulerContractOptions, type JobSchedulerUnderContract, } from './contracts/job-scheduler-contract.js';
12
18
  export { lockContract, type LockContractOptions } from './contracts/lock-contract.js';
13
19
  export { loggerContract, type LoggerContractOptions } from './contracts/logger-contract.js';
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACzF,YAAY,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AAClG,YAAY,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,GAC3B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,YAAY,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,YAAY,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AACvF,YAAY,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AACvF,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,GAC9B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAC5G,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,0CAA0C,CAAC;AACtH,YAAY,EAAE,2BAA2B,EAAE,MAAM,2DAA2D,CAAC;AAC7G,OAAO,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACxG,OAAO,EACL,uBAAuB,EACvB,KAAK,8BAA8B,GACpC,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAC1G,YAAY,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACzF,YAAY,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AAClG,YAAY,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,GAC3B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,2DAA2D,CAAC;AAC5G,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,+CAA+C,CAAC;AAChF,YAAY,EAAE,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,MAAM,4CAA4C,CAAC;AAChF,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,YAAY,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,YAAY,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AACvF,YAAY,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AACvF,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,GAC9B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAC5G,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,0CAA0C,CAAC;AACtH,YAAY,EAAE,2BAA2B,EAAE,MAAM,2DAA2D,CAAC;AAC7G,OAAO,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACxG,OAAO,EACL,uBAAuB,EACvB,KAAK,8BAA8B,GACpC,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAC1G,YAAY,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC"}
package/dist/contracts.js CHANGED
@@ -4,6 +4,9 @@ export { criteriaContract, } from './contracts/criteria-contract.js';
4
4
  export { criteriaContractDefinition } from './contracts/criteria/criteria-contract-definition.js';
5
5
  export { criteriaContractRows } from './contracts/criteria/criteria-contract-rows.js';
6
6
  export { eventBusContract, } from './contracts/event-bus-contract.js';
7
+ export { fileStorageContract } from './contracts/file-storage-contract.js';
8
+ export { followServed } from './contracts/file-storage/follow-served.js';
9
+ export { followWithFetch } from './contracts/file-storage/follow-with-fetch.js';
7
10
  export { jobSchedulerContract, } from './contracts/job-scheduler-contract.js';
8
11
  export { lockContract } from './contracts/lock-contract.js';
9
12
  export { loggerContract } from './contracts/logger-contract.js';
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA6B,MAAM,+BAA+B,CAAC;AAEzF,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,gBAAgB,GAGjB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AAElG,OAAO,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AACtF,OAAO,EACL,gBAAgB,GAGjB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,oBAAoB,GAGrB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAA4B,MAAM,8BAA8B,CAAC;AACtF,OAAO,EAAE,cAAc,EAA8B,MAAM,gCAAgC,CAAC;AAK5F,OAAO,EACL,mBAAmB,GAGpB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,2BAA2B,GAE5B,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAmC,MAAM,sCAAsC,CAAC;AAC5G,OAAO,EAAE,sBAAsB,EAAsC,MAAM,0CAA0C,CAAC;AAEtH,OAAO,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAkC,MAAM,oCAAoC,CAAC;AACxG,OAAO,EACL,uBAAuB,GAExB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAkC,MAAM,sCAAsC,CAAC"}
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA6B,MAAM,+BAA+B,CAAC;AAEzF,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,gBAAgB,GAGjB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AAElG,OAAO,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AACtF,OAAO,EACL,gBAAgB,GAGjB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAE3E,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,+CAA+C,CAAC;AAGhF,OAAO,EACL,oBAAoB,GAGrB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAA4B,MAAM,8BAA8B,CAAC;AACtF,OAAO,EAAE,cAAc,EAA8B,MAAM,gCAAgC,CAAC;AAK5F,OAAO,EACL,mBAAmB,GAGpB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,2BAA2B,GAE5B,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAmC,MAAM,sCAAsC,CAAC;AAC5G,OAAO,EAAE,sBAAsB,EAAsC,MAAM,0CAA0C,CAAC;AAEtH,OAAO,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAkC,MAAM,oCAAoC,CAAC;AACxG,OAAO,EACL,uBAAuB,GAExB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAkC,MAAM,sCAAsC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/testing",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Readable tests without mocks: test data builders, given/when/then scenarios and contract suites shared by fakes and real adapters.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,11 +46,11 @@
46
46
  "dist"
47
47
  ],
48
48
  "dependencies": {
49
- "@ddd-ts-kit/kernel": "0.8.0"
49
+ "@ddd-ts-kit/kernel": "0.9.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@ddd-ts-kit/aspects": "0.8.0",
53
- "@ddd-ts-kit/events": "0.8.0",
52
+ "@ddd-ts-kit/aspects": "0.9.0",
53
+ "@ddd-ts-kit/events": "0.9.0",
54
54
  "@faker-js/faker": "^10.6.0"
55
55
  },
56
56
  "peerDependencies": {