/*
* Copyright 2015-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.cache;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.util.ClassUtils.*;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueRetrievalException;
import org.springframework.cache.support.NullValue;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@SuppressWarnings("rawtypes")
@RunWith(MockitoJUnitRunner.Silent.class)
public class RedisCacheUnitTests {
private static final String CACHE_NAME = "foo";
private static final String PREFIX = "prefix:";
private static final byte[] PREFIX_BYTES = "prefix:".getBytes();
private static final byte[] KNOWN_KEYS_SET_NAME_BYTES = (CACHE_NAME + "~keys").getBytes();
private static final String KEY = "key";
private static final byte[] KEY_BYTES = KEY.getBytes();
private static final byte[] KEY_WITH_PREFIX_BYTES = (PREFIX + KEY).getBytes();
private static final String VALUE = "value";
private static final byte[] VALUE_BYTES = VALUE.getBytes();
private static final byte[] NO_PREFIX_BYTES = new byte[] {};
private static final long EXPIRATION = 1000;
RedisTemplate<?, ?> templateSpy;
@Mock RedisSerializer keySerializerMock;
@Mock RedisSerializer valueSerializerMock;
@Mock RedisConnectionFactory connectionFactoryMock;
@Mock RedisConnection connectionMock;
RedisCache cache;
public @Rule ExpectedException exception = ExpectedException.none();
@SuppressWarnings("unchecked")
@Before
public void setUp() {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(connectionFactoryMock);
template.setKeySerializer(keySerializerMock);
template.setValueSerializer(valueSerializerMock);
template.afterPropertiesSet();
templateSpy = spy(template);
when(connectionFactoryMock.getConnection()).thenReturn(connectionMock);
when(keySerializerMock.serialize(any())).thenReturn(KEY_BYTES);
when(valueSerializerMock.serialize(any())).thenReturn(VALUE_BYTES);
when(valueSerializerMock.deserialize(eq(VALUE_BYTES))).thenReturn(VALUE);
}
@Test // DATAREDIS-369
public void putShouldNotKeepTrackOfKnownKeysWhenPrefixIsSet() {
cache = new RedisCache(CACHE_NAME, PREFIX_BYTES, templateSpy, EXPIRATION);
cache.put(KEY, VALUE);
verify(connectionMock).set(eq(KEY_WITH_PREFIX_BYTES), eq(VALUE_BYTES));
verify(connectionMock).expire(eq(KEY_WITH_PREFIX_BYTES), eq(EXPIRATION));
verify(connectionMock, never()).zAdd(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0D), any(byte[].class));
}
@Test // DATAREDIS-369
public void putShouldKeepTrackOfKnownKeysWhenNoPrefixIsSet() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, EXPIRATION);
cache.put(KEY, VALUE);
verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(connectionMock).expire(eq(KEY_BYTES), eq(EXPIRATION));
verify(connectionMock).zAdd(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0D), eq(KEY_BYTES));
}
@Test // DATAREDIS-369
public void clearShouldRemoveKeysUsingKnownKeysWhenNoPrefixIsSet() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, EXPIRATION);
cache.clear();
verify(connectionMock).zRange(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0L), eq(127L));
}
@Test // DATAREDIS-369
public void clearShouldCallLuaScriptToRemoveKeysWhenPrefixIsSet() {
cache = new RedisCache(CACHE_NAME, PREFIX_BYTES, templateSpy, EXPIRATION);
cache.clear();
verify(connectionMock).eval(any(byte[].class), eq(ReturnType.INTEGER), eq(0),
eq((PREFIX + "*").getBytes()));
}
@Test // DATAREDIS-402
public void putShouldNotExpireKnownKeysSetWhenTtlIsZero() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
cache.put(KEY, VALUE);
verify(connectionMock, never()).expire(eq(KNOWN_KEYS_SET_NAME_BYTES), anyLong());
}
@Test // DATAREDIS-542
public void putIfAbsentShouldExpireWhenValueWasSet() {
when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(true);
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
assertThat(valueWrapper, is(nullValue()));
verify(connectionMock).setNX(KEY_BYTES, VALUE_BYTES);
verify(connectionMock).expire(eq(KEY_BYTES), anyLong());
}
@Test // DATAREDIS-542
public void putIfAbsentShouldNotExpireWhenValueWasNotSetAndRedisContainsOtherData() {
String other = "other";
when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(false);
when(connectionMock.get(KEY_BYTES)).thenReturn(other.getBytes());
when(valueSerializerMock.deserialize(eq(other.getBytes()))).thenReturn(other);
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
assertThat(valueWrapper, is(notNullValue()));
verify(connectionMock, never()).expire(eq(KEY_BYTES), anyLong());
}
@Test // DATAREDIS-542
public void putIfAbsentShouldNotSetExpireWhenValueWasNotSetAndRedisContainsSameData() {
when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(false);
when(connectionMock.get(KEY_BYTES)).thenReturn(VALUE_BYTES);
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
assertThat(valueWrapper, is(notNullValue()));
verify(connectionMock, never()).expire(eq(KEY_BYTES), anyLong());
}
@Test // DATAREDIS-443
@SuppressWarnings("unchecked")
public void getWithCallable() throws ClassNotFoundException {
if (isPresent("org.springframework.cache.Cache$ValueRetrievalException", getDefaultClassLoader())) {
exception.expect((Class<? extends Throwable>) forName("org.springframework.cache.Cache$ValueRetrievalException",
getDefaultClassLoader()));
} else {
exception.expect(RedisSystemException.class);
}
exception.expectMessage("Value for key 'key' could not be loaded");
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
throw new UnsupportedOperationException("Expected exception");
}
});
}
@Test(expected = ValueRetrievalException.class) // DATAREDIS-553, DATAREDIS-606
@SuppressWarnings("unchecked")
public void getWithCallableShouldThrowExceptionSotringNullWhenNotAllowingNull() throws ClassNotFoundException {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L, false);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
}
@Test // DATAREDIS-553
@SuppressWarnings("unchecked")
public void getWithCallableShouldStoreNullAllowingNull() throws ClassNotFoundException {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L, true);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
verify(valueSerializerMock).serialize(isA(NullValue.class));
verify(connectionMock).get(eq(KEY_BYTES));
verify(connectionMock).multi();
verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(connectionMock).exec();
}
@Test // DATAREDIS-443, DATAREDIS-592
public void getWithCallableShouldReadValueFromCallableAddToCache() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
return VALUE;
}
});
verify(connectionMock).get(eq(KEY_BYTES));
verify(connectionMock).multi();
verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(connectionMock, never()).expire(any(byte[].class), anyLong());
verify(connectionMock).exec();
}
@Test // DATAREDIS-592
public void getWithCallableShouldReadValueFromCallableAddToCacheWithTtl() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 100L);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
return VALUE;
}
});
verify(connectionMock).get(eq(KEY_BYTES));
verify(connectionMock).multi();
verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(connectionMock).expire(eq(KEY_BYTES), eq(100L));
verify(connectionMock).exec();
}
@Test // DATAREDIS-443
@SuppressWarnings("unchecked")
public void getWithCallableShouldNotReadValueFromCallableWhenAlreadyPresent() {
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
Callable<Object> callableMock = mock(Callable.class);
when(connectionMock.exists(KEY_BYTES)).thenReturn(true);
when(connectionMock.get(KEY_BYTES)).thenReturn(VALUE_BYTES);
assertThat((String) cache.get(KEY, callableMock), equalTo(VALUE));
verifyZeroInteractions(callableMock);
}
@Test // DATAREDIS-468
public void noMultiExecForCluster() {
RedisClusterConnection clusterConnectionMock = mock(RedisClusterConnection.class);
when(connectionFactoryMock.getConnection()).thenReturn(clusterConnectionMock);
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
when(connectionMock.exists(KEY_BYTES)).thenReturn(true);
when(connectionMock.get(KEY_BYTES)).thenReturn(null).thenReturn(VALUE_BYTES);
cache.put(KEY, VALUE);
verify(clusterConnectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(clusterConnectionMock, never()).multi();
verify(clusterConnectionMock, never()).exec();
verifyZeroInteractions(connectionMock);
}
@Test // DATAREDIS-468
public void getWithCallableForCluster() {
RedisClusterConnection clusterConnectionMock = mock(RedisClusterConnection.class);
when(connectionFactoryMock.getConnection()).thenReturn(clusterConnectionMock);
cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
cache.get(KEY, new Callable<Object>() {
@Override
public Object call() throws Exception {
return VALUE;
}
});
verify(clusterConnectionMock).get(eq(KEY_BYTES));
verify(clusterConnectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(clusterConnectionMock, never()).multi();
verify(clusterConnectionMock, never()).exec();
verifyZeroInteractions(connectionMock);
}
}