@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,532 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Edge case tests for CacheConnector
5
+ * Tests boundary conditions, edge cases, and error scenarios for better coverage
6
+ */
7
+
8
+ jest.mock('ioredis');
9
+ const Redis = require('ioredis');
10
+ const CacheConnector = require('../../src/index');
11
+
12
+ describe('CacheConnector - Edge Cases & Full Coverage', () => {
13
+ let cacheConnector;
14
+ let mockRedisClient;
15
+
16
+ beforeEach(() => {
17
+ mockRedisClient = {
18
+ get: jest.fn(),
19
+ set: jest.fn(),
20
+ setex: jest.fn(),
21
+ del: jest.fn(),
22
+ exists: jest.fn(),
23
+ expire: jest.fn(),
24
+ ttl: jest.fn(),
25
+ keys: jest.fn(),
26
+ scan: jest.fn(),
27
+ mget: jest.fn(),
28
+ mset: jest.fn(),
29
+ flushdb: jest.fn(),
30
+ on: jest.fn(),
31
+ connect: jest.fn().mockResolvedValue(),
32
+ ping: jest.fn().mockResolvedValue('PONG'),
33
+ quit: jest.fn().mockResolvedValue('OK'),
34
+ disconnect: jest.fn().mockResolvedValue('OK')
35
+ };
36
+
37
+ Redis.mockImplementation(() => mockRedisClient);
38
+ });
39
+
40
+ afterEach(() => {
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ describe('Constructor Edge Cases', () => {
45
+ it('should handle undefined config', () => {
46
+ const cache = new CacheConnector(undefined);
47
+ expect(cache.config).toBeDefined();
48
+ expect(cache.config.host).toBe('localhost');
49
+ });
50
+
51
+ it('should handle null config', () => {
52
+ const cache = new CacheConnector(null);
53
+ expect(cache.config).toBeDefined();
54
+ });
55
+
56
+ it('should use environment variables when available', () => {
57
+ process.env.REDIS_HOST = 'env-host';
58
+ process.env.REDIS_PORT = '6380';
59
+ process.env.REDIS_PASSWORD = 'secret';
60
+ process.env.REDIS_DB = '2';
61
+
62
+ const cache = new CacheConnector();
63
+
64
+ expect(cache.config.host).toBe('env-host');
65
+ expect(cache.config.port).toBe('6380');
66
+ expect(cache.config.password).toBe('secret');
67
+ expect(cache.config.db).toBe('2');
68
+
69
+ // Cleanup
70
+ delete process.env.REDIS_HOST;
71
+ delete process.env.REDIS_PORT;
72
+ delete process.env.REDIS_PASSWORD;
73
+ delete process.env.REDIS_DB;
74
+ });
75
+
76
+ it('should handle empty namespace', () => {
77
+ const cache = new CacheConnector({ namespace: '' });
78
+ expect(cache.namespace).toBe('');
79
+ });
80
+
81
+ it('should handle very long namespace', () => {
82
+ const longNamespace = 'a'.repeat(1000);
83
+ const cache = new CacheConnector({ namespace: longNamespace });
84
+ expect(cache.namespace).toBe(longNamespace);
85
+ });
86
+ });
87
+
88
+ describe('Connection Edge Cases', () => {
89
+ beforeEach(() => {
90
+ cacheConnector = new CacheConnector();
91
+ });
92
+
93
+ it('should handle connection timeout', async () => {
94
+ mockRedisClient.on.mockImplementation((event, handler) => {
95
+ if (event === 'error') {
96
+ setTimeout(() => handler(new Error('Connection timeout')), 0);
97
+ }
98
+ });
99
+
100
+ await cacheConnector.connect();
101
+
102
+ expect(mockRedisClient.on).toHaveBeenCalledWith('error', expect.any(Function));
103
+ });
104
+
105
+ it('should handle Redis auth error', async () => {
106
+ Redis.mockImplementationOnce(() => {
107
+ throw new Error('ERR invalid password');
108
+ });
109
+
110
+ await expect(cacheConnector.connect()).rejects.toThrow('ERR invalid password');
111
+ });
112
+
113
+ it('should handle multiple rapid connect calls', async () => {
114
+ const promises = [
115
+ cacheConnector.connect(),
116
+ cacheConnector.connect(),
117
+ cacheConnector.connect()
118
+ ];
119
+
120
+ await Promise.all(promises);
121
+
122
+ // Should only create one Redis instance
123
+ expect(Redis).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it('should handle disconnect during operation', async () => {
127
+ await cacheConnector.connect();
128
+
129
+ // Simulate disconnect during operation
130
+ mockRedisClient.get.mockImplementation(() => {
131
+ cacheConnector.connected = false;
132
+ throw new Error('Connection lost');
133
+ });
134
+
135
+ await expect(cacheConnector.get('key')).rejects.toThrow();
136
+ });
137
+ });
138
+
139
+ describe('Data Type Edge Cases', () => {
140
+ beforeEach(async () => {
141
+ cacheConnector = new CacheConnector();
142
+ await cacheConnector.connect();
143
+ });
144
+
145
+ it('should handle null values', async () => {
146
+ mockRedisClient.setex.mockResolvedValue('OK');
147
+ await cacheConnector.set('null', null);
148
+
149
+ mockRedisClient.get.mockResolvedValue(JSON.stringify(null));
150
+ const value = await cacheConnector.get('null');
151
+
152
+ expect(value).toBe(null);
153
+ });
154
+
155
+ it('should handle undefined values', async () => {
156
+ mockRedisClient.setex.mockResolvedValue('OK');
157
+ await cacheConnector.set('undefined', undefined);
158
+
159
+ mockRedisClient.get.mockResolvedValue(JSON.stringify(undefined));
160
+ const value = await cacheConnector.get('undefined');
161
+
162
+ expect(value).toBe(undefined);
163
+ });
164
+
165
+ it('should handle empty string', async () => {
166
+ mockRedisClient.setex.mockResolvedValue('OK');
167
+ await cacheConnector.set('empty', '');
168
+
169
+ mockRedisClient.get.mockResolvedValue(JSON.stringify(''));
170
+ const value = await cacheConnector.get('empty');
171
+
172
+ expect(value).toBe('');
173
+ });
174
+
175
+ it('should handle special characters in keys', async () => {
176
+ const specialKey = 'key:with:colons:and-dashes_underscores.dots';
177
+ mockRedisClient.setex.mockResolvedValue('OK');
178
+
179
+ await cacheConnector.set(specialKey, 'value');
180
+
181
+ expect(mockRedisClient.setex).toHaveBeenCalledWith(
182
+ expect.stringContaining(specialKey),
183
+ expect.any(Number),
184
+ expect.any(String)
185
+ );
186
+ });
187
+
188
+ it('should handle very large strings', async () => {
189
+ const largeString = 'x'.repeat(1024 * 1024); // 1MB string
190
+ mockRedisClient.setex.mockResolvedValue('OK');
191
+
192
+ await cacheConnector.set('large', largeString);
193
+
194
+ expect(mockRedisClient.setex).toHaveBeenCalled();
195
+ });
196
+
197
+ it('should handle special numbers', async () => {
198
+ mockRedisClient.setex.mockResolvedValue('OK');
199
+
200
+ // Infinity
201
+ await cacheConnector.set('infinity', Infinity);
202
+ // NaN
203
+ await cacheConnector.set('nan', NaN);
204
+ // Very large number
205
+ await cacheConnector.set('large-num', Number.MAX_SAFE_INTEGER);
206
+ // Very small number
207
+ await cacheConnector.set('small-num', Number.MIN_SAFE_INTEGER);
208
+
209
+ expect(mockRedisClient.setex).toHaveBeenCalledTimes(4);
210
+ });
211
+
212
+ it('should handle dates', async () => {
213
+ const date = new Date('2024-01-01');
214
+ mockRedisClient.setex.mockResolvedValue('OK');
215
+
216
+ await cacheConnector.set('date', date);
217
+
218
+ mockRedisClient.get.mockResolvedValue(JSON.stringify(date));
219
+ const retrieved = await cacheConnector.get('date');
220
+
221
+ expect(new Date(retrieved)).toEqual(date);
222
+ });
223
+ });
224
+
225
+ describe('TTL Edge Cases', () => {
226
+ beforeEach(async () => {
227
+ cacheConnector = new CacheConnector({ defaultTTL: 100 });
228
+ await cacheConnector.connect();
229
+ });
230
+
231
+ it('should handle zero TTL', async () => {
232
+ mockRedisClient.setex.mockResolvedValue('OK');
233
+
234
+ await cacheConnector.set('zero-ttl', 'value', 0);
235
+
236
+ // Should use default TTL when 0 is provided
237
+ expect(mockRedisClient.setex).toHaveBeenCalledWith(
238
+ expect.any(String),
239
+ 100, // default TTL
240
+ expect.any(String)
241
+ );
242
+ });
243
+
244
+ it('should handle negative TTL', async () => {
245
+ mockRedisClient.setex.mockResolvedValue('OK');
246
+
247
+ await cacheConnector.set('neg-ttl', 'value', -1);
248
+
249
+ // Should use default TTL for negative values
250
+ expect(mockRedisClient.setex).toHaveBeenCalledWith(
251
+ expect.any(String),
252
+ 100, // default TTL
253
+ expect.any(String)
254
+ );
255
+ });
256
+
257
+ it('should handle very large TTL', async () => {
258
+ mockRedisClient.setex.mockResolvedValue('OK');
259
+
260
+ const maxTTL = Number.MAX_SAFE_INTEGER;
261
+ await cacheConnector.set('max-ttl', 'value', maxTTL);
262
+
263
+ expect(mockRedisClient.setex).toHaveBeenCalledWith(
264
+ expect.any(String),
265
+ maxTTL,
266
+ expect.any(String)
267
+ );
268
+ });
269
+
270
+ it('should handle non-numeric TTL', async () => {
271
+ mockRedisClient.setex.mockResolvedValue('OK');
272
+
273
+ await cacheConnector.set('string-ttl', 'value', 'not-a-number');
274
+
275
+ // Should use default TTL for invalid values
276
+ expect(mockRedisClient.setex).toHaveBeenCalledWith(
277
+ expect.any(String),
278
+ 100, // default TTL
279
+ expect.any(String)
280
+ );
281
+ });
282
+ });
283
+
284
+ describe('Batch Operations Edge Cases', () => {
285
+ beforeEach(async () => {
286
+ cacheConnector = new CacheConnector();
287
+ await cacheConnector.connect();
288
+ });
289
+
290
+ it('should handle empty array in del', async () => {
291
+ const result = await cacheConnector.del([]);
292
+ expect(result).toBe(0);
293
+ });
294
+
295
+ it('should handle single string in del (not array)', async () => {
296
+ mockRedisClient.del.mockResolvedValue(1);
297
+
298
+ const result = await cacheConnector.del('single-key');
299
+
300
+ expect(mockRedisClient.del).toHaveBeenCalledWith(
301
+ expect.stringContaining('single-key')
302
+ );
303
+ expect(result).toBe(1);
304
+ });
305
+
306
+ it('should handle very large batch delete', async () => {
307
+ const keys = Array.from({ length: 10000 }, (_, i) => `key${i}`);
308
+ mockRedisClient.del.mockResolvedValue(keys.length);
309
+
310
+ const result = await cacheConnector.del(keys);
311
+
312
+ expect(result).toBe(10000);
313
+ });
314
+
315
+ it('should handle empty array in mget', async () => {
316
+ const result = await cacheConnector.mget([]);
317
+ expect(result).toEqual([]);
318
+ });
319
+
320
+ it('should handle null responses in mget', async () => {
321
+ mockRedisClient.mget.mockResolvedValue([
322
+ JSON.stringify('value1'),
323
+ null,
324
+ JSON.stringify('value3')
325
+ ]);
326
+
327
+ const result = await cacheConnector.mget(['key1', 'key2', 'key3']);
328
+
329
+ expect(result).toEqual(['value1', null, 'value3']);
330
+ });
331
+ });
332
+
333
+ describe('Clear Operations Edge Cases', () => {
334
+ beforeEach(async () => {
335
+ cacheConnector = new CacheConnector({ namespace: 'test' });
336
+ await cacheConnector.connect();
337
+ });
338
+
339
+ it('should handle clear with no matching keys', async () => {
340
+ mockRedisClient.keys.mockResolvedValue([]);
341
+
342
+ const result = await cacheConnector.clear();
343
+
344
+ expect(result).toBe(0);
345
+ expect(mockRedisClient.del).not.toHaveBeenCalled();
346
+ });
347
+
348
+ it('should handle clear with pattern matching error', async () => {
349
+ mockRedisClient.keys.mockRejectedValue(new Error('Pattern error'));
350
+
351
+ await expect(cacheConnector.clear()).rejects.toThrow('Pattern error');
352
+ });
353
+
354
+ it('should handle partial delete failure', async () => {
355
+ mockRedisClient.keys.mockResolvedValue(['key1', 'key2', 'key3']);
356
+ mockRedisClient.del.mockResolvedValue(2); // Only 2 deleted instead of 3
357
+
358
+ const result = await cacheConnector.clear();
359
+
360
+ expect(result).toBe(2);
361
+ });
362
+ });
363
+
364
+ describe('Statistics Edge Cases', () => {
365
+ beforeEach(() => {
366
+ cacheConnector = new CacheConnector();
367
+ });
368
+
369
+ it('should handle stats when no operations performed', () => {
370
+ const stats = cacheConnector.getStats();
371
+
372
+ expect(stats).toEqual({
373
+ hits: 0,
374
+ misses: 0,
375
+ sets: 0,
376
+ deletes: 0,
377
+ errors: 0,
378
+ hitRate: 0,
379
+ totalOperations: 0
380
+ });
381
+ });
382
+
383
+ it('should handle hitRate with only misses', () => {
384
+ cacheConnector.stats.misses = 10;
385
+
386
+ const stats = cacheConnector.getStats();
387
+
388
+ expect(stats.hitRate).toBe(0);
389
+ });
390
+
391
+ it('should handle hitRate with only hits', () => {
392
+ cacheConnector.stats.hits = 10;
393
+
394
+ const stats = cacheConnector.getStats();
395
+
396
+ expect(stats.hitRate).toBe(1);
397
+ });
398
+
399
+ it('should handle integer overflow in stats', () => {
400
+ cacheConnector.stats = {
401
+ hits: Number.MAX_SAFE_INTEGER,
402
+ misses: Number.MAX_SAFE_INTEGER,
403
+ sets: Number.MAX_SAFE_INTEGER,
404
+ deletes: Number.MAX_SAFE_INTEGER,
405
+ errors: Number.MAX_SAFE_INTEGER
406
+ };
407
+
408
+ const stats = cacheConnector.getStats();
409
+
410
+ expect(stats.totalOperations).toBeDefined();
411
+ expect(stats.hitRate).toBe(0.5);
412
+ });
413
+ });
414
+
415
+ describe('Error Propagation', () => {
416
+ beforeEach(async () => {
417
+ cacheConnector = new CacheConnector();
418
+ await cacheConnector.connect();
419
+ });
420
+
421
+ it('should propagate Redis errors with context', async () => {
422
+ const redisError = new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
423
+ mockRedisClient.get.mockRejectedValue(redisError);
424
+
425
+ await expect(cacheConnector.get('key')).rejects.toThrow('WRONGTYPE');
426
+ expect(cacheConnector.stats.errors).toBe(1);
427
+ });
428
+
429
+ it('should handle out of memory errors', async () => {
430
+ const oomError = new Error('OOM command not allowed when used memory > maxmemory');
431
+ mockRedisClient.setex.mockRejectedValue(oomError);
432
+
433
+ await expect(cacheConnector.set('key', 'value')).rejects.toThrow('OOM');
434
+ expect(cacheConnector.stats.errors).toBe(1);
435
+ });
436
+
437
+ it('should handle script errors', async () => {
438
+ const scriptError = new Error('ERR Error running script');
439
+ mockRedisClient.get.mockRejectedValue(scriptError);
440
+
441
+ await expect(cacheConnector.get('key')).rejects.toThrow('ERR Error');
442
+ expect(cacheConnector.stats.errors).toBe(1);
443
+ });
444
+ });
445
+
446
+ describe('Method Aliases and Helpers', () => {
447
+ beforeEach(async () => {
448
+ cacheConnector = new CacheConnector();
449
+ await cacheConnector.connect();
450
+ });
451
+
452
+ it('should have working expire method', async () => {
453
+ mockRedisClient.expire.mockResolvedValue(1);
454
+
455
+ const result = await cacheConnector.expire('key', 300);
456
+
457
+ expect(mockRedisClient.expire).toHaveBeenCalledWith(
458
+ expect.stringContaining('key'),
459
+ 300
460
+ );
461
+ expect(result).toBe(1);
462
+ });
463
+
464
+ it('should have working resetStats method', () => {
465
+ cacheConnector.stats = {
466
+ hits: 100,
467
+ misses: 50,
468
+ sets: 75,
469
+ deletes: 25,
470
+ errors: 5
471
+ };
472
+
473
+ cacheConnector.resetStats();
474
+
475
+ expect(cacheConnector.stats).toEqual({
476
+ hits: 0,
477
+ misses: 0,
478
+ sets: 0,
479
+ deletes: 0,
480
+ errors: 0
481
+ });
482
+ });
483
+
484
+ it('should have working isConnected method', () => {
485
+ expect(cacheConnector.isConnected()).toBe(true);
486
+
487
+ cacheConnector.connected = false;
488
+ expect(cacheConnector.isConnected()).toBe(false);
489
+ });
490
+ });
491
+
492
+ describe('Concurrent Operations', () => {
493
+ beforeEach(async () => {
494
+ cacheConnector = new CacheConnector();
495
+ await cacheConnector.connect();
496
+ });
497
+
498
+ it('should handle concurrent sets correctly', async () => {
499
+ mockRedisClient.setex.mockResolvedValue('OK');
500
+
501
+ const operations = Array.from({ length: 100 }, (_, i) =>
502
+ cacheConnector.set(`concurrent:${i}`, `value${i}`)
503
+ );
504
+
505
+ const results = await Promise.all(operations);
506
+
507
+ expect(results).toHaveLength(100);
508
+ expect(results.every(r => r === 'OK')).toBe(true);
509
+ expect(cacheConnector.stats.sets).toBe(100);
510
+ });
511
+
512
+ it('should handle mixed concurrent operations', async () => {
513
+ mockRedisClient.setex.mockResolvedValue('OK');
514
+ mockRedisClient.get.mockResolvedValue(JSON.stringify('value'));
515
+ mockRedisClient.del.mockResolvedValue(1);
516
+ mockRedisClient.exists.mockResolvedValue(1);
517
+
518
+ const operations = [
519
+ ...Array.from({ length: 25 }, () => cacheConnector.set('key', 'value')),
520
+ ...Array.from({ length: 25 }, () => cacheConnector.get('key')),
521
+ ...Array.from({ length: 25 }, () => cacheConnector.del('key')),
522
+ ...Array.from({ length: 25 }, () => cacheConnector.exists('key'))
523
+ ];
524
+
525
+ await Promise.all(operations);
526
+
527
+ expect(cacheConnector.stats.sets).toBe(25);
528
+ expect(cacheConnector.stats.hits + cacheConnector.stats.misses).toBe(25);
529
+ expect(cacheConnector.stats.deletes).toBe(25);
530
+ });
531
+ });
532
+ });