@onlineapps/conn-base-cache 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 +692 -0
- package/README.md +383 -0
- package/coverage/clover.xml +205 -0
- package/coverage/coverage-final.json +2 -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 +116 -0
- package/coverage/lcov-report/index.js.html +2851 -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 +370 -0
- package/package.json +36 -0
- package/src/index.js +923 -0
- package/test/component/cache.component.test.js +428 -0
- package/test/integration/cache.integration.test.js +356 -0
- package/test/unit/CacheConnector.edge.test.js +532 -0
- package/test/unit/CacheConnector.test.js +811 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests for CacheConnector
|
|
5
|
+
* Tests with real Redis instance when available
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CacheConnector = require('../../src/index');
|
|
9
|
+
|
|
10
|
+
const SKIP_INTEGRATION = process.env.SKIP_INTEGRATION === 'true';
|
|
11
|
+
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
|
|
12
|
+
const REDIS_PORT = process.env.REDIS_PORT || 6379;
|
|
13
|
+
|
|
14
|
+
describe('CacheConnector - Integration Tests', () => {
|
|
15
|
+
if (SKIP_INTEGRATION) {
|
|
16
|
+
it.skip('Skipping integration tests (SKIP_INTEGRATION=true)', () => {});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let cacheConnector;
|
|
21
|
+
let testNamespace;
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
testNamespace = `test-${Date.now()}`;
|
|
25
|
+
|
|
26
|
+
cacheConnector = new CacheConnector({
|
|
27
|
+
host: REDIS_HOST,
|
|
28
|
+
port: REDIS_PORT,
|
|
29
|
+
namespace: testNamespace,
|
|
30
|
+
defaultTTL: 60
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await cacheConnector.connect();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn('Redis not available, skipping integration tests');
|
|
37
|
+
console.warn('Error:', error.message);
|
|
38
|
+
cacheConnector = null;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
if (cacheConnector && cacheConnector.connected) {
|
|
44
|
+
// Clean up test data
|
|
45
|
+
await cacheConnector.clear();
|
|
46
|
+
await cacheConnector.disconnect();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
if (!cacheConnector) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Clear cache before each test
|
|
55
|
+
await cacheConnector.clear();
|
|
56
|
+
cacheConnector.resetStats();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Redis Connection', () => {
|
|
60
|
+
it('should connect to Redis successfully', async () => {
|
|
61
|
+
if (!cacheConnector) {
|
|
62
|
+
pending('Redis not available');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(cacheConnector.connected).toBe(true);
|
|
67
|
+
|
|
68
|
+
const pong = await cacheConnector.client.ping();
|
|
69
|
+
expect(pong).toBe('PONG');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle reconnection', async () => {
|
|
73
|
+
if (!cacheConnector) {
|
|
74
|
+
pending('Redis not available');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await cacheConnector.disconnect();
|
|
79
|
+
expect(cacheConnector.connected).toBe(false);
|
|
80
|
+
|
|
81
|
+
await cacheConnector.connect();
|
|
82
|
+
expect(cacheConnector.connected).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Real-world Caching Scenarios', () => {
|
|
87
|
+
it('should cache API response data', async () => {
|
|
88
|
+
if (!cacheConnector) {
|
|
89
|
+
pending('Redis not available');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const apiResponse = {
|
|
94
|
+
users: [
|
|
95
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com' },
|
|
96
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
|
97
|
+
],
|
|
98
|
+
total: 2,
|
|
99
|
+
page: 1,
|
|
100
|
+
timestamp: Date.now()
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await cacheConnector.set('api:users:page:1', apiResponse, 300);
|
|
104
|
+
|
|
105
|
+
const cached = await cacheConnector.get('api:users:page:1');
|
|
106
|
+
expect(cached).toEqual(apiResponse);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle high-volume operations', async () => {
|
|
110
|
+
if (!cacheConnector) {
|
|
111
|
+
pending('Redis not available');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const operations = [];
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
|
|
118
|
+
// Perform 100 set operations
|
|
119
|
+
for (let i = 0; i < 100; i++) {
|
|
120
|
+
operations.push(
|
|
121
|
+
cacheConnector.set(`perf:key${i}`, { index: i, data: 'x'.repeat(100) })
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await Promise.all(operations);
|
|
126
|
+
|
|
127
|
+
// Perform 100 get operations
|
|
128
|
+
const getOperations = [];
|
|
129
|
+
for (let i = 0; i < 100; i++) {
|
|
130
|
+
getOperations.push(cacheConnector.get(`perf:key${i}`));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const results = await Promise.all(getOperations);
|
|
134
|
+
const duration = Date.now() - startTime;
|
|
135
|
+
|
|
136
|
+
expect(results).toHaveLength(100);
|
|
137
|
+
expect(results[0]).toEqual({ index: 0, data: 'x'.repeat(100) });
|
|
138
|
+
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle concurrent access correctly', async () => {
|
|
142
|
+
if (!cacheConnector) {
|
|
143
|
+
pending('Redis not available');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const key = 'concurrent:counter';
|
|
148
|
+
let counter = 0;
|
|
149
|
+
|
|
150
|
+
// Simulate concurrent increments
|
|
151
|
+
const incrementOperations = Array.from({ length: 50 }, async () => {
|
|
152
|
+
const current = await cacheConnector.get(key) || 0;
|
|
153
|
+
const newValue = current + 1;
|
|
154
|
+
await cacheConnector.set(key, newValue, 60);
|
|
155
|
+
counter++;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await Promise.all(incrementOperations);
|
|
159
|
+
|
|
160
|
+
// Due to race conditions, final value might be less than 50
|
|
161
|
+
const finalValue = await cacheConnector.get(key);
|
|
162
|
+
expect(finalValue).toBeGreaterThan(0);
|
|
163
|
+
expect(finalValue).toBeLessThanOrEqual(50);
|
|
164
|
+
expect(counter).toBe(50);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('TTL and Expiration', () => {
|
|
169
|
+
it('should respect TTL on real Redis', async () => {
|
|
170
|
+
if (!cacheConnector) {
|
|
171
|
+
pending('Redis not available');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await cacheConnector.set('ttl:test', 'value', 2); // 2 second TTL
|
|
176
|
+
|
|
177
|
+
// Check immediately
|
|
178
|
+
let value = await cacheConnector.get('ttl:test');
|
|
179
|
+
expect(value).toBe('value');
|
|
180
|
+
|
|
181
|
+
// Check TTL
|
|
182
|
+
const ttl = await cacheConnector.ttl('ttl:test');
|
|
183
|
+
expect(ttl).toBeGreaterThan(0);
|
|
184
|
+
expect(ttl).toBeLessThanOrEqual(2);
|
|
185
|
+
|
|
186
|
+
// Wait for expiry
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 2100));
|
|
188
|
+
|
|
189
|
+
value = await cacheConnector.get('ttl:test');
|
|
190
|
+
expect(value).toBe(null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should update TTL on existing keys', async () => {
|
|
194
|
+
if (!cacheConnector) {
|
|
195
|
+
pending('Redis not available');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await cacheConnector.set('ttl:update', 'value', 10);
|
|
200
|
+
|
|
201
|
+
// Update with new TTL
|
|
202
|
+
await cacheConnector.expire('ttl:update', 30);
|
|
203
|
+
|
|
204
|
+
const ttl = await cacheConnector.ttl('ttl:update');
|
|
205
|
+
expect(ttl).toBeGreaterThan(25);
|
|
206
|
+
expect(ttl).toBeLessThanOrEqual(30);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Namespace Isolation', () => {
|
|
211
|
+
it('should isolate data between different namespaces', async () => {
|
|
212
|
+
if (!cacheConnector) {
|
|
213
|
+
pending('Redis not available');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create second cache with different namespace
|
|
218
|
+
const cache2 = new CacheConnector({
|
|
219
|
+
host: REDIS_HOST,
|
|
220
|
+
port: REDIS_PORT,
|
|
221
|
+
namespace: `${testNamespace}-2`
|
|
222
|
+
});
|
|
223
|
+
await cache2.connect();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await cacheConnector.set('shared:key', 'value1');
|
|
227
|
+
await cache2.set('shared:key', 'value2');
|
|
228
|
+
|
|
229
|
+
const value1 = await cacheConnector.get('shared:key');
|
|
230
|
+
const value2 = await cache2.get('shared:key');
|
|
231
|
+
|
|
232
|
+
expect(value1).toBe('value1');
|
|
233
|
+
expect(value2).toBe('value2');
|
|
234
|
+
|
|
235
|
+
// Clear should only affect own namespace
|
|
236
|
+
await cache2.clear();
|
|
237
|
+
|
|
238
|
+
expect(await cacheConnector.get('shared:key')).toBe('value1');
|
|
239
|
+
expect(await cache2.get('shared:key')).toBe(null);
|
|
240
|
+
|
|
241
|
+
} finally {
|
|
242
|
+
await cache2.disconnect();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('Error Recovery', () => {
|
|
248
|
+
it('should handle temporary network issues', async () => {
|
|
249
|
+
if (!cacheConnector) {
|
|
250
|
+
pending('Redis not available');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Store original client
|
|
255
|
+
const originalClient = cacheConnector.client;
|
|
256
|
+
|
|
257
|
+
// Simulate network issue by replacing client
|
|
258
|
+
cacheConnector.client = {
|
|
259
|
+
get: jest.fn().mockRejectedValueOnce(new Error('ECONNRESET'))
|
|
260
|
+
.mockImplementation((...args) => originalClient.get(...args))
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// First call fails
|
|
264
|
+
try {
|
|
265
|
+
await cacheConnector.get('test');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
expect(error.message).toContain('ECONNRESET');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Restore client
|
|
271
|
+
cacheConnector.client = originalClient;
|
|
272
|
+
|
|
273
|
+
// Should work again
|
|
274
|
+
await cacheConnector.set('recovery:test', 'recovered');
|
|
275
|
+
const value = await cacheConnector.get('recovery:test');
|
|
276
|
+
expect(value).toBe('recovered');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Large Data Handling', () => {
|
|
281
|
+
it('should handle large JSON objects', async () => {
|
|
282
|
+
if (!cacheConnector) {
|
|
283
|
+
pending('Redis not available');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const largeObject = {
|
|
288
|
+
data: Array.from({ length: 1000 }, (_, i) => ({
|
|
289
|
+
id: i,
|
|
290
|
+
name: `User ${i}`,
|
|
291
|
+
email: `user${i}@example.com`,
|
|
292
|
+
metadata: {
|
|
293
|
+
created: Date.now(),
|
|
294
|
+
tags: ['tag1', 'tag2', 'tag3'],
|
|
295
|
+
description: 'x'.repeat(100)
|
|
296
|
+
}
|
|
297
|
+
}))
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
await cacheConnector.set('large:object', largeObject, 60);
|
|
301
|
+
const retrieved = await cacheConnector.get('large:object');
|
|
302
|
+
|
|
303
|
+
expect(retrieved).toEqual(largeObject);
|
|
304
|
+
expect(retrieved.data).toHaveLength(1000);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should handle binary data (base64 encoded)', async () => {
|
|
308
|
+
if (!cacheConnector) {
|
|
309
|
+
pending('Redis not available');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Simulate binary data as base64
|
|
314
|
+
const binaryData = Buffer.from('Hello, World!').toString('base64');
|
|
315
|
+
|
|
316
|
+
await cacheConnector.set('binary:data', binaryData);
|
|
317
|
+
const retrieved = await cacheConnector.get('binary:data');
|
|
318
|
+
|
|
319
|
+
expect(retrieved).toBe(binaryData);
|
|
320
|
+
|
|
321
|
+
const decoded = Buffer.from(retrieved, 'base64').toString();
|
|
322
|
+
expect(decoded).toBe('Hello, World!');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Performance Monitoring', () => {
|
|
327
|
+
it('should accurately track statistics', async () => {
|
|
328
|
+
if (!cacheConnector) {
|
|
329
|
+
pending('Redis not available');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Perform various operations
|
|
334
|
+
await cacheConnector.set('stat:1', 'value1');
|
|
335
|
+
await cacheConnector.set('stat:2', 'value2');
|
|
336
|
+
await cacheConnector.set('stat:3', 'value3');
|
|
337
|
+
|
|
338
|
+
await cacheConnector.get('stat:1'); // hit
|
|
339
|
+
await cacheConnector.get('stat:2'); // hit
|
|
340
|
+
await cacheConnector.get('stat:missing'); // miss
|
|
341
|
+
await cacheConnector.get('stat:missing2'); // miss
|
|
342
|
+
|
|
343
|
+
await cacheConnector.del('stat:1');
|
|
344
|
+
await cacheConnector.del(['stat:2', 'stat:3']);
|
|
345
|
+
|
|
346
|
+
const stats = cacheConnector.getStats();
|
|
347
|
+
|
|
348
|
+
expect(stats.sets).toBe(3);
|
|
349
|
+
expect(stats.hits).toBe(2);
|
|
350
|
+
expect(stats.misses).toBe(2);
|
|
351
|
+
expect(stats.deletes).toBe(2); // del operations, not key count
|
|
352
|
+
expect(stats.hitRate).toBe(0.5);
|
|
353
|
+
expect(stats.totalOperations).toBe(9);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|