|
| 1 | +import { Logger } from '@nestjs/common'; |
| 2 | +import { InMemoryCacheProvider } from './in-memory-cache.provider'; |
| 3 | +import { TTLBucket } from './i-cache-provider'; |
| 4 | + |
| 5 | +describe('InMemoryCacheProvider', () => { |
| 6 | + let cacheProvider: InMemoryCacheProvider; |
| 7 | + // Used to simulate the current time. |
| 8 | + const startTime = new Date(2025, 0, 1, 0, 0, 0).getTime(); |
| 9 | + |
| 10 | + beforeAll(() => { |
| 11 | + // Silence the logger by overriding its methods. |
| 12 | + jest.spyOn(Logger.prototype, 'verbose').mockImplementation(() => {}); |
| 13 | + jest.spyOn(Logger.prototype, 'debug').mockImplementation(() => {}); |
| 14 | + jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {}); |
| 15 | + }); |
| 16 | + |
| 17 | + beforeEach(() => { |
| 18 | + jest.useFakeTimers(); |
| 19 | + jest.setSystemTime(startTime); |
| 20 | + cacheProvider = new InMemoryCacheProvider(); |
| 21 | + }); |
| 22 | + |
| 23 | + afterEach(() => { |
| 24 | + jest.useRealTimers(); |
| 25 | + }); |
| 26 | + |
| 27 | + test('should return null when a key is not found', async () => { |
| 28 | + const result = await cacheProvider.get('nonExistentKey'); |
| 29 | + expect(result).toBeNull(); |
| 30 | + }); |
| 31 | + |
| 32 | + describe('Non-expiring keys (TTLBucket.Never)', () => { |
| 33 | + test('should cache and retrieve a non-expiring key', async () => { |
| 34 | + cacheProvider.set('nonExpiringKey', 'nonExpiringValue', TTLBucket.Never); |
| 35 | + let result = await cacheProvider.get('nonExpiringKey'); |
| 36 | + expect(result).toEqual('nonExpiringValue'); |
| 37 | + |
| 38 | + // Advance time by 10 minutes - the key should still be retrievable. |
| 39 | + await jest.advanceTimersByTimeAsync(10 * 60 * 1000); |
| 40 | + result = await cacheProvider.get('nonExpiringKey'); |
| 41 | + expect(result).toEqual('nonExpiringValue'); |
| 42 | + }); |
| 43 | + }); |
| 44 | + |
| 45 | + describe('TTL keys with TTLBucket.Short', () => { |
| 46 | + // For TTLBucket.Short, this implementation sets: |
| 47 | + // min TTL = 1 * 60 seconds (60 sec) and max TTL = 60 * 60 seconds (3600 sec). |
| 48 | + test('should cache and retrieve a TTL key and extend its TTL on get', async () => { |
| 49 | + cacheProvider.set('ttlKey', 'ttlValue', TTLBucket.Short); |
| 50 | + |
| 51 | + // Immediately get - the entry is found. |
| 52 | + let result = await cacheProvider.get('ttlKey'); |
| 53 | + expect(result).toEqual('ttlValue'); |
| 54 | + |
| 55 | + // Advance time by 30 seconds, well within the initial 60-sec TTL. |
| 56 | + await jest.advanceTimersByTimeAsync(30 * 1000); |
| 57 | + result = await cacheProvider.get('ttlKey'); |
| 58 | + expect(result).toEqual('ttlValue'); |
| 59 | + // Note: Each get extends the TTL to "now + 60 sec" (but never past the |
| 60 | + // absolute expiry of t0 + 3600 sec). |
| 61 | + |
| 62 | + // Advance time to nearly the maximum allowed expiry. |
| 63 | + // (Imagine we are nearly at startTime + 3600 seconds.) |
| 64 | + jest.setSystemTime(startTime + 3599 * 1000); |
| 65 | + result = await cacheProvider.get('ttlKey'); |
| 66 | + expect(result).toEqual('ttlValue'); |
| 67 | + |
| 68 | + // Now set the time beyond the maximum TTL boundary. |
| 69 | + jest.setSystemTime(startTime + 3601 * 1000); |
| 70 | + result = await cacheProvider.get('ttlKey'); |
| 71 | + expect(result).toBeNull(); |
| 72 | + }); |
| 73 | + |
| 74 | + test('should return null if a TTL key expires in the LRUCache (without a get extension)', async () => { |
| 75 | + cacheProvider.set('ttlNoAccess', 'valueNoAccess', TTLBucket.Short); |
| 76 | + // If we do not call get within the initial 60 seconds (min TTL), the key |
| 77 | + // should be removed by lru-cache. |
| 78 | + jest.setSystemTime(startTime + 61 * 1000); |
| 79 | + await jest.advanceTimersByTimeAsync(0); |
| 80 | + await new Promise((resolve) => |
| 81 | + jest.requireActual('timers').setImmediate(resolve), |
| 82 | + ); |
| 83 | + const result = await cacheProvider.get('ttlNoAccess'); |
| 84 | + expect(result).toBeNull(); |
| 85 | + }); |
| 86 | + }); |
| 87 | + |
| 88 | + describe('TTL keys with TTLBucket.Long', () => { |
| 89 | + // For TTLBucket.Long, our implementation sets: |
| 90 | + // min TTL = 30 * 60 seconds (1800 sec) and max TTL = 4 * 60 * 60 seconds (14400 sec). |
| 91 | + test('should cache and retrieve a Long TTL key and extend its TTL on get', async () => { |
| 92 | + cacheProvider.set('longTTLKey', 'longTTLValue', TTLBucket.Long); |
| 93 | + |
| 94 | + // Immediately get the value. |
| 95 | + let result = await cacheProvider.get('longTTLKey'); |
| 96 | + expect(result).toEqual('longTTLValue'); |
| 97 | + |
| 98 | + // Advance a short amount of time (e.g. 10 seconds) and the key should |
| 99 | + // still exist. |
| 100 | + await jest.advanceTimersByTimeAsync(10 * 1000); |
| 101 | + result = await cacheProvider.get('longTTLKey'); |
| 102 | + expect(result).toEqual('longTTLValue'); |
| 103 | + |
| 104 | + // Advance time to just before the maximum expiry (t0 + 14400 sec - 1 sec): |
| 105 | + jest.setSystemTime(startTime + (14400 - 1) * 1000); |
| 106 | + result = await cacheProvider.get('longTTLKey'); |
| 107 | + expect(result).toEqual('longTTLValue'); |
| 108 | + |
| 109 | + // Advance beyond the maximum TTL. |
| 110 | + jest.setSystemTime(startTime + 14400 * 1000 + 1); |
| 111 | + result = await cacheProvider.get('longTTLKey'); |
| 112 | + expect(result).toBeNull(); |
| 113 | + }); |
| 114 | + }); |
| 115 | + |
| 116 | + describe('Delete operation', () => { |
| 117 | + test('should delete a TTL key from the cache', async () => { |
| 118 | + cacheProvider.set('keyToDelete', 'deleteValue', TTLBucket.Short); |
| 119 | + let result = await cacheProvider.get('keyToDelete'); |
| 120 | + expect(result).toEqual('deleteValue'); |
| 121 | + |
| 122 | + // Delete the key. |
| 123 | + cacheProvider.delete('keyToDelete'); |
| 124 | + result = await cacheProvider.get('keyToDelete'); |
| 125 | + expect(result).toBeNull(); |
| 126 | + }); |
| 127 | + |
| 128 | + test('should delete a non-expiring key from the cache', async () => { |
| 129 | + cacheProvider.set('nonExpDelete', 'nonExpValue', TTLBucket.Never); |
| 130 | + let result = await cacheProvider.get('nonExpDelete'); |
| 131 | + expect(result).toEqual('nonExpValue'); |
| 132 | + |
| 133 | + // Delete the non-expiring key. |
| 134 | + cacheProvider.delete('nonExpDelete'); |
| 135 | + result = await cacheProvider.get('nonExpDelete'); |
| 136 | + expect(result).toBeNull(); |
| 137 | + }); |
| 138 | + }); |
| 139 | + |
| 140 | + describe('Overwriting existing keys', () => { |
| 141 | + test('should override an existing key when set is called again', async () => { |
| 142 | + // First, set a key with a TTL. |
| 143 | + cacheProvider.set('duplicateKey', 'initialValue', TTLBucket.Short); |
| 144 | + let result = await cacheProvider.get('duplicateKey'); |
| 145 | + expect(result).toEqual('initialValue'); |
| 146 | + |
| 147 | + // Now, override it with a new value (using a different TTL bucket, |
| 148 | + // e.g. non-expiring). |
| 149 | + cacheProvider.set('duplicateKey', 'newValue', TTLBucket.Never); |
| 150 | + result = await cacheProvider.get('duplicateKey'); |
| 151 | + expect(result).toEqual('newValue'); |
| 152 | + }); |
| 153 | + }); |
| 154 | +}); |
0 commit comments