package com.njcn.middle.cache.template; import com.alibaba.fastjson.JSON; import com.njcn.middle.cache.autoconfig.RedisCacheProperties; import com.njcn.middle.cache.constant.CacheConstant; import com.njcn.middle.cache.support.CacheKeyBuilder; import com.njcn.middle.cache.support.CacheValueCodec; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class RedisCacheEnhanceTemplateTest { @Mock StringRedisTemplate redis; @Mock ValueOperations valueOps; CacheValueCodec codec = new CacheValueCodec(); CacheKeyBuilder keyBuilder = new CacheKeyBuilder("cache:", false, ""); RedisCacheProperties props = new RedisCacheProperties(); RedisCacheEnhanceTemplate template; @Data @AllArgsConstructor @NoArgsConstructor static class User { private Long id; private String name; } @BeforeEach void init() { MockitoAnnotations.initMocks(this); lenient().when(redis.opsForValue()).thenReturn(valueOps); template = new RedisCacheEnhanceTemplate(redis, keyBuilder, codec, props); } @Test void setSerializesWithExactTtlWhenNoJitter() { props.setJitterRatio(0); User u = new User(1L, "a"); template.set("user:1", u, 100); verify(valueOps).set("cache:user:1", JSON.toJSONString(u), 100L, TimeUnit.SECONDS); } @Test void getHitDeserializes() { when(valueOps.get("cache:user:1")).thenReturn(JSON.toJSONString(new User(1L, "a"))); User got = template.get("user:1", User.class); assertEquals("a", got.getName()); } @Test void getMissReturnsNull() { when(valueOps.get("cache:user:1")).thenReturn(null); assertNull(template.get("user:1", User.class)); } @Test void getNullSentinelReturnsNull() { when(valueOps.get("cache:user:1")).thenReturn(CacheConstant.NULL_SENTINEL); assertNull(template.get("user:1", User.class)); } @Test void getDirtyDataThrowsIllegalState() { when(valueOps.get("cache:user:1")).thenReturn("{not-json"); assertThrows(IllegalStateException.class, () -> template.get("user:1", User.class)); } @Test void expireDelegatesWithJitterTtl() { props.setJitterRatio(0); when(redis.expire("cache:user:1", 200L, TimeUnit.SECONDS)).thenReturn(true); assertTrue(template.expire("user:1", 200)); verify(redis).expire("cache:user:1", 200L, TimeUnit.SECONDS); } @Test void applyJitterWithinBounds() { props.setJitterRatio(0.1); long bound = (long) (100 * 0.1); for (int i = 0; i < 50; i++) { long ttl = template.applyJitter(100); assertTrue(ttl >= 100 && ttl <= 100 + bound, "ttl=" + ttl); } props.setJitterRatio(0); assertEquals(100, template.applyJitter(100)); } @Test void slidingOffDoesNotRenewOnGet() { when(valueOps.get("cache:user:1")).thenReturn(JSON.toJSONString(new User(1L, "a"))); template.get("user:1", User.class); verify(redis, never()).expire(any(), anyLong(), any()); } @Test void slidingOnRenewsRealValueOnGet() { props.setSlidingExpireEnabled(true); props.setJitterRatio(0); when(valueOps.get("cache:user:1")).thenReturn(JSON.toJSONString(new User(1L, "a"))); when(redis.expire(eq("cache:user:1"), anyLong(), eq(TimeUnit.SECONDS))).thenReturn(true); template.get("user:1", User.class); verify(redis).expire("cache:user:1", props.getDefaultTtlSeconds(), TimeUnit.SECONDS); } @Test void slidingOnDoesNotRenewNullSentinel() { props.setSlidingExpireEnabled(true); when(valueOps.get("cache:user:1")).thenReturn(CacheConstant.NULL_SENTINEL); assertNull(template.get("user:1", User.class)); verify(redis, never()).expire(any(), anyLong(), any()); } }