/* * 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.core; import static org.hamcrest.core.IsCollectionContaining.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.IndexedData; import org.springframework.data.redis.core.convert.MappingRedisConverter; import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.core.convert.ReferenceResolver; import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl * @auhtor Rob Winch */ @RunWith(MockitoJUnitRunner.Silent.class) public class IndexWriterUnitTests { private static final Charset CHARSET = Charset.forName("UTF-8"); private static final String KEYSPACE = "persons"; private static final String KEY = "key-1"; private static final byte[] KEY_BIN = KEY.getBytes(CHARSET); IndexWriter writer; MappingRedisConverter converter; @Mock RedisConnection connectionMock; @Mock ReferenceResolver referenceResolverMock; @Before public void setUp() { converter = new MappingRedisConverter(new RedisMappingContext(), new PathIndexResolver(), referenceResolverMock); converter.afterPropertiesSet(); writer = new IndexWriter(connectionMock, converter); } @Test // DATAREDIS-425 public void addKeyToIndexShouldInvokeSaddCorrectly() { writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", "Rand")); verify(connectionMock).sAdd(eq("persons:firstname:Rand".getBytes(CHARSET)), eq(KEY_BIN)); verify(connectionMock).sAdd(eq("persons:key-1:idx".getBytes(CHARSET)), eq("persons:firstname:Rand".getBytes(CHARSET))); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-425 public void addKeyToIndexShouldThrowErrorWhenIndexedDataIsNull() { writer.addKeyToIndex(KEY_BIN, null); } @Test // DATAREDIS-425 public void removeKeyFromExistingIndexesShouldCheckForExistingIndexesForPath() { writer.removeKeyFromExistingIndexes(KEY_BIN, new StubIndxedData()); verify(connectionMock).keys(eq(("persons:address.city:*").getBytes(CHARSET))); verifyNoMoreInteractions(connectionMock); } @Test // DATAREDIS-425 public void removeKeyFromExistingIndexesShouldRemoveKeyFromAllExistingIndexesForPath() { byte[] indexKey1 = "persons:firstname:rand".getBytes(CHARSET); byte[] indexKey2 = "persons:firstname:mat".getBytes(CHARSET); when(connectionMock.keys(any(byte[].class))) .thenReturn(new LinkedHashSet<byte[]>(Arrays.asList(indexKey1, indexKey2))); writer.removeKeyFromExistingIndexes(KEY_BIN, new StubIndxedData()); verify(connectionMock).sRem(indexKey1, KEY_BIN); verify(connectionMock).sRem(indexKey2, KEY_BIN); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-425 public void removeKeyFromExistingIndexesShouldThrowExecptionForNullIndexedData() { writer.removeKeyFromExistingIndexes(KEY_BIN, null); } @Test // DATAREDIS-425 public void removeAllIndexesShouldDeleteAllIndexKeys() { byte[] indexKey1 = "persons:firstname:rand".getBytes(CHARSET); byte[] indexKey2 = "persons:firstname:mat".getBytes(CHARSET); when(connectionMock.keys(any(byte[].class))) .thenReturn(new LinkedHashSet<byte[]>(Arrays.asList(indexKey1, indexKey2))); writer.removeAllIndexes(KEYSPACE); ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); verify(connectionMock, times(1)).del(captor.capture()); assertThat(captor.getAllValues(), hasItems(indexKey1, indexKey2)); } @Test(expected = InvalidDataAccessApiUsageException.class) // DATAREDIS-425 public void addToIndexShouldThrowDataAccessExceptionWhenAddingDataThatConnotBeConverted() { writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", new DummyObject())); } @Test // DATAREDIS-425 public void addToIndexShouldUseRegisteredConverterWhenAddingData() { DummyObject value = new DummyObject(); final String identityHexString = ObjectUtils.getIdentityHexString(value); ((GenericConversionService) converter.getConversionService()).addConverter(new Converter<DummyObject, byte[]>() { @Override public byte[] convert(DummyObject source) { return identityHexString.getBytes(CHARSET); } }); writer.addKeyToIndex(KEY_BIN, new SimpleIndexedPropertyValue(KEYSPACE, "firstname", value)); verify(connectionMock).sAdd(eq(("persons:firstname:" + identityHexString).getBytes(CHARSET)), eq(KEY_BIN)); } @Test // DATAREDIS-512 public void createIndexShouldNotTryToRemoveExistingValues() { when(connectionMock.keys(any(byte[].class))) .thenReturn(new LinkedHashSet<byte[]>(Arrays.asList("persons:firstname:rand".getBytes(CHARSET)))); writer.createIndexes(KEY_BIN, Collections.<IndexedData> singleton(new SimpleIndexedPropertyValue(KEYSPACE, "firstname", "Rand"))); verify(connectionMock).sAdd(eq("persons:firstname:Rand".getBytes(CHARSET)), eq(KEY_BIN)); verify(connectionMock).sAdd(eq("persons:key-1:idx".getBytes(CHARSET)), eq("persons:firstname:Rand".getBytes(CHARSET))); verify(connectionMock, never()).sRem(any(byte[].class), eq(KEY_BIN)); } @Test // DATAREDIS-512 public void updateIndexShouldRemoveExistingValues() { when(connectionMock.keys(any(byte[].class))) .thenReturn(new LinkedHashSet<byte[]>(Arrays.asList("persons:firstname:rand".getBytes(CHARSET)))); writer.updateIndexes(KEY_BIN, Collections.<IndexedData> singleton(new SimpleIndexedPropertyValue(KEYSPACE, "firstname", "Rand"))); verify(connectionMock).sAdd(eq("persons:firstname:Rand".getBytes(CHARSET)), eq(KEY_BIN)); verify(connectionMock).sAdd(eq("persons:key-1:idx".getBytes(CHARSET)), eq("persons:firstname:Rand".getBytes(CHARSET))); verify(connectionMock, times(1)).sRem(any(byte[].class), eq(KEY_BIN)); } @Test // DATAREDIS-533 public void removeGeoIndexShouldCallGeoRemove() { byte[] indexKey1 = "persons:location".getBytes(CHARSET); when(connectionMock.keys(any(byte[].class))).thenReturn(new LinkedHashSet<byte[]>(Arrays.asList(indexKey1))); writer.removeKeyFromExistingIndexes(KEY_BIN, new GeoIndexedPropertyValue(KEYSPACE, "address.city", null)); verify(connectionMock).geoRemove(indexKey1, KEY_BIN); } static class StubIndxedData implements IndexedData { @Override public String getIndexName() { return "address.city"; } @Override public String getKeyspace() { return KEYSPACE; } } static class DummyObject { } }