@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.
@@ -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
+ });