@@ -8,6 +8,7 @@ import type { IConfigurationService } from '@/config/configuration.service.inter
8
8
import clearAllMocks = jest . clearAllMocks ;
9
9
import { redisClientFactory } from '@/__tests__/redis-client.factory' ;
10
10
import { MAX_TTL } from '@/datasources/cache/constants' ;
11
+ import { offsetByPercentage } from '@/domain/common/utils/number' ;
11
12
12
13
const mockLoggingService : jest . MockedObjectDeep < ILoggingService > = {
13
14
info : jest . fn ( ) ,
@@ -24,6 +25,8 @@ const mockConfigurationService = jest.mocked(configurationService);
24
25
describe ( 'RedisCacheService' , ( ) => {
25
26
let redisCacheService : RedisCacheService ;
26
27
let defaultExpirationTimeInSeconds : number ;
28
+ let defaultExpirationDeviatePercent : number ;
29
+ let maxTtlDeviated : number ;
27
30
const keyPrefix = '' ;
28
31
let redisClient : RedisClientType ;
29
32
@@ -39,10 +42,16 @@ describe('RedisCacheService', () => {
39
42
clearAllMocks ( ) ;
40
43
await redisClient . flushDb ( ) ;
41
44
defaultExpirationTimeInSeconds = faker . number . int ( { min : 1 , max : 3600 } ) ;
45
+ defaultExpirationDeviatePercent = faker . number . int ( { min : 1 , max : 99 } ) ;
46
+ maxTtlDeviated =
47
+ MAX_TTL - ( MAX_TTL * defaultExpirationDeviatePercent ) / 100 ;
42
48
mockConfigurationService . getOrThrow . mockImplementation ( ( key ) => {
43
49
if ( key === 'expirationTimeInSeconds.default' ) {
44
50
return defaultExpirationTimeInSeconds ;
45
51
}
52
+ if ( key === 'expirationTimeInSeconds.deviatePercent' ) {
53
+ return defaultExpirationDeviatePercent ;
54
+ }
46
55
throw Error ( `Unexpected key: ${ key } ` ) ;
47
56
} ) ;
48
57
@@ -75,7 +84,7 @@ describe('RedisCacheService', () => {
75
84
const value = fakeJson ( ) ;
76
85
const expireTime = faker . number . int ( ) ;
77
86
78
- await redisCacheService . hSet ( cacheDir , value , expireTime ) ;
87
+ await redisCacheService . hSet ( cacheDir , value , expireTime , 0 ) ;
79
88
80
89
const storedValue = await redisClient . hGet ( cacheDir . key , cacheDir . field ) ;
81
90
const ttl = await redisClient . ttl ( cacheDir . key ) ;
@@ -84,19 +93,48 @@ describe('RedisCacheService', () => {
84
93
expect ( ttl ) . toBeLessThanOrEqual ( expireTime ) ;
85
94
} ) ;
86
95
87
- it ( 'Setting key throws on expire ' , async ( ) => {
96
+ it ( 'Setting key with expireTimeSeconds and expireDeviatePercent does store the value with the deviated TTL ' , async ( ) => {
88
97
const cacheDir = new CacheDir (
89
98
faker . string . alphanumeric ( ) ,
90
99
faker . string . sample ( ) ,
91
100
) ;
101
+ const value = fakeJson ( ) ;
102
+ const expireTime = faker . number . int ( { min : 1 , max : 3600 } ) ;
103
+ const expireDeviatePercent = faker . number . int ( { min : 1 , max : 100 } ) ;
104
+ const maxDeviation = offsetByPercentage ( expireTime , expireDeviatePercent ) ;
105
+
106
+ await redisCacheService . hSet (
107
+ cacheDir ,
108
+ value ,
109
+ expireTime ,
110
+ expireDeviatePercent ,
111
+ ) ;
92
112
93
- // Expiration time out of range to force an error
94
- await expect (
95
- redisCacheService . hSet ( cacheDir , '' , Number . MAX_VALUE + 1 ) ,
96
- ) . rejects . toThrow ( ) ;
113
+ const storedValue = await redisClient . hGet ( cacheDir . key , cacheDir . field ) ;
114
+ const ttl = await redisClient . ttl ( cacheDir . key ) ;
115
+ expect ( storedValue ) . toEqual ( value ) ;
116
+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
117
+ expect ( ttl ) . toBeLessThanOrEqual ( maxDeviation ) ;
118
+ } ) ;
119
+
120
+ it ( 'Setting key with expireTimeSeconds and no expireDeviatePercent does store the value with the default TTL deviation' , async ( ) => {
121
+ const cacheDir = new CacheDir (
122
+ faker . string . alphanumeric ( ) ,
123
+ faker . string . sample ( ) ,
124
+ ) ;
125
+ const value = fakeJson ( ) ;
126
+ const expireTime = faker . number . int ( { min : 1 , max : 3600 } ) ;
127
+ const maxDeviation = offsetByPercentage (
128
+ expireTime ,
129
+ defaultExpirationDeviatePercent ,
130
+ ) ;
131
+ await redisCacheService . hSet ( cacheDir , value , expireTime ) ;
97
132
98
133
const storedValue = await redisClient . hGet ( cacheDir . key , cacheDir . field ) ;
99
- expect ( storedValue ) . toBeNull ( ) ;
134
+ const ttl = await redisClient . ttl ( cacheDir . key ) ;
135
+ expect ( storedValue ) . toEqual ( value ) ;
136
+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
137
+ expect ( ttl ) . toBeLessThanOrEqual ( maxDeviation ) ;
100
138
} ) ;
101
139
102
140
it ( 'Getting key gets the stored value' , async ( ) => {
@@ -148,19 +186,27 @@ describe('RedisCacheService', () => {
148
186
} ) ;
149
187
150
188
it ( 'creates a missing key and increments its value' , async ( ) => {
151
- const expireTime = faker . number . int ( { min : 1 } ) ;
189
+ const expireTime = faker . number . int ( { min : 1 , max : maxTtlDeviated } ) ;
190
+ const maxExpireTime = offsetByPercentage (
191
+ expireTime ,
192
+ defaultExpirationDeviatePercent ,
193
+ ) ;
152
194
const key = faker . string . alphanumeric ( ) ;
153
195
154
196
const firstResult = await redisCacheService . increment ( key , expireTime ) ;
155
197
156
198
const ttl = await redisClient . ttl ( key ) ;
157
199
expect ( firstResult ) . toEqual ( 1 ) ;
158
200
expect ( ttl ) . toBeGreaterThan ( 0 ) ;
159
- expect ( ttl ) . toBeLessThanOrEqual ( expireTime ) ;
201
+ expect ( ttl ) . toBeLessThanOrEqual ( maxExpireTime ) ;
160
202
} ) ;
161
203
162
204
it ( 'increments the value of an existing key' , async ( ) => {
163
- const expireTime = faker . number . int ( { min : 1 } ) ;
205
+ const expireTime = faker . number . int ( { min : 1 , max : maxTtlDeviated } ) ;
206
+ const maxExpireTime = offsetByPercentage (
207
+ expireTime ,
208
+ defaultExpirationDeviatePercent ,
209
+ ) ;
164
210
const key = faker . string . alphanumeric ( ) ;
165
211
const initialValue = faker . number . int ( { min : 100 } ) ;
166
212
await redisClient . set ( key , initialValue , { EX : expireTime } ) ;
@@ -172,21 +218,21 @@ describe('RedisCacheService', () => {
172
218
173
219
const ttl = await redisClient . ttl ( key ) ;
174
220
expect ( ttl ) . toBeGreaterThan ( 0 ) ;
175
- expect ( ttl ) . toBeLessThanOrEqual ( expireTime ) ;
221
+ expect ( ttl ) . toBeLessThanOrEqual ( maxExpireTime ) ;
176
222
} ) ;
177
223
178
224
it ( 'sets and gets the value of a counter key' , async ( ) => {
179
225
const key = faker . string . alphanumeric ( ) ;
180
226
const value = faker . number . int ( { min : 100 } ) ;
181
- await redisCacheService . setCounter ( key , value , MAX_TTL ) ;
227
+ await redisCacheService . setCounter ( key , value , maxTtlDeviated ) ;
182
228
183
229
const result = await redisCacheService . getCounter ( key ) ;
184
230
expect ( result ) . toEqual ( value ) ;
185
231
} ) ;
186
232
187
233
it ( 'sets and gets the value of a zero-value counter' , async ( ) => {
188
234
const key = faker . string . alphanumeric ( ) ;
189
- await redisCacheService . setCounter ( key , 0 , MAX_TTL ) ;
235
+ await redisCacheService . setCounter ( key , 0 , maxTtlDeviated ) ;
190
236
191
237
const result = await redisCacheService . getCounter ( key ) ;
192
238
expect ( result ) . toEqual ( 0 ) ;
@@ -225,7 +271,7 @@ describe('RedisCacheService', () => {
225
271
const value = faker . string . sample ( ) ;
226
272
227
273
try {
228
- await redisCacheService . hSet ( new CacheDir ( key , '' ) , value , MAX_TTL ) ;
274
+ await redisCacheService . hSet ( new CacheDir ( key , '' ) , value , MAX_TTL , 0 ) ;
229
275
} catch ( err ) {
230
276
console . error ( err ) ;
231
277
throw new Error ( 'Should not throw' ) ;
@@ -237,4 +283,44 @@ describe('RedisCacheService', () => {
237
283
expect ( ttl ) . toBeGreaterThan ( 0 ) ;
238
284
expect ( ttl ) . toBeLessThanOrEqual ( Number . MAX_SAFE_INTEGER ) ;
239
285
} ) ;
286
+
287
+ it ( 'Setting key with TTL larger than MAX_TTL enforces MAX_TTL limit' , async ( ) => {
288
+ const cacheDir = new CacheDir (
289
+ faker . string . alphanumeric ( ) ,
290
+ faker . string . sample ( ) ,
291
+ ) ;
292
+ const value = fakeJson ( ) ;
293
+ const expireTime = MAX_TTL + faker . number . int ( { min : 1000 , max : 10000 } ) ;
294
+
295
+ await redisCacheService . hSet ( cacheDir , value , expireTime , 0 ) ;
296
+
297
+ const storedValue = await redisClient . hGet ( cacheDir . key , cacheDir . field ) ;
298
+ const ttl = await redisClient . ttl ( cacheDir . key ) ;
299
+ expect ( storedValue ) . toEqual ( value ) ;
300
+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
301
+ expect ( ttl ) . toBeLessThanOrEqual ( MAX_TTL ) ;
302
+ } ) ;
303
+
304
+ it ( 'Increment with TTL larger than MAX_TTL enforces MAX_TTL limit' , async ( ) => {
305
+ const key = faker . string . alphanumeric ( ) ;
306
+ const expireTime = MAX_TTL + faker . number . int ( { min : 1000 , max : 10000 } ) ;
307
+
308
+ await redisCacheService . increment ( key , expireTime , 0 ) ;
309
+
310
+ const ttl = await redisClient . ttl ( key ) ;
311
+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
312
+ expect ( ttl ) . toBeLessThanOrEqual ( MAX_TTL ) ;
313
+ } ) ;
314
+
315
+ it ( 'SetCounter with TTL larger than MAX_TTL enforces MAX_TTL limit' , async ( ) => {
316
+ const key = faker . string . alphanumeric ( ) ;
317
+ const value = faker . number . int ( { min : 1 , max : 100 } ) ;
318
+ const expireTime = MAX_TTL + faker . number . int ( { min : 1000 , max : 10000 } ) ;
319
+
320
+ await redisCacheService . setCounter ( key , value , expireTime , 0 ) ;
321
+
322
+ const ttl = await redisClient . ttl ( key ) ;
323
+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
324
+ expect ( ttl ) . toBeLessThanOrEqual ( MAX_TTL ) ;
325
+ } ) ;
240
326
} ) ;
0 commit comments