@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.
- package/API.md +618 -0
- package/README.md +341 -0
- package/SHARED_URL_ADDRESSING.md +258 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +213 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/index.js.html +1579 -0
- package/coverage/internal-url-adapter.js.html +604 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/index.js.html +1579 -0
- package/coverage/lcov-report/internal-url-adapter.js.html +604 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +434 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/jest.config.js +13 -0
- package/jest.integration.config.js +9 -0
- package/package.json +33 -0
- package/src/index.js +853 -0
- package/src/internal-url-adapter.js +174 -0
- package/src/sharedUrlAdapter.js +258 -0
- package/test/component/storage.component.test.js +363 -0
- package/test/integration/setup.js +3 -0
- package/test/integration/storage.integration.test.js +224 -0
- package/test/unit/internal-url-adapter.test.js +211 -0
- package/test/unit/legacy.storage.test.js.bak +614 -0
- package/test/unit/storage.extended.unit.test.js +435 -0
- 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
|
+
});
|