@vankyle/storage-core 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 (190) hide show
  1. package/README.md +83 -0
  2. package/dist/application/index.d.ts +3 -0
  3. package/dist/application/index.d.ts.map +1 -0
  4. package/dist/application/index.js +3 -0
  5. package/dist/application/index.js.map +1 -0
  6. package/dist/application/policies/index.d.ts +2 -0
  7. package/dist/application/policies/index.d.ts.map +1 -0
  8. package/dist/application/policies/index.js +2 -0
  9. package/dist/application/policies/index.js.map +1 -0
  10. package/dist/application/policies/object-key-policy.d.ts +13 -0
  11. package/dist/application/policies/object-key-policy.d.ts.map +1 -0
  12. package/dist/application/policies/object-key-policy.js +24 -0
  13. package/dist/application/policies/object-key-policy.js.map +1 -0
  14. package/dist/application/policies/object-key-policy.test.d.ts +2 -0
  15. package/dist/application/policies/object-key-policy.test.d.ts.map +1 -0
  16. package/dist/application/policies/object-key-policy.test.js +53 -0
  17. package/dist/application/policies/object-key-policy.test.js.map +1 -0
  18. package/dist/application/services/default-storage-service.d.ts +39 -0
  19. package/dist/application/services/default-storage-service.d.ts.map +1 -0
  20. package/dist/application/services/default-storage-service.js +290 -0
  21. package/dist/application/services/default-storage-service.js.map +1 -0
  22. package/dist/application/services/default-storage-service.test.d.ts +2 -0
  23. package/dist/application/services/default-storage-service.test.d.ts.map +1 -0
  24. package/dist/application/services/default-storage-service.test.js +902 -0
  25. package/dist/application/services/default-storage-service.test.js.map +1 -0
  26. package/dist/application/services/index.d.ts +2 -0
  27. package/dist/application/services/index.d.ts.map +1 -0
  28. package/dist/application/services/index.js +2 -0
  29. package/dist/application/services/index.js.map +1 -0
  30. package/dist/domain/enums/blob-status.d.ts +8 -0
  31. package/dist/domain/enums/blob-status.d.ts.map +1 -0
  32. package/dist/domain/enums/blob-status.js +7 -0
  33. package/dist/domain/enums/blob-status.js.map +1 -0
  34. package/dist/domain/enums/enums.test.d.ts +2 -0
  35. package/dist/domain/enums/enums.test.d.ts.map +1 -0
  36. package/dist/domain/enums/enums.test.js +59 -0
  37. package/dist/domain/enums/enums.test.js.map +1 -0
  38. package/dist/domain/enums/file-status.d.ts +6 -0
  39. package/dist/domain/enums/file-status.d.ts.map +1 -0
  40. package/dist/domain/enums/file-status.js +5 -0
  41. package/dist/domain/enums/file-status.js.map +1 -0
  42. package/dist/domain/enums/index.d.ts +5 -0
  43. package/dist/domain/enums/index.d.ts.map +1 -0
  44. package/dist/domain/enums/index.js +5 -0
  45. package/dist/domain/enums/index.js.map +1 -0
  46. package/dist/domain/enums/storage-provider.d.ts +7 -0
  47. package/dist/domain/enums/storage-provider.d.ts.map +1 -0
  48. package/dist/domain/enums/storage-provider.js +6 -0
  49. package/dist/domain/enums/storage-provider.js.map +1 -0
  50. package/dist/domain/enums/upload-status.d.ts +13 -0
  51. package/dist/domain/enums/upload-status.d.ts.map +1 -0
  52. package/dist/domain/enums/upload-status.js +11 -0
  53. package/dist/domain/enums/upload-status.js.map +1 -0
  54. package/dist/domain/index.d.ts +4 -0
  55. package/dist/domain/index.d.ts.map +1 -0
  56. package/dist/domain/index.js +4 -0
  57. package/dist/domain/index.js.map +1 -0
  58. package/dist/domain/models/blob-reference.d.ts +8 -0
  59. package/dist/domain/models/blob-reference.d.ts.map +1 -0
  60. package/dist/domain/models/blob-reference.js +2 -0
  61. package/dist/domain/models/blob-reference.js.map +1 -0
  62. package/dist/domain/models/blob.d.ts +20 -0
  63. package/dist/domain/models/blob.d.ts.map +1 -0
  64. package/dist/domain/models/blob.js +2 -0
  65. package/dist/domain/models/blob.js.map +1 -0
  66. package/dist/domain/models/file-version.d.ts +14 -0
  67. package/dist/domain/models/file-version.d.ts.map +1 -0
  68. package/dist/domain/models/file-version.js +2 -0
  69. package/dist/domain/models/file-version.js.map +1 -0
  70. package/dist/domain/models/file.d.ts +17 -0
  71. package/dist/domain/models/file.d.ts.map +1 -0
  72. package/dist/domain/models/file.js +2 -0
  73. package/dist/domain/models/file.js.map +1 -0
  74. package/dist/domain/models/index.d.ts +7 -0
  75. package/dist/domain/models/index.d.ts.map +1 -0
  76. package/dist/domain/models/index.js +2 -0
  77. package/dist/domain/models/index.js.map +1 -0
  78. package/dist/domain/models/upload-session.d.ts +26 -0
  79. package/dist/domain/models/upload-session.d.ts.map +1 -0
  80. package/dist/domain/models/upload-session.js +2 -0
  81. package/dist/domain/models/upload-session.js.map +1 -0
  82. package/dist/domain/models/uploaded-part.d.ts +13 -0
  83. package/dist/domain/models/uploaded-part.d.ts.map +1 -0
  84. package/dist/domain/models/uploaded-part.js +2 -0
  85. package/dist/domain/models/uploaded-part.js.map +1 -0
  86. package/dist/domain/value-objects/index.d.ts +3 -0
  87. package/dist/domain/value-objects/index.d.ts.map +1 -0
  88. package/dist/domain/value-objects/index.js +2 -0
  89. package/dist/domain/value-objects/index.js.map +1 -0
  90. package/dist/domain/value-objects/signed-access.d.ts +9 -0
  91. package/dist/domain/value-objects/signed-access.d.ts.map +1 -0
  92. package/dist/domain/value-objects/signed-access.js +2 -0
  93. package/dist/domain/value-objects/signed-access.js.map +1 -0
  94. package/dist/domain/value-objects/storage-locator.d.ts +7 -0
  95. package/dist/domain/value-objects/storage-locator.d.ts.map +1 -0
  96. package/dist/domain/value-objects/storage-locator.js +2 -0
  97. package/dist/domain/value-objects/storage-locator.js.map +1 -0
  98. package/dist/index.d.ts +6 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +6 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/ports/index.d.ts +4 -0
  103. package/dist/ports/index.d.ts.map +1 -0
  104. package/dist/ports/index.js +4 -0
  105. package/dist/ports/index.js.map +1 -0
  106. package/dist/ports/metadata/i-blob-store.d.ts +38 -0
  107. package/dist/ports/metadata/i-blob-store.d.ts.map +1 -0
  108. package/dist/ports/metadata/i-blob-store.js +2 -0
  109. package/dist/ports/metadata/i-blob-store.js.map +1 -0
  110. package/dist/ports/metadata/i-file-store.d.ts +43 -0
  111. package/dist/ports/metadata/i-file-store.d.ts.map +1 -0
  112. package/dist/ports/metadata/i-file-store.js +2 -0
  113. package/dist/ports/metadata/i-file-store.js.map +1 -0
  114. package/dist/ports/metadata/i-metadata-store.d.ts +9 -0
  115. package/dist/ports/metadata/i-metadata-store.d.ts.map +1 -0
  116. package/dist/ports/metadata/i-metadata-store.js +2 -0
  117. package/dist/ports/metadata/i-metadata-store.js.map +1 -0
  118. package/dist/ports/metadata/i-upload-session-store.d.ts +48 -0
  119. package/dist/ports/metadata/i-upload-session-store.d.ts.map +1 -0
  120. package/dist/ports/metadata/i-upload-session-store.js +2 -0
  121. package/dist/ports/metadata/i-upload-session-store.js.map +1 -0
  122. package/dist/ports/metadata/index.d.ts +5 -0
  123. package/dist/ports/metadata/index.d.ts.map +1 -0
  124. package/dist/ports/metadata/index.js +2 -0
  125. package/dist/ports/metadata/index.js.map +1 -0
  126. package/dist/ports/services/i-storage-service.d.ts +75 -0
  127. package/dist/ports/services/i-storage-service.d.ts.map +1 -0
  128. package/dist/ports/services/i-storage-service.js +2 -0
  129. package/dist/ports/services/i-storage-service.js.map +1 -0
  130. package/dist/ports/services/index.d.ts +2 -0
  131. package/dist/ports/services/index.d.ts.map +1 -0
  132. package/dist/ports/services/index.js +2 -0
  133. package/dist/ports/services/index.js.map +1 -0
  134. package/dist/ports/storage/i-storage.d.ts +19 -0
  135. package/dist/ports/storage/i-storage.d.ts.map +1 -0
  136. package/dist/ports/storage/i-storage.js +2 -0
  137. package/dist/ports/storage/i-storage.js.map +1 -0
  138. package/dist/ports/storage/index.d.ts +3 -0
  139. package/dist/ports/storage/index.d.ts.map +1 -0
  140. package/dist/ports/storage/index.js +2 -0
  141. package/dist/ports/storage/index.js.map +1 -0
  142. package/dist/ports/storage/storage.types.d.ts +112 -0
  143. package/dist/ports/storage/storage.types.d.ts.map +1 -0
  144. package/dist/ports/storage/storage.types.js +2 -0
  145. package/dist/ports/storage/storage.types.js.map +1 -0
  146. package/dist/schemas/blob-reference.schema.d.ts +10 -0
  147. package/dist/schemas/blob-reference.schema.d.ts.map +1 -0
  148. package/dist/schemas/blob-reference.schema.js +9 -0
  149. package/dist/schemas/blob-reference.schema.js.map +1 -0
  150. package/dist/schemas/blob.schema.d.ts +28 -0
  151. package/dist/schemas/blob.schema.d.ts.map +1 -0
  152. package/dist/schemas/blob.schema.js +26 -0
  153. package/dist/schemas/blob.schema.js.map +1 -0
  154. package/dist/schemas/file-version.schema.d.ts +15 -0
  155. package/dist/schemas/file-version.schema.d.ts.map +1 -0
  156. package/dist/schemas/file-version.schema.js +15 -0
  157. package/dist/schemas/file-version.schema.js.map +1 -0
  158. package/dist/schemas/file.schema.d.ts +20 -0
  159. package/dist/schemas/file.schema.d.ts.map +1 -0
  160. package/dist/schemas/file.schema.js +18 -0
  161. package/dist/schemas/file.schema.js.map +1 -0
  162. package/dist/schemas/index.d.ts +7 -0
  163. package/dist/schemas/index.d.ts.map +1 -0
  164. package/dist/schemas/index.js +7 -0
  165. package/dist/schemas/index.js.map +1 -0
  166. package/dist/schemas/schemas.test.d.ts +2 -0
  167. package/dist/schemas/schemas.test.d.ts.map +1 -0
  168. package/dist/schemas/schemas.test.js +275 -0
  169. package/dist/schemas/schemas.test.js.map +1 -0
  170. package/dist/schemas/upload-session.schema.d.ts +37 -0
  171. package/dist/schemas/upload-session.schema.d.ts.map +1 -0
  172. package/dist/schemas/upload-session.schema.js +32 -0
  173. package/dist/schemas/upload-session.schema.js.map +1 -0
  174. package/dist/schemas/uploaded-part.schema.d.ts +14 -0
  175. package/dist/schemas/uploaded-part.schema.d.ts.map +1 -0
  176. package/dist/schemas/uploaded-part.schema.js +14 -0
  177. package/dist/schemas/uploaded-part.schema.js.map +1 -0
  178. package/dist/utils/ids.d.ts +2 -0
  179. package/dist/utils/ids.d.ts.map +1 -0
  180. package/dist/utils/ids.js +5 -0
  181. package/dist/utils/ids.js.map +1 -0
  182. package/dist/utils/ids.test.d.ts +2 -0
  183. package/dist/utils/ids.test.d.ts.map +1 -0
  184. package/dist/utils/ids.test.js +13 -0
  185. package/dist/utils/ids.test.js.map +1 -0
  186. package/dist/utils/index.d.ts +2 -0
  187. package/dist/utils/index.d.ts.map +1 -0
  188. package/dist/utils/index.js +2 -0
  189. package/dist/utils/index.js.map +1 -0
  190. package/package.json +40 -0
@@ -0,0 +1,902 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { DefaultStorageService, StorageProvider, UploadMode, UploadSessionStatus, BlobStatus, FileStatus, } from "@vankyle-hub/storage-core";
3
+ import { CapabilityNotSupportedError, MetadataNotFoundError, } from "@vankyle-hub/storage-shared";
4
+ function createMockStorage(overrides) {
5
+ const storage = {
6
+ provider: StorageProvider.S3,
7
+ capabilities: {
8
+ multipartUpload: true,
9
+ signedReadUrl: true,
10
+ signedPutUrl: true,
11
+ signedPartUrl: true,
12
+ },
13
+ putObject: vi.fn(),
14
+ getObject: vi.fn(),
15
+ headObject: vi.fn().mockResolvedValue({
16
+ contentType: "application/octet-stream",
17
+ contentLength: 1024,
18
+ etag: '"abc"',
19
+ }),
20
+ deleteObject: vi.fn(),
21
+ initUploadSession: vi.fn().mockResolvedValue({
22
+ providerUploadId: "provider-upload-1",
23
+ }),
24
+ uploadPart: vi.fn().mockResolvedValue({
25
+ etag: '"part-etag"',
26
+ partNumber: 1,
27
+ size: 512,
28
+ }),
29
+ completeUploadSession: vi.fn().mockResolvedValue({
30
+ etag: '"complete-etag"',
31
+ }),
32
+ abortUploadSession: vi.fn().mockResolvedValue(undefined),
33
+ createReadUrl: vi.fn().mockResolvedValue({
34
+ url: "https://example.com/read",
35
+ method: "GET",
36
+ expiresAt: new Date(Date.now() + 3600_000),
37
+ }),
38
+ createPutUrl: vi.fn().mockResolvedValue({
39
+ url: "https://example.com/put",
40
+ method: "PUT",
41
+ expiresAt: new Date(Date.now() + 3600_000),
42
+ }),
43
+ createUploadPartUrl: vi.fn().mockResolvedValue({
44
+ url: "https://example.com/part",
45
+ method: "PUT",
46
+ expiresAt: new Date(Date.now() + 3600_000),
47
+ }),
48
+ };
49
+ if (!overrides) {
50
+ return storage;
51
+ }
52
+ const { createReadUrl, createPutUrl, createUploadPartUrl, ...restOverrides } = overrides;
53
+ Object.assign(storage, restOverrides);
54
+ if ("createReadUrl" in overrides) {
55
+ if (createReadUrl) {
56
+ storage.createReadUrl = createReadUrl;
57
+ }
58
+ else {
59
+ delete storage.createReadUrl;
60
+ }
61
+ }
62
+ if ("createPutUrl" in overrides) {
63
+ if (createPutUrl) {
64
+ storage.createPutUrl = createPutUrl;
65
+ }
66
+ else {
67
+ delete storage.createPutUrl;
68
+ }
69
+ }
70
+ if ("createUploadPartUrl" in overrides) {
71
+ if (createUploadPartUrl) {
72
+ storage.createUploadPartUrl = createUploadPartUrl;
73
+ }
74
+ else {
75
+ delete storage.createUploadPartUrl;
76
+ }
77
+ }
78
+ return storage;
79
+ }
80
+ function createMockUploadSessionStore() {
81
+ return {
82
+ createSession: vi.fn().mockImplementation(async (input) => ({
83
+ ...input,
84
+ status: UploadSessionStatus.Pending,
85
+ createdAt: new Date(),
86
+ updatedAt: new Date(),
87
+ })),
88
+ getSession: vi.fn(),
89
+ updateSession: vi.fn().mockImplementation(async (id, input) => ({
90
+ id,
91
+ ...input,
92
+ updatedAt: new Date(),
93
+ })),
94
+ addPart: vi.fn().mockImplementation(async (input) => ({
95
+ ...input,
96
+ uploadedAt: new Date(),
97
+ })),
98
+ getPart: vi.fn(),
99
+ listParts: vi.fn().mockResolvedValue([]),
100
+ };
101
+ }
102
+ function createMockBlobStore() {
103
+ return {
104
+ createBlob: vi.fn().mockImplementation(async (input) => ({
105
+ ...input,
106
+ status: BlobStatus.Active,
107
+ createdAt: new Date(),
108
+ updatedAt: new Date(),
109
+ })),
110
+ getBlob: vi.fn(),
111
+ updateBlob: vi.fn().mockImplementation(async (id, input) => ({
112
+ id,
113
+ ...input,
114
+ updatedAt: new Date(),
115
+ })),
116
+ findBlobBySha256: vi.fn(),
117
+ findBlobByLocator: vi.fn(),
118
+ createReference: vi.fn().mockImplementation(async (input) => ({
119
+ ...input,
120
+ createdAt: new Date(),
121
+ })),
122
+ listReferences: vi.fn().mockResolvedValue([]),
123
+ deleteReference: vi.fn(),
124
+ };
125
+ }
126
+ function createMockFileStore() {
127
+ return {
128
+ createFile: vi.fn().mockImplementation(async (input) => ({
129
+ ...input,
130
+ status: FileStatus.Active,
131
+ createdAt: new Date(),
132
+ updatedAt: new Date(),
133
+ })),
134
+ getFile: vi.fn(),
135
+ updateFile: vi.fn().mockImplementation(async (id, input) => ({
136
+ id,
137
+ ...input,
138
+ updatedAt: new Date(),
139
+ })),
140
+ createVersion: vi.fn().mockImplementation(async (input) => ({
141
+ ...input,
142
+ createdAt: new Date(),
143
+ })),
144
+ getVersion: vi.fn(),
145
+ listVersions: vi.fn().mockResolvedValue([]),
146
+ getLatestVersion: vi.fn(),
147
+ };
148
+ }
149
+ function createMockMetadata() {
150
+ return {
151
+ uploads: createMockUploadSessionStore(),
152
+ blobs: createMockBlobStore(),
153
+ files: createMockFileStore(),
154
+ };
155
+ }
156
+ function createService(storageOverrides, metadataOverrides) {
157
+ const storage = createMockStorage(storageOverrides);
158
+ const metadata = createMockMetadata();
159
+ if (metadataOverrides) {
160
+ Object.assign(metadata, metadataOverrides);
161
+ }
162
+ const service = new DefaultStorageService({
163
+ storage,
164
+ metadata,
165
+ bucket: "test-bucket",
166
+ });
167
+ return { service, storage, metadata };
168
+ }
169
+ // ── Tests ──
170
+ describe("DefaultStorageService", () => {
171
+ describe("createUploadSession", () => {
172
+ it("should create a multipart upload session by default", async () => {
173
+ const { service, storage, metadata } = createService();
174
+ const result = await service.createUploadSession({
175
+ fileName: "test.txt",
176
+ mimeType: "text/plain",
177
+ });
178
+ expect(result.session).toBeDefined();
179
+ expect(storage.initUploadSession).toHaveBeenCalledWith(expect.objectContaining({
180
+ bucket: "test-bucket",
181
+ contentType: "text/plain",
182
+ }));
183
+ expect(metadata.uploads.createSession).toHaveBeenCalledWith(expect.objectContaining({
184
+ provider: StorageProvider.S3,
185
+ bucket: "test-bucket",
186
+ mode: UploadMode.Multipart,
187
+ fileName: "test.txt",
188
+ mimeType: "text/plain",
189
+ providerUploadId: "provider-upload-1",
190
+ }));
191
+ });
192
+ it("should create a single mode session and generate put URL", async () => {
193
+ const { service, storage, metadata } = createService();
194
+ const result = await service.createUploadSession({
195
+ fileName: "photo.jpg",
196
+ mode: UploadMode.Single,
197
+ });
198
+ expect(storage.initUploadSession).not.toHaveBeenCalled();
199
+ expect(storage.createPutUrl).toHaveBeenCalled();
200
+ expect(result.uploadUrl).toBeDefined();
201
+ expect(result.uploadUrl.url).toBe("https://example.com/put");
202
+ });
203
+ it("should not generate put URL in single mode if not supported", async () => {
204
+ const { service, storage } = createService({
205
+ createPutUrl: undefined,
206
+ });
207
+ const result = await service.createUploadSession({
208
+ mode: UploadMode.Single,
209
+ });
210
+ expect(result.uploadUrl).toBeUndefined();
211
+ });
212
+ it("should pass ownerId and createdBy to session", async () => {
213
+ const { service, metadata } = createService();
214
+ await service.createUploadSession({
215
+ ownerId: "owner-1",
216
+ createdBy: "user-1",
217
+ });
218
+ expect(metadata.uploads.createSession).toHaveBeenCalledWith(expect.objectContaining({
219
+ ownerId: "owner-1",
220
+ createdBy: "user-1",
221
+ }));
222
+ });
223
+ it("should set expiresAt based on expiresInSeconds", async () => {
224
+ const { service, metadata } = createService();
225
+ const before = Date.now();
226
+ await service.createUploadSession({
227
+ expiresInSeconds: 7200,
228
+ });
229
+ const call = metadata.uploads.createSession.mock.calls[0][0];
230
+ const expiresAt = call.expiresAt;
231
+ expect(expiresAt.getTime()).toBeGreaterThanOrEqual(before + 7200_000 - 100);
232
+ expect(expiresAt.getTime()).toBeLessThanOrEqual(Date.now() + 7200_000 + 100);
233
+ });
234
+ it("should use default expiry of 3600 seconds", async () => {
235
+ const { service, metadata } = createService();
236
+ const before = Date.now();
237
+ await service.createUploadSession({});
238
+ const call = metadata.uploads.createSession.mock.calls[0][0];
239
+ const expiresAt = call.expiresAt;
240
+ expect(expiresAt.getTime()).toBeGreaterThanOrEqual(before + 3600_000 - 100);
241
+ });
242
+ it("should generate object key using policy", async () => {
243
+ const { service, metadata } = createService();
244
+ await service.createUploadSession({
245
+ fileName: "report.pdf",
246
+ ownerId: "user-1",
247
+ });
248
+ const call = metadata.uploads.createSession.mock.calls[0][0];
249
+ expect(call.objectKey).toBeDefined();
250
+ expect(typeof call.objectKey).toBe("string");
251
+ // Key should contain an extension from fileName
252
+ expect(call.objectKey).toMatch(/\.pdf$/);
253
+ });
254
+ });
255
+ describe("getUploadPartUrl", () => {
256
+ it("should return a signed URL for the part", async () => {
257
+ const { service, storage, metadata } = createService();
258
+ metadata.uploads.getSession.mockResolvedValue({
259
+ id: "session-1",
260
+ provider: StorageProvider.S3,
261
+ bucket: "test-bucket",
262
+ objectKey: "path/to/file.txt",
263
+ providerUploadId: "upload-abc",
264
+ mode: UploadMode.Multipart,
265
+ status: UploadSessionStatus.InProgress,
266
+ });
267
+ const result = await service.getUploadPartUrl({
268
+ sessionId: "session-1",
269
+ partNumber: 3,
270
+ });
271
+ expect(result.url).toBe("https://example.com/part");
272
+ expect(storage.createUploadPartUrl).toHaveBeenCalledWith(expect.objectContaining({
273
+ bucket: "test-bucket",
274
+ objectKey: "path/to/file.txt",
275
+ providerUploadId: "upload-abc",
276
+ partNumber: 3,
277
+ }));
278
+ });
279
+ it("should throw MetadataNotFoundError when session not found", async () => {
280
+ const { service, metadata } = createService();
281
+ metadata.uploads.getSession.mockResolvedValue(undefined);
282
+ await expect(service.getUploadPartUrl({ sessionId: "not-exist", partNumber: 1 })).rejects.toThrow(MetadataNotFoundError);
283
+ });
284
+ it("should throw CapabilityNotSupportedError when part URL not supported", async () => {
285
+ const { service, metadata } = createService({
286
+ createUploadPartUrl: undefined,
287
+ });
288
+ metadata.uploads.getSession.mockResolvedValue({
289
+ id: "session-1",
290
+ providerUploadId: "upload-abc",
291
+ mode: UploadMode.Multipart,
292
+ status: UploadSessionStatus.InProgress,
293
+ });
294
+ await expect(service.getUploadPartUrl({ sessionId: "session-1", partNumber: 1 })).rejects.toThrow(CapabilityNotSupportedError);
295
+ });
296
+ it("should throw when session has no providerUploadId", async () => {
297
+ const { service, metadata } = createService();
298
+ metadata.uploads.getSession.mockResolvedValue({
299
+ id: "session-1",
300
+ providerUploadId: undefined,
301
+ mode: UploadMode.Multipart,
302
+ status: UploadSessionStatus.InProgress,
303
+ });
304
+ await expect(service.getUploadPartUrl({ sessionId: "session-1", partNumber: 1 })).rejects.toThrow(MetadataNotFoundError);
305
+ });
306
+ });
307
+ describe("uploadPart", () => {
308
+ it("should upload a part via storage and record in metadata", async () => {
309
+ const { service, storage, metadata } = createService();
310
+ metadata.uploads.getSession.mockResolvedValue({
311
+ id: "session-1",
312
+ provider: StorageProvider.S3,
313
+ bucket: "test-bucket",
314
+ objectKey: "obj-key",
315
+ providerUploadId: "upload-1",
316
+ mode: UploadMode.Multipart,
317
+ status: UploadSessionStatus.InProgress,
318
+ });
319
+ const body = new Uint8Array([1, 2, 3]);
320
+ const result = await service.uploadPart({
321
+ sessionId: "session-1",
322
+ partNumber: 1,
323
+ body,
324
+ });
325
+ expect(storage.uploadPart).toHaveBeenCalledWith(expect.objectContaining({
326
+ bucket: "test-bucket",
327
+ objectKey: "obj-key",
328
+ providerUploadId: "upload-1",
329
+ partNumber: 1,
330
+ body,
331
+ }));
332
+ expect(metadata.uploads.addPart).toHaveBeenCalledWith(expect.objectContaining({
333
+ sessionId: "session-1",
334
+ partNumber: 1,
335
+ etag: '"part-etag"',
336
+ }));
337
+ expect(result).toBeDefined();
338
+ });
339
+ it("should update session to in-progress if pending", async () => {
340
+ const { service, metadata } = createService();
341
+ metadata.uploads.getSession.mockResolvedValue({
342
+ id: "session-1",
343
+ providerUploadId: "upload-1",
344
+ bucket: "test-bucket",
345
+ objectKey: "key",
346
+ mode: UploadMode.Multipart,
347
+ status: UploadSessionStatus.Pending,
348
+ });
349
+ await service.uploadPart({
350
+ sessionId: "session-1",
351
+ partNumber: 1,
352
+ body: new Uint8Array(),
353
+ });
354
+ expect(metadata.uploads.updateSession).toHaveBeenCalledWith("session-1", expect.objectContaining({
355
+ status: UploadSessionStatus.InProgress,
356
+ }));
357
+ });
358
+ it("should not update status if already in-progress", async () => {
359
+ const { service, metadata } = createService();
360
+ metadata.uploads.getSession.mockResolvedValue({
361
+ id: "session-1",
362
+ providerUploadId: "upload-1",
363
+ bucket: "test-bucket",
364
+ objectKey: "key",
365
+ mode: UploadMode.Multipart,
366
+ status: UploadSessionStatus.InProgress,
367
+ });
368
+ await service.uploadPart({
369
+ sessionId: "session-1",
370
+ partNumber: 2,
371
+ body: new Uint8Array(),
372
+ });
373
+ expect(metadata.uploads.updateSession).not.toHaveBeenCalled();
374
+ });
375
+ it("should throw when session not found", async () => {
376
+ const { service, metadata } = createService();
377
+ metadata.uploads.getSession.mockResolvedValue(undefined);
378
+ await expect(service.uploadPart({
379
+ sessionId: "missing",
380
+ partNumber: 1,
381
+ body: new Uint8Array(),
382
+ })).rejects.toThrow(MetadataNotFoundError);
383
+ });
384
+ });
385
+ describe("registerPart", () => {
386
+ it("should record a part in metadata without uploading", async () => {
387
+ const { service, storage, metadata } = createService();
388
+ metadata.uploads.getSession.mockResolvedValue({
389
+ id: "session-1",
390
+ providerUploadId: "upload-1",
391
+ bucket: "test-bucket",
392
+ objectKey: "key",
393
+ mode: UploadMode.Multipart,
394
+ status: UploadSessionStatus.InProgress,
395
+ });
396
+ const result = await service.registerPart({
397
+ sessionId: "session-1",
398
+ partNumber: 1,
399
+ size: 1024,
400
+ etag: '"client-etag"',
401
+ });
402
+ expect(storage.uploadPart).not.toHaveBeenCalled();
403
+ expect(metadata.uploads.addPart).toHaveBeenCalledWith(expect.objectContaining({
404
+ sessionId: "session-1",
405
+ partNumber: 1,
406
+ size: 1024,
407
+ etag: '"client-etag"',
408
+ }));
409
+ expect(result).toBeDefined();
410
+ });
411
+ it("should transition pending session to in-progress", async () => {
412
+ const { service, metadata } = createService();
413
+ metadata.uploads.getSession.mockResolvedValue({
414
+ id: "session-1",
415
+ providerUploadId: "upload-1",
416
+ bucket: "test-bucket",
417
+ objectKey: "key",
418
+ mode: UploadMode.Multipart,
419
+ status: UploadSessionStatus.Pending,
420
+ });
421
+ await service.registerPart({
422
+ sessionId: "session-1",
423
+ partNumber: 1,
424
+ size: 512,
425
+ });
426
+ expect(metadata.uploads.updateSession).toHaveBeenCalledWith("session-1", expect.objectContaining({ status: UploadSessionStatus.InProgress }));
427
+ });
428
+ });
429
+ describe("completeUploadSession", () => {
430
+ it("should complete a multipart upload and create blob", async () => {
431
+ const { service, storage, metadata } = createService();
432
+ const parts = [
433
+ { id: "p1", sessionId: "session-1", partNumber: 1, size: 512, etag: '"e1"', uploadedAt: new Date() },
434
+ { id: "p2", sessionId: "session-1", partNumber: 2, size: 512, etag: '"e2"', uploadedAt: new Date() },
435
+ ];
436
+ metadata.uploads.getSession.mockResolvedValue({
437
+ id: "session-1",
438
+ provider: StorageProvider.S3,
439
+ bucket: "test-bucket",
440
+ objectKey: "key.txt",
441
+ providerUploadId: "upload-1",
442
+ mode: UploadMode.Multipart,
443
+ status: UploadSessionStatus.InProgress,
444
+ mimeType: "text/plain",
445
+ });
446
+ metadata.uploads.listParts.mockResolvedValue(parts);
447
+ const result = await service.completeUploadSession({
448
+ sessionId: "session-1",
449
+ });
450
+ expect(storage.completeUploadSession).toHaveBeenCalledWith(expect.objectContaining({
451
+ bucket: "test-bucket",
452
+ objectKey: "key.txt",
453
+ providerUploadId: "upload-1",
454
+ parts: [
455
+ { partNumber: 1, etag: '"e1"' },
456
+ { partNumber: 2, etag: '"e2"' },
457
+ ],
458
+ }));
459
+ expect(storage.headObject).toHaveBeenCalledWith(expect.objectContaining({
460
+ bucket: "test-bucket",
461
+ objectKey: "key.txt",
462
+ }));
463
+ expect(metadata.uploads.updateSession).toHaveBeenCalledWith("session-1", expect.objectContaining({
464
+ status: UploadSessionStatus.Completed,
465
+ }));
466
+ expect(metadata.blobs.createBlob).toHaveBeenCalledWith(expect.objectContaining({
467
+ provider: StorageProvider.S3,
468
+ bucket: "test-bucket",
469
+ objectKey: "key.txt",
470
+ }));
471
+ expect(result.blob).toBeDefined();
472
+ expect(result.file).toBeUndefined();
473
+ expect(result.fileVersion).toBeUndefined();
474
+ });
475
+ it("should create file and version when createFile is provided", async () => {
476
+ const { service, metadata } = createService();
477
+ metadata.uploads.getSession.mockResolvedValue({
478
+ id: "session-1",
479
+ provider: StorageProvider.S3,
480
+ bucket: "test-bucket",
481
+ objectKey: "key.txt",
482
+ providerUploadId: "upload-1",
483
+ mode: UploadMode.Multipart,
484
+ status: UploadSessionStatus.InProgress,
485
+ ownerId: "owner-1",
486
+ createdBy: "user-1",
487
+ mimeType: "text/plain",
488
+ });
489
+ metadata.uploads.listParts.mockResolvedValue([]);
490
+ const result = await service.completeUploadSession({
491
+ sessionId: "session-1",
492
+ createFile: {
493
+ displayName: "My Document.txt",
494
+ },
495
+ });
496
+ expect(metadata.files.createVersion).toHaveBeenCalledWith(expect.objectContaining({
497
+ version: 1,
498
+ createdBy: "user-1",
499
+ }));
500
+ expect(metadata.files.createFile).toHaveBeenCalledWith(expect.objectContaining({
501
+ displayName: "My Document.txt",
502
+ ownerId: "owner-1",
503
+ }));
504
+ expect(metadata.files.updateFile).toHaveBeenCalled();
505
+ expect(metadata.blobs.createReference).toHaveBeenCalledWith(expect.objectContaining({
506
+ refType: "file-version",
507
+ }));
508
+ expect(result.file).toBeDefined();
509
+ expect(result.fileVersion).toBeDefined();
510
+ });
511
+ it("should use createFile.ownerId over session.ownerId", async () => {
512
+ const { service, metadata } = createService();
513
+ metadata.uploads.getSession.mockResolvedValue({
514
+ id: "session-1",
515
+ provider: StorageProvider.S3,
516
+ bucket: "test-bucket",
517
+ objectKey: "key.txt",
518
+ providerUploadId: "upload-1",
519
+ mode: UploadMode.Multipart,
520
+ status: UploadSessionStatus.InProgress,
521
+ ownerId: "session-owner",
522
+ });
523
+ metadata.uploads.listParts.mockResolvedValue([]);
524
+ await service.completeUploadSession({
525
+ sessionId: "session-1",
526
+ createFile: {
527
+ displayName: "test.txt",
528
+ ownerId: "file-owner",
529
+ },
530
+ });
531
+ expect(metadata.files.createFile).toHaveBeenCalledWith(expect.objectContaining({
532
+ ownerId: "file-owner",
533
+ }));
534
+ });
535
+ it("should handle single mode (no multipart completion)", async () => {
536
+ const { service, storage, metadata } = createService();
537
+ metadata.uploads.getSession.mockResolvedValue({
538
+ id: "session-1",
539
+ provider: StorageProvider.S3,
540
+ bucket: "test-bucket",
541
+ objectKey: "key.txt",
542
+ providerUploadId: undefined,
543
+ mode: UploadMode.Single,
544
+ status: UploadSessionStatus.Pending,
545
+ });
546
+ const result = await service.completeUploadSession({
547
+ sessionId: "session-1",
548
+ });
549
+ expect(storage.completeUploadSession).not.toHaveBeenCalled();
550
+ expect(storage.headObject).toHaveBeenCalled();
551
+ expect(result.blob).toBeDefined();
552
+ });
553
+ });
554
+ describe("abortUploadSession", () => {
555
+ it("should abort provider session and update metadata", async () => {
556
+ const { service, storage, metadata } = createService();
557
+ metadata.uploads.getSession.mockResolvedValue({
558
+ id: "session-1",
559
+ bucket: "test-bucket",
560
+ objectKey: "key.txt",
561
+ providerUploadId: "upload-1",
562
+ mode: UploadMode.Multipart,
563
+ status: UploadSessionStatus.InProgress,
564
+ });
565
+ await service.abortUploadSession("session-1");
566
+ expect(storage.abortUploadSession).toHaveBeenCalledWith(expect.objectContaining({
567
+ bucket: "test-bucket",
568
+ objectKey: "key.txt",
569
+ providerUploadId: "upload-1",
570
+ }));
571
+ expect(metadata.uploads.updateSession).toHaveBeenCalledWith("session-1", expect.objectContaining({
572
+ status: UploadSessionStatus.Aborted,
573
+ }));
574
+ });
575
+ it("should skip provider abort when no providerUploadId", async () => {
576
+ const { service, storage, metadata } = createService();
577
+ metadata.uploads.getSession.mockResolvedValue({
578
+ id: "session-1",
579
+ bucket: "test-bucket",
580
+ objectKey: "key.txt",
581
+ providerUploadId: undefined,
582
+ mode: UploadMode.Single,
583
+ status: UploadSessionStatus.Pending,
584
+ });
585
+ await service.abortUploadSession("session-1");
586
+ expect(storage.abortUploadSession).not.toHaveBeenCalled();
587
+ expect(metadata.uploads.updateSession).toHaveBeenCalledWith("session-1", expect.objectContaining({
588
+ status: UploadSessionStatus.Aborted,
589
+ }));
590
+ });
591
+ it("should throw when session not found", async () => {
592
+ const { service, metadata } = createService();
593
+ metadata.uploads.getSession.mockResolvedValue(undefined);
594
+ await expect(service.abortUploadSession("missing")).rejects.toThrow(MetadataNotFoundError);
595
+ });
596
+ });
597
+ describe("getReadUrl", () => {
598
+ it("should resolve file → version → blob → signed URL", async () => {
599
+ const { service, storage, metadata } = createService();
600
+ metadata.files.getFile.mockResolvedValue({
601
+ id: "file-1",
602
+ displayName: "doc.pdf",
603
+ currentVersionId: "v-1",
604
+ mimeType: "application/pdf",
605
+ status: FileStatus.Active,
606
+ });
607
+ metadata.files.getVersion.mockResolvedValue({
608
+ id: "v-1",
609
+ fileId: "file-1",
610
+ blobId: "blob-1",
611
+ version: 1,
612
+ size: 2048,
613
+ });
614
+ metadata.blobs.getBlob.mockResolvedValue({
615
+ id: "blob-1",
616
+ provider: StorageProvider.S3,
617
+ bucket: "test-bucket",
618
+ objectKey: "path/to/blob",
619
+ size: 2048,
620
+ status: BlobStatus.Active,
621
+ });
622
+ const result = await service.getReadUrl({ fileId: "file-1" });
623
+ expect(result.url).toBe("https://example.com/read");
624
+ expect(storage.createReadUrl).toHaveBeenCalledWith(expect.objectContaining({
625
+ bucket: "test-bucket",
626
+ objectKey: "path/to/blob",
627
+ responseContentType: "application/pdf",
628
+ }));
629
+ });
630
+ it("should use specific versionId when provided", async () => {
631
+ const { service, metadata } = createService();
632
+ metadata.files.getFile.mockResolvedValue({
633
+ id: "file-1",
634
+ displayName: "doc.pdf",
635
+ currentVersionId: "v-2",
636
+ status: FileStatus.Active,
637
+ });
638
+ metadata.files.getVersion.mockResolvedValue({
639
+ id: "v-1",
640
+ fileId: "file-1",
641
+ blobId: "blob-old",
642
+ version: 1,
643
+ size: 1024,
644
+ });
645
+ metadata.blobs.getBlob.mockResolvedValue({
646
+ id: "blob-old",
647
+ bucket: "test-bucket",
648
+ objectKey: "old-path",
649
+ size: 1024,
650
+ status: BlobStatus.Active,
651
+ });
652
+ await service.getReadUrl({ fileId: "file-1", versionId: "v-1" });
653
+ expect(metadata.files.getVersion).toHaveBeenCalledWith("v-1");
654
+ });
655
+ it("should throw when file not found", async () => {
656
+ const { service, metadata } = createService();
657
+ metadata.files.getFile.mockResolvedValue(undefined);
658
+ await expect(service.getReadUrl({ fileId: "not-found" })).rejects.toThrow(MetadataNotFoundError);
659
+ });
660
+ it("should throw when file has no currentVersionId", async () => {
661
+ const { service, metadata } = createService();
662
+ metadata.files.getFile.mockResolvedValue({
663
+ id: "file-1",
664
+ displayName: "test",
665
+ currentVersionId: undefined,
666
+ status: FileStatus.Active,
667
+ });
668
+ await expect(service.getReadUrl({ fileId: "file-1" })).rejects.toThrow(MetadataNotFoundError);
669
+ });
670
+ it("should throw when version not found", async () => {
671
+ const { service, metadata } = createService();
672
+ metadata.files.getFile.mockResolvedValue({
673
+ id: "file-1",
674
+ currentVersionId: "v-missing",
675
+ status: FileStatus.Active,
676
+ });
677
+ metadata.files.getVersion.mockResolvedValue(undefined);
678
+ await expect(service.getReadUrl({ fileId: "file-1" })).rejects.toThrow(MetadataNotFoundError);
679
+ });
680
+ it("should throw when blob not found", async () => {
681
+ const { service, metadata } = createService();
682
+ metadata.files.getFile.mockResolvedValue({
683
+ id: "file-1",
684
+ currentVersionId: "v-1",
685
+ status: FileStatus.Active,
686
+ });
687
+ metadata.files.getVersion.mockResolvedValue({
688
+ id: "v-1",
689
+ blobId: "blob-gone",
690
+ version: 1,
691
+ size: 10,
692
+ });
693
+ metadata.blobs.getBlob.mockResolvedValue(undefined);
694
+ await expect(service.getReadUrl({ fileId: "file-1" })).rejects.toThrow(MetadataNotFoundError);
695
+ });
696
+ it("should throw CapabilityNotSupportedError when signedReadUrl not supported", async () => {
697
+ const { service } = createService({ createReadUrl: undefined });
698
+ await expect(service.getReadUrl({ fileId: "file-1" })).rejects.toThrow(CapabilityNotSupportedError);
699
+ });
700
+ });
701
+ describe("getFile", () => {
702
+ it("should delegate to metadata.files.getFile", async () => {
703
+ const { service, metadata } = createService();
704
+ const mockFile = {
705
+ id: "file-1",
706
+ displayName: "test.txt",
707
+ status: FileStatus.Active,
708
+ createdAt: new Date(),
709
+ updatedAt: new Date(),
710
+ };
711
+ metadata.files.getFile.mockResolvedValue(mockFile);
712
+ const result = await service.getFile("file-1");
713
+ expect(result).toBe(mockFile);
714
+ expect(metadata.files.getFile).toHaveBeenCalledWith("file-1");
715
+ });
716
+ it("should return undefined for non-existent file", async () => {
717
+ const { service, metadata } = createService();
718
+ metadata.files.getFile.mockResolvedValue(undefined);
719
+ const result = await service.getFile("not-found");
720
+ expect(result).toBeUndefined();
721
+ });
722
+ });
723
+ describe("getBlob", () => {
724
+ it("should delegate to metadata.blobs.getBlob", async () => {
725
+ const { service, metadata } = createService();
726
+ const mockBlob = {
727
+ id: "blob-1",
728
+ provider: StorageProvider.S3,
729
+ bucket: "bucket",
730
+ objectKey: "key",
731
+ size: 100,
732
+ status: BlobStatus.Active,
733
+ };
734
+ metadata.blobs.getBlob.mockResolvedValue(mockBlob);
735
+ const result = await service.getBlob("blob-1");
736
+ expect(result).toBe(mockBlob);
737
+ });
738
+ });
739
+ describe("deleteFile", () => {
740
+ it("should soft-delete file and orphan unreferenced blobs", async () => {
741
+ const { service, metadata } = createService();
742
+ metadata.files.getFile.mockResolvedValue({
743
+ id: "file-1",
744
+ displayName: "test.txt",
745
+ status: FileStatus.Active,
746
+ });
747
+ const version = {
748
+ id: "v-1",
749
+ fileId: "file-1",
750
+ blobId: "blob-1",
751
+ version: 1,
752
+ size: 1024,
753
+ };
754
+ metadata.files.listVersions.mockResolvedValue([version]);
755
+ const ref = {
756
+ id: "ref-1",
757
+ blobId: "blob-1",
758
+ refType: "file-version",
759
+ refId: "v-1",
760
+ createdAt: new Date(),
761
+ };
762
+ metadata.blobs.listReferences
763
+ .mockResolvedValueOnce([ref]) // First call: find refs for version
764
+ .mockResolvedValueOnce([]); // Second call: check remaining refs
765
+ await service.deleteFile({ fileId: "file-1" });
766
+ // Should soft-delete the file
767
+ expect(metadata.files.updateFile).toHaveBeenCalledWith("file-1", expect.objectContaining({
768
+ status: FileStatus.Deleted,
769
+ }));
770
+ // Should delete the blob reference
771
+ expect(metadata.blobs.deleteReference).toHaveBeenCalledWith("ref-1");
772
+ // Should mark blob as orphaned since no remaining references
773
+ expect(metadata.blobs.updateBlob).toHaveBeenCalledWith("blob-1", expect.objectContaining({
774
+ status: BlobStatus.Orphaned,
775
+ }));
776
+ });
777
+ it("should not orphan blob if other references remain", async () => {
778
+ const { service, metadata } = createService();
779
+ metadata.files.getFile.mockResolvedValue({
780
+ id: "file-1",
781
+ displayName: "test.txt",
782
+ status: FileStatus.Active,
783
+ });
784
+ const version = {
785
+ id: "v-1",
786
+ fileId: "file-1",
787
+ blobId: "blob-shared",
788
+ version: 1,
789
+ size: 1024,
790
+ };
791
+ metadata.files.listVersions.mockResolvedValue([version]);
792
+ const ownRef = {
793
+ id: "ref-1",
794
+ blobId: "blob-shared",
795
+ refType: "file-version",
796
+ refId: "v-1",
797
+ createdAt: new Date(),
798
+ };
799
+ const otherRef = {
800
+ id: "ref-2",
801
+ blobId: "blob-shared",
802
+ refType: "file-version",
803
+ refId: "v-other",
804
+ createdAt: new Date(),
805
+ };
806
+ metadata.blobs.listReferences
807
+ .mockResolvedValueOnce([ownRef, otherRef]) // First call
808
+ .mockResolvedValueOnce([otherRef]); // After deletion, other ref remains
809
+ await service.deleteFile({ fileId: "file-1" });
810
+ expect(metadata.blobs.deleteReference).toHaveBeenCalledWith("ref-1");
811
+ expect(metadata.blobs.updateBlob).not.toHaveBeenCalled();
812
+ });
813
+ it("should throw when file not found", async () => {
814
+ const { service, metadata } = createService();
815
+ metadata.files.getFile.mockResolvedValue(undefined);
816
+ await expect(service.deleteFile({ fileId: "missing" })).rejects.toThrow(MetadataNotFoundError);
817
+ });
818
+ it("should handle file with no versions", async () => {
819
+ const { service, metadata } = createService();
820
+ metadata.files.getFile.mockResolvedValue({
821
+ id: "file-1",
822
+ displayName: "empty.txt",
823
+ status: FileStatus.Active,
824
+ });
825
+ metadata.files.listVersions.mockResolvedValue([]);
826
+ await service.deleteFile({ fileId: "file-1" });
827
+ expect(metadata.files.updateFile).toHaveBeenCalledWith("file-1", expect.objectContaining({ status: FileStatus.Deleted }));
828
+ expect(metadata.blobs.deleteReference).not.toHaveBeenCalled();
829
+ });
830
+ });
831
+ describe("constructor options", () => {
832
+ it("should use custom objectKeyPolicy", async () => {
833
+ const customPolicy = {
834
+ generate: vi.fn().mockReturnValue("custom/key/path"),
835
+ };
836
+ const storage = createMockStorage();
837
+ const metadata = createMockMetadata();
838
+ const service = new DefaultStorageService({
839
+ storage,
840
+ metadata,
841
+ bucket: "test-bucket",
842
+ objectKeyPolicy: customPolicy,
843
+ });
844
+ await service.createUploadSession({ fileName: "test.txt" });
845
+ expect(customPolicy.generate).toHaveBeenCalled();
846
+ expect(metadata.uploads.createSession).toHaveBeenCalledWith(expect.objectContaining({
847
+ objectKey: "custom/key/path",
848
+ }));
849
+ });
850
+ it("should pass objectKeyPrefix to policy", async () => {
851
+ const customPolicy = {
852
+ generate: vi.fn().mockReturnValue("prefix/obj"),
853
+ };
854
+ const storage = createMockStorage();
855
+ const metadata = createMockMetadata();
856
+ const service = new DefaultStorageService({
857
+ storage,
858
+ metadata,
859
+ bucket: "test-bucket",
860
+ objectKeyPolicy: customPolicy,
861
+ objectKeyPrefix: "my-prefix",
862
+ });
863
+ await service.createUploadSession({});
864
+ expect(customPolicy.generate).toHaveBeenCalledWith(expect.objectContaining({
865
+ prefix: "my-prefix",
866
+ }));
867
+ });
868
+ it("should use custom read URL expiry", async () => {
869
+ const storage = createMockStorage();
870
+ const metadata = createMockMetadata();
871
+ const service = new DefaultStorageService({
872
+ storage,
873
+ metadata,
874
+ bucket: "test-bucket",
875
+ defaultReadUrlExpiresInSeconds: 600,
876
+ });
877
+ metadata.files.getFile.mockResolvedValue({
878
+ id: "file-1",
879
+ currentVersionId: "v-1",
880
+ status: FileStatus.Active,
881
+ });
882
+ metadata.files.getVersion.mockResolvedValue({
883
+ id: "v-1",
884
+ blobId: "blob-1",
885
+ version: 1,
886
+ size: 100,
887
+ });
888
+ metadata.blobs.getBlob.mockResolvedValue({
889
+ id: "blob-1",
890
+ bucket: "test-bucket",
891
+ objectKey: "key",
892
+ size: 100,
893
+ status: BlobStatus.Active,
894
+ });
895
+ await service.getReadUrl({ fileId: "file-1" });
896
+ expect(storage.createReadUrl).toHaveBeenCalledWith(expect.objectContaining({
897
+ expiresInSeconds: 600,
898
+ }));
899
+ });
900
+ });
901
+ });
902
+ //# sourceMappingURL=default-storage-service.test.js.map