@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests pro StorageConnector
|
|
3
|
+
* 100% mockované - MUSÍ VŽDY PROJÍT (100% pass rate)
|
|
4
|
+
* Žádné externí závislosti - vše je mockované
|
|
5
|
+
*
|
|
6
|
+
* Testovací strategie:
|
|
7
|
+
* - Všechny MinIO operace jsou mockovány
|
|
8
|
+
* - Testy běží bez potřeby MinIO serveru
|
|
9
|
+
* - Pokrývají všechny metody a edge cases
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const StorageConnector = require('../../src/index');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
// Mock MinIO client kompletně
|
|
16
|
+
jest.mock('minio', () => {
|
|
17
|
+
return {
|
|
18
|
+
Client: jest.fn().mockImplementation(() => ({
|
|
19
|
+
bucketExists: jest.fn(),
|
|
20
|
+
makeBucket: jest.fn(),
|
|
21
|
+
setBucketPolicy: jest.fn(),
|
|
22
|
+
putObject: jest.fn(),
|
|
23
|
+
getObject: jest.fn(),
|
|
24
|
+
statObject: jest.fn(),
|
|
25
|
+
removeObject: jest.fn(),
|
|
26
|
+
removeBucket: jest.fn(),
|
|
27
|
+
listObjectsV2: jest.fn(),
|
|
28
|
+
presignedGetObject: jest.fn(),
|
|
29
|
+
presignedGetUrl: jest.fn(),
|
|
30
|
+
useSSL: false,
|
|
31
|
+
port: 9000,
|
|
32
|
+
host: 'localhost'
|
|
33
|
+
}))
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('StorageConnector Unit Tests', () => {
|
|
38
|
+
describe('Initialization', () => {
|
|
39
|
+
test('should create instance with default config', () => {
|
|
40
|
+
const storage = new StorageConnector();
|
|
41
|
+
expect(storage).toBeDefined();
|
|
42
|
+
expect(storage.config).toBeDefined();
|
|
43
|
+
expect(storage.config.endPoint).toBe('localhost');
|
|
44
|
+
expect(storage.config.port).toBe(9000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should create instance with custom config', () => {
|
|
48
|
+
const config = {
|
|
49
|
+
endPoint: 'minio.example.com',
|
|
50
|
+
port: 9001,
|
|
51
|
+
useSSL: true,
|
|
52
|
+
accessKey: 'custom-key',
|
|
53
|
+
secretKey: 'custom-secret',
|
|
54
|
+
bucketName: 'custom-bucket'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const storage = new StorageConnector(config);
|
|
58
|
+
expect(storage.config.endPoint).toBe('minio.example.com');
|
|
59
|
+
expect(storage.config.port).toBe(9001);
|
|
60
|
+
expect(storage.config.useSSL).toBe(true);
|
|
61
|
+
expect(storage.config.bucketName).toBe('custom-bucket');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Fingerprinting', () => {
|
|
66
|
+
let storage;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
storage = new StorageConnector();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should generate fingerprint for string content', () => {
|
|
73
|
+
const content = 'Hello, World!';
|
|
74
|
+
const fingerprint = storage.generateFingerprint(content);
|
|
75
|
+
|
|
76
|
+
expect(fingerprint).toBeDefined();
|
|
77
|
+
expect(fingerprint).toHaveLength(64); // SHA256 produces 64 hex characters
|
|
78
|
+
expect(fingerprint).toMatch(/^[a-f0-9]{64}$/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should generate consistent fingerprint for same content', () => {
|
|
82
|
+
const content = 'Test content';
|
|
83
|
+
const fingerprint1 = storage.generateFingerprint(content);
|
|
84
|
+
const fingerprint2 = storage.generateFingerprint(content);
|
|
85
|
+
|
|
86
|
+
expect(fingerprint1).toBe(fingerprint2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should generate different fingerprints for different content', () => {
|
|
90
|
+
const fingerprint1 = storage.generateFingerprint('Content 1');
|
|
91
|
+
const fingerprint2 = storage.generateFingerprint('Content 2');
|
|
92
|
+
|
|
93
|
+
expect(fingerprint1).not.toBe(fingerprint2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should handle Buffer content', () => {
|
|
97
|
+
const buffer = Buffer.from('Buffer content');
|
|
98
|
+
const fingerprint = storage.generateFingerprint(buffer);
|
|
99
|
+
|
|
100
|
+
expect(fingerprint).toBeDefined();
|
|
101
|
+
expect(fingerprint).toHaveLength(64);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should handle object content', () => {
|
|
105
|
+
const obj = { key: 'value', nested: { data: 123 } };
|
|
106
|
+
const fingerprint = storage.generateFingerprint(obj);
|
|
107
|
+
|
|
108
|
+
expect(fingerprint).toBeDefined();
|
|
109
|
+
expect(fingerprint).toHaveLength(64);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should handle array content', () => {
|
|
113
|
+
const arr = [1, 2, 3, 'test', { key: 'value' }];
|
|
114
|
+
const fingerprint = storage.generateFingerprint(arr);
|
|
115
|
+
|
|
116
|
+
expect(fingerprint).toBeDefined();
|
|
117
|
+
expect(fingerprint).toHaveLength(64);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should handle null content', () => {
|
|
121
|
+
const fingerprint = storage.generateFingerprint(null);
|
|
122
|
+
expect(fingerprint).toBeDefined();
|
|
123
|
+
expect(fingerprint).toHaveLength(64);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should handle undefined content', () => {
|
|
127
|
+
const fingerprint = storage.generateFingerprint(undefined);
|
|
128
|
+
expect(fingerprint).toBeDefined();
|
|
129
|
+
expect(fingerprint).toHaveLength(64);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should handle circular references in objects', () => {
|
|
133
|
+
const obj = { name: 'test' };
|
|
134
|
+
obj.circular = obj;
|
|
135
|
+
|
|
136
|
+
expect(() => storage.generateFingerprint(obj)).not.toThrow();
|
|
137
|
+
const fingerprint = storage.generateFingerprint(obj);
|
|
138
|
+
expect(fingerprint).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Cache Management', () => {
|
|
143
|
+
let storage;
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
storage = new StorageConnector({ cacheMaxSize: 3 });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should add items to cache', () => {
|
|
150
|
+
storage.addToCache('key1', 'value1');
|
|
151
|
+
expect(storage.getFromCache('key1')).toBe('value1');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should maintain insertion order (Map behavior)', () => {
|
|
155
|
+
storage.addToCache('key1', 'value1');
|
|
156
|
+
storage.addToCache('key2', 'value2');
|
|
157
|
+
storage.addToCache('key3', 'value3');
|
|
158
|
+
storage.addToCache('key4', 'value4');
|
|
159
|
+
|
|
160
|
+
// Map maintains insertion order, removes oldest
|
|
161
|
+
expect(storage.getFromCache('key1')).toBeUndefined();
|
|
162
|
+
expect(storage.getFromCache('key2')).toBe('value2');
|
|
163
|
+
expect(storage.getFromCache('key3')).toBe('value3');
|
|
164
|
+
expect(storage.getFromCache('key4')).toBe('value4');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should clear cache', () => {
|
|
168
|
+
storage.addToCache('key1', 'value1');
|
|
169
|
+
storage.addToCache('key2', 'value2');
|
|
170
|
+
|
|
171
|
+
storage.clearCache();
|
|
172
|
+
|
|
173
|
+
expect(storage.getFromCache('key1')).toBeUndefined();
|
|
174
|
+
expect(storage.getFromCache('key2')).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should respect cache max size', () => {
|
|
178
|
+
for (let i = 0; i < 10; i++) {
|
|
179
|
+
storage.addToCache(`key${i}`, `value${i}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Only last 3 items should be in cache
|
|
183
|
+
expect(storage.cache.size).toBe(3);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Error Handling', () => {
|
|
188
|
+
let storage;
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
storage = new StorageConnector();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should handle invalid content types for fingerprinting', () => {
|
|
195
|
+
const symbol = Symbol('test');
|
|
196
|
+
expect(() => storage.generateFingerprint(symbol)).not.toThrow();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should validate bucket name', () => {
|
|
200
|
+
const validNames = ['valid-bucket', 'bucket123', 'my.bucket'];
|
|
201
|
+
const invalidNames = ['', 'a', 'UPPERCASE', 'bucket_name', 'bucket-'];
|
|
202
|
+
|
|
203
|
+
validNames.forEach(name => {
|
|
204
|
+
const s = new StorageConnector({ bucketName: name });
|
|
205
|
+
expect(s.config.bucketName).toBe(name);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Invalid names should use default
|
|
209
|
+
invalidNames.forEach(name => {
|
|
210
|
+
const s = new StorageConnector({ bucketName: name });
|
|
211
|
+
expect(s.config.bucketName).toBe('oa-drive-storage');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('Utility Methods', () => {
|
|
217
|
+
let storage;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
storage = new StorageConnector();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should generate object name with fingerprint', () => {
|
|
224
|
+
const originalName = 'test-file.json';
|
|
225
|
+
const fingerprint = 'abc123def456';
|
|
226
|
+
|
|
227
|
+
const objectName = storage.getObjectNameWithFingerprint(originalName, fingerprint);
|
|
228
|
+
|
|
229
|
+
expect(objectName).toContain(fingerprint);
|
|
230
|
+
expect(objectName).toContain('test-file');
|
|
231
|
+
expect(objectName).toContain('.json');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('should extract fingerprint from object name', () => {
|
|
235
|
+
// SHA256 produces 64 character hex string
|
|
236
|
+
const testFingerprint = 'a'.repeat(64);
|
|
237
|
+
const objectName = `test-file-${testFingerprint}.json`;
|
|
238
|
+
const fingerprint = storage.extractFingerprintFromName(objectName);
|
|
239
|
+
|
|
240
|
+
expect(fingerprint).toBe(testFingerprint);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('should return null for object name without fingerprint', () => {
|
|
244
|
+
const objectName = 'regular-file.json';
|
|
245
|
+
const fingerprint = storage.extractFingerprintFromName(objectName);
|
|
246
|
+
|
|
247
|
+
expect(fingerprint).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('Mocked MinIO Operations', () => {
|
|
252
|
+
let storage;
|
|
253
|
+
let mockClient;
|
|
254
|
+
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
jest.clearAllMocks();
|
|
257
|
+
storage = new StorageConnector();
|
|
258
|
+
mockClient = storage.minioClient;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('should initialize with mocked MinIO client', async () => {
|
|
262
|
+
mockClient.bucketExists.mockResolvedValue(false);
|
|
263
|
+
mockClient.makeBucket.mockResolvedValue();
|
|
264
|
+
mockClient.setBucketPolicy.mockResolvedValue();
|
|
265
|
+
|
|
266
|
+
await storage.initialize();
|
|
267
|
+
|
|
268
|
+
expect(storage.initialized).toBe(true);
|
|
269
|
+
expect(mockClient.bucketExists).toHaveBeenCalled();
|
|
270
|
+
expect(mockClient.makeBucket).toHaveBeenCalled();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('should upload with fingerprint using mocked client', async () => {
|
|
274
|
+
mockClient.statObject.mockRejectedValue({ code: 'NotFound' });
|
|
275
|
+
mockClient.putObject.mockResolvedValue();
|
|
276
|
+
|
|
277
|
+
const result = await storage.uploadWithFingerprint('test-bucket', 'test content');
|
|
278
|
+
|
|
279
|
+
expect(result.fingerprint).toBeDefined();
|
|
280
|
+
expect(result.existed).toBe(false);
|
|
281
|
+
expect(mockClient.putObject).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('should download with verification using mocked client', async () => {
|
|
285
|
+
const content = 'test content';
|
|
286
|
+
const fingerprint = storage.generateFingerprint(content);
|
|
287
|
+
|
|
288
|
+
// Mock stream for getObject
|
|
289
|
+
const mockStream = {
|
|
290
|
+
[Symbol.asyncIterator]: async function* () {
|
|
291
|
+
yield Buffer.from(content);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
mockClient.getObject.mockResolvedValue(mockStream);
|
|
295
|
+
|
|
296
|
+
const result = await storage.downloadWithVerification(
|
|
297
|
+
'test-bucket',
|
|
298
|
+
'test-path',
|
|
299
|
+
fingerprint
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(result).toBe(content);
|
|
303
|
+
expect(mockClient.getObject).toHaveBeenCalledWith('test-bucket', 'test-path');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('should list objects with mocked client', async () => {
|
|
307
|
+
const mockObjects = [
|
|
308
|
+
{ name: 'file1.txt', size: 100, lastModified: new Date() },
|
|
309
|
+
{ name: 'file2.txt', size: 200, lastModified: new Date() }
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
mockClient.listObjectsV2.mockReturnValue({
|
|
313
|
+
on: jest.fn((event, callback) => {
|
|
314
|
+
if (event === 'data') {
|
|
315
|
+
mockObjects.forEach(callback);
|
|
316
|
+
} else if (event === 'end') {
|
|
317
|
+
callback();
|
|
318
|
+
}
|
|
319
|
+
return mockClient.listObjectsV2();
|
|
320
|
+
})
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const result = await storage.listObjectsWithFingerprints('test-bucket');
|
|
324
|
+
|
|
325
|
+
expect(result).toHaveLength(2);
|
|
326
|
+
expect(result[0].name).toBe('file1.txt');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('should generate presigned URL with mocked client', async () => {
|
|
330
|
+
const expectedUrl = 'http://localhost:9000/bucket/object?signature=xyz';
|
|
331
|
+
mockClient.presignedGetUrl.mockResolvedValue(expectedUrl);
|
|
332
|
+
|
|
333
|
+
const url = await storage.getPresignedUrl('test-bucket', 'test-object');
|
|
334
|
+
|
|
335
|
+
expect(url).toBe(expectedUrl);
|
|
336
|
+
expect(mockClient.presignedGetUrl).toHaveBeenCalledWith(
|
|
337
|
+
'GET',
|
|
338
|
+
'test-bucket',
|
|
339
|
+
'test-object',
|
|
340
|
+
86400
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('should handle errors from mocked client', async () => {
|
|
345
|
+
mockClient.bucketExists.mockRejectedValue(new Error('Connection failed'));
|
|
346
|
+
|
|
347
|
+
await expect(storage.initialize()).rejects.toThrow('Connection failed');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('should detect existing objects (idempotency)', async () => {
|
|
351
|
+
mockClient.statObject.mockResolvedValue({ size: 100, metaData: {} });
|
|
352
|
+
|
|
353
|
+
const result = await storage.uploadWithFingerprint('test-bucket', 'content');
|
|
354
|
+
|
|
355
|
+
expect(result.existed).toBe(true);
|
|
356
|
+
expect(mockClient.putObject).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('should verify fingerprint mismatch', async () => {
|
|
360
|
+
const wrongContent = 'wrong content';
|
|
361
|
+
const mockStream = {
|
|
362
|
+
[Symbol.asyncIterator]: async function* () {
|
|
363
|
+
yield Buffer.from(wrongContent);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
mockClient.getObject.mockResolvedValue(mockStream);
|
|
367
|
+
|
|
368
|
+
await expect(
|
|
369
|
+
storage.downloadWithVerification('bucket', 'path', 'expectedfingerprint123')
|
|
370
|
+
).rejects.toThrow('Fingerprint mismatch');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|