@onlineapps/conn-base-storage 1.0.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 (39) hide show
  1. package/API.md +618 -0
  2. package/README.md +341 -0
  3. package/SHARED_URL_ADDRESSING.md +258 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +213 -0
  7. package/coverage/coverage-final.json +3 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +131 -0
  10. package/coverage/index.js.html +1579 -0
  11. package/coverage/internal-url-adapter.js.html +604 -0
  12. package/coverage/lcov-report/base.css +224 -0
  13. package/coverage/lcov-report/block-navigation.js +87 -0
  14. package/coverage/lcov-report/favicon.png +0 -0
  15. package/coverage/lcov-report/index.html +131 -0
  16. package/coverage/lcov-report/index.js.html +1579 -0
  17. package/coverage/lcov-report/internal-url-adapter.js.html +604 -0
  18. package/coverage/lcov-report/prettify.css +1 -0
  19. package/coverage/lcov-report/prettify.js +2 -0
  20. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  21. package/coverage/lcov-report/sorter.js +210 -0
  22. package/coverage/lcov.info +434 -0
  23. package/coverage/prettify.css +1 -0
  24. package/coverage/prettify.js +2 -0
  25. package/coverage/sort-arrow-sprite.png +0 -0
  26. package/coverage/sorter.js +210 -0
  27. package/jest.config.js +13 -0
  28. package/jest.integration.config.js +9 -0
  29. package/package.json +33 -0
  30. package/src/index.js +853 -0
  31. package/src/internal-url-adapter.js +174 -0
  32. package/src/sharedUrlAdapter.js +258 -0
  33. package/test/component/storage.component.test.js +363 -0
  34. package/test/integration/setup.js +3 -0
  35. package/test/integration/storage.integration.test.js +224 -0
  36. package/test/unit/internal-url-adapter.test.js +211 -0
  37. package/test/unit/legacy.storage.test.js.bak +614 -0
  38. package/test/unit/storage.extended.unit.test.js +435 -0
  39. package/test/unit/storage.unit.test.js +373 -0
@@ -0,0 +1,614 @@
1
+ const StorageConnector = require('../src/index');
2
+ const crypto = require('crypto');
3
+
4
+ describe('StorageConnector', () => {
5
+ let storage;
6
+
7
+ beforeEach(() => {
8
+ storage = new StorageConnector({
9
+ endPoint: 'localhost',
10
+ port: 9000,
11
+ useSSL: false,
12
+ accessKey: 'minioadmin',
13
+ secretKey: 'minioadmin',
14
+ defaultBucket: 'test-bucket',
15
+ logLevel: 'error' // Quiet during tests
16
+ });
17
+ });
18
+
19
+ describe('Initialization', () => {
20
+ test('should create instance with default config', () => {
21
+ const defaultStorage = new StorageConnector();
22
+ expect(defaultStorage.defaultBucket).toBe('api-storage');
23
+ expect(defaultStorage.cacheEnabled).toBe(true);
24
+ expect(defaultStorage.maxCacheSize).toBe(100);
25
+ });
26
+
27
+ test('should create instance with custom config', () => {
28
+ const customStorage = new StorageConnector({
29
+ defaultBucket: 'custom-bucket',
30
+ cacheEnabled: false,
31
+ maxCacheSize: 50
32
+ });
33
+ expect(customStorage.defaultBucket).toBe('custom-bucket');
34
+ expect(customStorage.cacheEnabled).toBe(false);
35
+ expect(customStorage.maxCacheSize).toBe(50);
36
+ });
37
+ });
38
+
39
+ describe('Fingerprinting', () => {
40
+ test('should generate consistent fingerprint for same content', () => {
41
+ const content = { test: 'data', value: 123 };
42
+
43
+ const fingerprint1 = storage.generateFingerprint(content);
44
+ const fingerprint2 = storage.generateFingerprint(content);
45
+
46
+ expect(fingerprint1).toBe(fingerprint2);
47
+ expect(fingerprint1).toHaveLength(64); // SHA256 hex length
48
+ });
49
+
50
+ test('should generate different fingerprints for different content', () => {
51
+ const content1 = { test: 'data1' };
52
+ const content2 = { test: 'data2' };
53
+
54
+ const fingerprint1 = storage.generateFingerprint(content1);
55
+ const fingerprint2 = storage.generateFingerprint(content2);
56
+
57
+ expect(fingerprint1).not.toBe(fingerprint2);
58
+ });
59
+
60
+ test('should handle string content', () => {
61
+ const content = 'test string';
62
+ const fingerprint = storage.generateFingerprint(content);
63
+
64
+ expect(fingerprint).toBeDefined();
65
+ expect(fingerprint).toHaveLength(64);
66
+ });
67
+
68
+ test('should handle buffer content', () => {
69
+ const content = Buffer.from('test buffer');
70
+ const fingerprint = storage.generateFingerprint(content);
71
+
72
+ expect(fingerprint).toBeDefined();
73
+ expect(fingerprint).toHaveLength(64);
74
+ });
75
+
76
+ test('should handle JSON objects with consistent ordering', () => {
77
+ const obj1 = { b: 2, a: 1 };
78
+ const obj2 = { a: 1, b: 2 };
79
+
80
+ const fp1 = storage.generateFingerprint(obj1);
81
+ const fp2 = storage.generateFingerprint(obj2);
82
+
83
+ // JSON.stringify may produce different results for different key orders
84
+ // This test documents the behavior
85
+ expect(fp1).toBeDefined();
86
+ expect(fp2).toBeDefined();
87
+ });
88
+
89
+ test('should handle nested objects', () => {
90
+ const nested = {
91
+ level1: {
92
+ level2: {
93
+ data: 'test'
94
+ }
95
+ }
96
+ };
97
+
98
+ const fingerprint = storage.generateFingerprint(nested);
99
+ expect(fingerprint).toBeDefined();
100
+ expect(fingerprint).toHaveLength(64);
101
+ });
102
+
103
+ test('should handle arrays', () => {
104
+ const array = [1, 2, 3, { test: 'data' }];
105
+ const fingerprint = storage.generateFingerprint(array);
106
+ expect(fingerprint).toBeDefined();
107
+ expect(fingerprint).toHaveLength(64);
108
+ });
109
+ });
110
+
111
+ describe('Cache Management', () => {
112
+ test('should cache content when enabled', () => {
113
+ storage.cacheEnabled = true;
114
+ const key = 'test/path/abc123';
115
+ const value = 'test content';
116
+
117
+ storage.updateCache(key, value);
118
+
119
+ expect(storage.contentCache.has(key)).toBe(true);
120
+ expect(storage.contentCache.get(key)).toBe(value);
121
+ });
122
+
123
+ test('should not cache when disabled', () => {
124
+ storage.cacheEnabled = false;
125
+ const key = 'test/path/abc123';
126
+ const value = 'test content';
127
+
128
+ // updateCache should still work but won't be used when cacheEnabled is false
129
+ storage.updateCache(key, value);
130
+
131
+ // Cache is still updated but won't be checked when disabled
132
+ expect(storage.contentCache.has(key)).toBe(true);
133
+ });
134
+
135
+ test('should evict oldest entry when cache is full', () => {
136
+ storage.maxCacheSize = 2;
137
+
138
+ storage.updateCache('key1', 'value1');
139
+ storage.updateCache('key2', 'value2');
140
+ storage.updateCache('key3', 'value3'); // Should evict key1
141
+
142
+ expect(storage.contentCache.has('key1')).toBe(false);
143
+ expect(storage.contentCache.has('key2')).toBe(true);
144
+ expect(storage.contentCache.has('key3')).toBe(true);
145
+ });
146
+
147
+ test('should maintain insertion order (Map behavior)', () => {
148
+ storage.maxCacheSize = 3;
149
+
150
+ storage.updateCache('key1', 'value1');
151
+ storage.updateCache('key2', 'value2');
152
+ storage.updateCache('key3', 'value3');
153
+
154
+ // Map maintains insertion order, not LRU
155
+ // When adding key4, it will evict the first inserted (key1)
156
+ storage.updateCache('key4', 'value4');
157
+
158
+ expect(storage.contentCache.has('key1')).toBe(false); // First inserted, first evicted
159
+ expect(storage.contentCache.has('key2')).toBe(true);
160
+ expect(storage.contentCache.has('key3')).toBe(true);
161
+ expect(storage.contentCache.has('key4')).toBe(true);
162
+ });
163
+
164
+ test('should clear cache', () => {
165
+ storage.updateCache('key1', 'value1');
166
+ storage.updateCache('key2', 'value2');
167
+
168
+ storage.clearCache();
169
+
170
+ expect(storage.contentCache.size).toBe(0);
171
+ });
172
+
173
+ test('should handle large cache sizes', () => {
174
+ storage.maxCacheSize = 1000;
175
+
176
+ for (let i = 0; i < 1000; i++) {
177
+ storage.updateCache(`key${i}`, `value${i}`);
178
+ }
179
+
180
+ expect(storage.contentCache.size).toBe(1000);
181
+
182
+ // Add one more - should evict first
183
+ storage.updateCache('key1000', 'value1000');
184
+ expect(storage.contentCache.size).toBe(1000);
185
+ expect(storage.contentCache.has('key0')).toBe(false);
186
+ expect(storage.contentCache.has('key1000')).toBe(true);
187
+ });
188
+ });
189
+
190
+ describe('Internal URL Generation', () => {
191
+ test('should generate abstract internal URL', () => {
192
+ const url = storage.getInternalUrl('my-bucket', 'path/to/file.json');
193
+ expect(url).toBe('internal://storage/my-bucket/path/to/file.json');
194
+ });
195
+
196
+ test('should generate internal URL for different buckets', () => {
197
+ const url1 = storage.getInternalUrl('bucket1', 'file1.json');
198
+ const url2 = storage.getInternalUrl('bucket2', 'file2.json');
199
+
200
+ expect(url1).toBe('internal://storage/bucket1/file1.json');
201
+ expect(url2).toBe('internal://storage/bucket2/file2.json');
202
+ });
203
+
204
+ test('should handle complex paths', () => {
205
+ const url = storage.getInternalUrl('my-bucket', 'deep/nested/path/to/file.json');
206
+ expect(url).toBe('internal://storage/my-bucket/deep/nested/path/to/file.json');
207
+ });
208
+
209
+ test('should handle special characters in path', () => {
210
+ const url = storage.getInternalUrl('my-bucket', 'path/to/file with spaces.json');
211
+ expect(url).toBe('internal://storage/my-bucket/path/to/file with spaces.json');
212
+ });
213
+
214
+ test('getPublicUrl should return internal URL for backward compatibility', () => {
215
+ const url = storage.getPublicUrl('my-bucket', 'file.json');
216
+ expect(url).toBe('internal://storage/my-bucket/file.json');
217
+ });
218
+ });
219
+
220
+ describe('Error Handling', () => {
221
+ test('should handle invalid content types for fingerprinting', () => {
222
+ const invalidContent = undefined;
223
+
224
+ // Should convert to string
225
+ expect(() => storage.generateFingerprint(invalidContent)).not.toThrow();
226
+ });
227
+
228
+ test('should handle null content', () => {
229
+ const nullContent = null;
230
+
231
+ // Should convert to string
232
+ expect(() => storage.generateFingerprint(nullContent)).not.toThrow();
233
+ });
234
+
235
+ test('should handle circular references in objects', () => {
236
+ const obj = { a: 1 };
237
+ obj.circular = obj;
238
+
239
+ // Should handle circular reference gracefully
240
+ expect(() => storage.generateFingerprint(obj)).not.toThrow();
241
+
242
+ // Should produce consistent fingerprint for circular references
243
+ const fingerprint = storage.generateFingerprint(obj);
244
+ expect(fingerprint).toBeDefined();
245
+ expect(fingerprint).toHaveLength(64);
246
+ });
247
+ });
248
+ });
249
+
250
+ // Mock MinIO client for unit tests
251
+ describe('StorageConnector Mock Tests', () => {
252
+ let storage;
253
+ let mockClient;
254
+
255
+ beforeEach(() => {
256
+ storage = new StorageConnector({
257
+ defaultBucket: 'test-bucket',
258
+ logLevel: 'error'
259
+ });
260
+
261
+ // Mock MinIO client methods
262
+ mockClient = {
263
+ bucketExists: jest.fn(),
264
+ makeBucket: jest.fn(),
265
+ setBucketPolicy: jest.fn(),
266
+ putObject: jest.fn(),
267
+ getObject: jest.fn(),
268
+ statObject: jest.fn(),
269
+ removeObject: jest.fn(),
270
+ listObjectsV2: jest.fn(),
271
+ presignedGetObject: jest.fn()
272
+ };
273
+
274
+ storage.client = mockClient;
275
+ });
276
+
277
+ describe('initialize', () => {
278
+ test('should create bucket if it does not exist', async () => {
279
+ mockClient.bucketExists.mockResolvedValue(false);
280
+ mockClient.makeBucket.mockResolvedValue();
281
+ mockClient.setBucketPolicy.mockResolvedValue();
282
+
283
+ const result = await storage.initialize();
284
+
285
+ expect(result).toBe(true);
286
+ expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
287
+ expect(mockClient.makeBucket).toHaveBeenCalledWith('test-bucket', 'us-east-1');
288
+ expect(mockClient.setBucketPolicy).toHaveBeenCalled();
289
+ });
290
+
291
+ test('should not create bucket if it exists', async () => {
292
+ mockClient.bucketExists.mockResolvedValue(true);
293
+
294
+ const result = await storage.initialize();
295
+
296
+ expect(result).toBe(true);
297
+ expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
298
+ expect(mockClient.makeBucket).not.toHaveBeenCalled();
299
+ });
300
+
301
+ test('should handle initialization errors', async () => {
302
+ mockClient.bucketExists.mockRejectedValue(new Error('Connection failed'));
303
+
304
+ await expect(storage.initialize()).rejects.toThrow('Connection failed');
305
+ });
306
+ });
307
+
308
+ describe('uploadWithFingerprint', () => {
309
+ test('should upload new content with fingerprint', async () => {
310
+ const content = { test: 'data' };
311
+ const fingerprint = storage.generateFingerprint(content);
312
+
313
+ mockClient.statObject.mockRejectedValue(new Error('Not found'));
314
+ mockClient.putObject.mockResolvedValue();
315
+
316
+ const result = await storage.uploadWithFingerprint('test-bucket', content, 'test/path');
317
+
318
+ expect(result).toMatchObject({
319
+ fingerprint,
320
+ bucket: 'test-bucket',
321
+ path: `test/path/${fingerprint}.json`,
322
+ existed: false
323
+ });
324
+
325
+ expect(mockClient.putObject).toHaveBeenCalled();
326
+ });
327
+
328
+ test('should skip upload if content already exists', async () => {
329
+ const content = { test: 'data' };
330
+ const fingerprint = storage.generateFingerprint(content);
331
+
332
+ mockClient.statObject.mockResolvedValue({ size: 100 });
333
+
334
+ const result = await storage.uploadWithFingerprint('test-bucket', content, 'test/path');
335
+
336
+ expect(result).toMatchObject({
337
+ fingerprint,
338
+ bucket: 'test-bucket',
339
+ path: `test/path/${fingerprint}.json`,
340
+ existed: true
341
+ });
342
+
343
+ expect(mockClient.putObject).not.toHaveBeenCalled();
344
+ });
345
+
346
+ test('should handle upload without path prefix', async () => {
347
+ const content = 'simple string';
348
+ const fingerprint = storage.generateFingerprint(content);
349
+
350
+ mockClient.statObject.mockRejectedValue(new Error('Not found'));
351
+ mockClient.putObject.mockResolvedValue();
352
+
353
+ const result = await storage.uploadWithFingerprint('test-bucket', content);
354
+
355
+ expect(result.path).toBe(`${fingerprint}.json`);
356
+ });
357
+
358
+ test('should use default bucket if not specified', async () => {
359
+ const content = { test: 'data' };
360
+
361
+ mockClient.statObject.mockRejectedValue(new Error('Not found'));
362
+ mockClient.putObject.mockResolvedValue();
363
+
364
+ const result = await storage.uploadWithFingerprint(null, content);
365
+
366
+ expect(result.bucket).toBe('test-bucket');
367
+ });
368
+ });
369
+
370
+ describe('ensureBucket', () => {
371
+ test('should create bucket if not exists', async () => {
372
+ mockClient.bucketExists.mockResolvedValue(false);
373
+ mockClient.makeBucket.mockResolvedValue();
374
+
375
+ const result = await storage.ensureBucket('new-bucket');
376
+
377
+ expect(result).toBe(true);
378
+ expect(mockClient.makeBucket).toHaveBeenCalledWith('new-bucket', 'us-east-1');
379
+ });
380
+
381
+ test('should not create bucket if exists', async () => {
382
+ mockClient.bucketExists.mockResolvedValue(true);
383
+
384
+ const result = await storage.ensureBucket('existing-bucket');
385
+
386
+ expect(result).toBe(true);
387
+ expect(mockClient.makeBucket).not.toHaveBeenCalled();
388
+ });
389
+
390
+ test('should handle errors', async () => {
391
+ mockClient.bucketExists.mockRejectedValue(new Error('Network error'));
392
+
393
+ await expect(storage.ensureBucket('error-bucket')).rejects.toThrow('Network error');
394
+ });
395
+ });
396
+ });
397
+
398
+ // Integration tests (require actual MinIO)
399
+ // Helper to conditionally skip tests
400
+ const testIf = (condition) => condition ? test : test.skip;
401
+
402
+ // Check if MinIO is available
403
+ const checkMinIOConnection = async () => {
404
+ const net = require('net');
405
+ return new Promise((resolve) => {
406
+ const client = new net.Socket();
407
+ const timeout = setTimeout(() => {
408
+ client.destroy();
409
+ resolve(false);
410
+ }, 1000);
411
+
412
+ client.connect(9000, 'localhost', () => {
413
+ clearTimeout(timeout);
414
+ client.destroy();
415
+ resolve(true);
416
+ });
417
+
418
+ client.on('error', () => {
419
+ clearTimeout(timeout);
420
+ resolve(false);
421
+ });
422
+ });
423
+ };
424
+
425
+ describe('StorageConnector Integration', () => {
426
+ let storage;
427
+ let minioAvailable = false;
428
+
429
+ beforeAll(async () => {
430
+ minioAvailable = await checkMinIOConnection();
431
+
432
+ if (!minioAvailable) {
433
+ console.log('⚠️ MinIO is not available - integration tests will be skipped');
434
+ return;
435
+ }
436
+
437
+ storage = new StorageConnector({
438
+ endPoint: process.env.MINIO_ENDPOINT || 'localhost',
439
+ port: parseInt(process.env.MINIO_PORT || 9000),
440
+ useSSL: process.env.MINIO_USE_SSL === 'true',
441
+ accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
442
+ secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
443
+ defaultBucket: 'test-integration',
444
+ logLevel: 'error'
445
+ });
446
+
447
+ try {
448
+ await storage.initialize();
449
+ } catch (error) {
450
+ minioAvailable = false;
451
+ console.log('MinIO initialization failed - integration tests will be skipped');
452
+ }
453
+ });
454
+
455
+ testIf(minioAvailable)('should upload and download with fingerprint verification', async () => {
456
+ const content = {
457
+ service: 'test-service',
458
+ version: '1.0.0',
459
+ timestamp: new Date().toISOString()
460
+ };
461
+
462
+ // Upload
463
+ const uploadResult = await storage.uploadWithFingerprint(
464
+ 'test-integration',
465
+ content,
466
+ 'test/specs'
467
+ );
468
+
469
+ expect(uploadResult.fingerprint).toBeDefined();
470
+ expect(uploadResult.path).toContain(uploadResult.fingerprint);
471
+
472
+ // Download with verification
473
+ const downloaded = await storage.downloadWithVerification(
474
+ uploadResult.bucket,
475
+ uploadResult.path,
476
+ uploadResult.fingerprint
477
+ );
478
+
479
+ const parsed = JSON.parse(downloaded);
480
+ expect(parsed).toEqual(content);
481
+ });
482
+
483
+ testIf(minioAvailable)('should detect fingerprint mismatch', async () => {
484
+ const content = { test: 'data' };
485
+
486
+ const uploadResult = await storage.uploadWithFingerprint(
487
+ 'test-integration',
488
+ content,
489
+ 'test/verify'
490
+ );
491
+
492
+ // Try to download with wrong fingerprint
493
+ await expect(
494
+ storage.downloadWithVerification(
495
+ uploadResult.bucket,
496
+ uploadResult.path,
497
+ 'wrong-fingerprint'
498
+ )
499
+ ).rejects.toThrow('Fingerprint mismatch');
500
+ });
501
+
502
+ testIf(minioAvailable)('should handle duplicate uploads (idempotency)', async () => {
503
+ const content = { test: 'duplicate', timestamp: 123 };
504
+
505
+ // First upload
506
+ const result1 = await storage.uploadWithFingerprint(
507
+ 'test-integration',
508
+ content,
509
+ 'test/duplicate'
510
+ );
511
+
512
+ expect(result1.existed).toBe(false);
513
+
514
+ // Second upload with same content
515
+ const result2 = await storage.uploadWithFingerprint(
516
+ 'test-integration',
517
+ content,
518
+ 'test/duplicate'
519
+ );
520
+
521
+ expect(result2.existed).toBe(true);
522
+ expect(result2.fingerprint).toBe(result1.fingerprint);
523
+ expect(result2.path).toBe(result1.path);
524
+ });
525
+
526
+ testIf(minioAvailable)('should list objects with fingerprints', async () => {
527
+ // Upload test objects
528
+ await storage.uploadWithFingerprint(
529
+ 'test-integration',
530
+ { test: 'list1' },
531
+ 'test/list'
532
+ );
533
+
534
+ await storage.uploadWithFingerprint(
535
+ 'test-integration',
536
+ { test: 'list2' },
537
+ 'test/list'
538
+ );
539
+
540
+ // List objects
541
+ const objects = await storage.listWithFingerprints('test-integration', 'test/list/');
542
+
543
+ expect(objects).toBeInstanceOf(Array);
544
+ expect(objects.length).toBeGreaterThanOrEqual(2);
545
+
546
+ objects.forEach(obj => {
547
+ expect(obj).toHaveProperty('name');
548
+ expect(obj).toHaveProperty('fingerprint');
549
+ expect(obj).toHaveProperty('uploadedAt');
550
+ });
551
+ });
552
+
553
+ testIf(minioAvailable)('should generate presigned URL', async () => {
554
+ const content = { test: 'presigned' };
555
+
556
+ const uploadResult = await storage.uploadWithFingerprint(
557
+ 'test-integration',
558
+ content,
559
+ 'test/presigned'
560
+ );
561
+
562
+ const url = await storage.getPresignedUrl(
563
+ uploadResult.bucket,
564
+ uploadResult.path,
565
+ 300 // 5 minutes
566
+ );
567
+
568
+ expect(url).toContain('X-Amz-Algorithm');
569
+ expect(url).toContain('X-Amz-Expires');
570
+ });
571
+
572
+ testIf(minioAvailable)('should handle cache correctly', async () => {
573
+ storage.clearCache(); // Start fresh
574
+
575
+ const content = { test: 'cache-test' };
576
+ const uploadResult = await storage.uploadWithFingerprint(
577
+ 'test-integration',
578
+ content,
579
+ 'test/cache'
580
+ );
581
+
582
+ // First download - should not be cached
583
+ const download1 = await storage.downloadWithVerification(
584
+ uploadResult.bucket,
585
+ uploadResult.path,
586
+ uploadResult.fingerprint
587
+ );
588
+
589
+ // Cache should now contain the content
590
+ const cacheKey = `${uploadResult.bucket}/${uploadResult.path}/${uploadResult.fingerprint}`;
591
+ expect(storage.contentCache.has(cacheKey)).toBe(true);
592
+
593
+ // Second download - should use cache
594
+ const download2 = await storage.downloadWithVerification(
595
+ uploadResult.bucket,
596
+ uploadResult.path,
597
+ uploadResult.fingerprint
598
+ );
599
+
600
+ expect(download1).toBe(download2);
601
+ });
602
+
603
+ afterAll(async () => {
604
+ // Cleanup test bucket
605
+ try {
606
+ const objects = await storage.listWithFingerprints('test-integration', 'test/');
607
+ for (const obj of objects) {
608
+ await storage.deleteObject('test-integration', obj.name);
609
+ }
610
+ } catch (error) {
611
+ // Ignore cleanup errors
612
+ }
613
+ });
614
+ });