/*
* Copyright 2014-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.keyvalue.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
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.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.keyvalue.SubclassOfTypeWithCustomComposedKeySpaceAnnotation;
import org.springframework.data.keyvalue.TypeWithCustomComposedKeySpaceAnnotation;
import org.springframework.data.keyvalue.TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor;
import org.springframework.data.keyvalue.core.event.KeyValueEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterDeleteEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterDropKeySpaceEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterGetEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterInsertEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.AfterUpdateEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeDeleteEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeGetEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeInsertEvent;
import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeUpdateEvent;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @author Thomas Darimont
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.Silent.class)
public class KeyValueTemplateUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
private static final Foo FOO_ONE = new Foo("one");
private static final Foo FOO_TWO = new Foo("two");
private static final TypeWithCustomComposedKeySpaceAnnotation ALIASED = new TypeWithCustomComposedKeySpaceAnnotation(
"super");
private static final TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor ALIASED_USING_ALIAS_FOR = new TypeWithCustomComposedKeySpaceAnnotationUsingAliasFor(
"super");
private static final SubclassOfTypeWithCustomComposedKeySpaceAnnotation SUBCLASS_OF_ALIASED = new SubclassOfTypeWithCustomComposedKeySpaceAnnotation(
"sub");
private static final KeyValueQuery<String> STRING_QUERY = new KeyValueQuery<>("foo == 'two'");
private @Mock KeyValueAdapter adapterMock;
private KeyValueTemplate template;
private @Mock ApplicationEventPublisher publisherMock;
@Before
public void setUp() throws InstantiationException, IllegalAccessException {
this.template = new KeyValueTemplate(adapterMock);
this.template.setApplicationEventPublisher(publisherMock);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void shouldThrowExceptionWhenCreatingNewTempateWithNullAdapter() {
new KeyValueTemplate(null);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void shouldThrowExceptionWhenCreatingNewTempateWithNullMappingContext() {
new KeyValueTemplate(adapterMock, null);
}
@Test // DATACMNS-525
public void insertShouldLookUpValuesBeforeInserting() {
template.insert("1", FOO_ONE);
verify(adapterMock, times(1)).contains("1", Foo.class.getName());
}
@Test // DATACMNS-525
public void insertShouldInsertUseClassNameAsDefaultKeyspace() {
template.insert("1", FOO_ONE);
verify(adapterMock, times(1)).put("1", FOO_ONE, Foo.class.getName());
}
@Test // DATACMNS-525
public void insertShouldThrowExceptionWhenObectWithIdAlreadyExists() {
exception.expect(DuplicateKeyException.class);
exception.expectMessage("id 1");
when(adapterMock.contains(anyString(), anyString())).thenReturn(true);
template.insert("1", FOO_ONE);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void insertShouldThrowExceptionForNullId() {
template.insert(null, FOO_ONE);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void insertShouldThrowExceptionForNullObject() {
template.insert("some-id", null);
}
@Test // DATACMNS-525
public void insertShouldGenerateId() {
ClassWithStringId target = template.insert(new ClassWithStringId());
assertThat(target.id, notNullValue());
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void insertShouldThrowErrorWhenIdCannotBeResolved() {
template.insert(FOO_ONE);
}
@Test // DATACMNS-525
public void insertShouldReturnSameInstanceGenerateId() {
ClassWithStringId source = new ClassWithStringId();
ClassWithStringId target = template.insert(source);
assertThat(target, sameInstance(source));
}
@Test // DATACMNS-525
public void insertShouldRespectExistingId() {
ClassWithStringId source = new ClassWithStringId();
source.id = "one";
template.insert(source);
verify(adapterMock, times(1)).put("one", source, ClassWithStringId.class.getName());
}
@Test // DATACMNS-525
public void findByIdShouldReturnOptionalEmptyWhenNoElementsPresent() {
assertThat(template.findById("1", Foo.class), is(Optional.empty()));
}
@Test // DATACMNS-525
public void findByIdShouldReturnObjectWithMatchingIdAndType() {
template.findById("1", Foo.class);
verify(adapterMock, times(1)).get("1", Foo.class.getName(), Foo.class);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void findByIdShouldThrowExceptionWhenGivenNullId() {
template.findById((Serializable) null, Foo.class);
}
@Test // DATACMNS-525
public void findAllOfShouldReturnEntireCollection() {
template.findAll(Foo.class);
verify(adapterMock, times(1)).getAllOf(Foo.class.getName());
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void findAllOfShouldThrowExceptionWhenGivenNullType() {
template.findAll(null);
}
@Test // DATACMNS-525
public void findShouldCallFindOnAdapterToResolveMatching() {
template.find(STRING_QUERY, Foo.class);
verify(adapterMock, times(1)).find(STRING_QUERY, Foo.class.getName(), Foo.class);
}
@Test // DATACMNS-525
@SuppressWarnings("rawtypes")
public void findInRangeShouldRespectOffset() {
ArgumentCaptor<KeyValueQuery> captor = ArgumentCaptor.forClass(KeyValueQuery.class);
template.findInRange(1, 5, Foo.class);
verify(adapterMock, times(1)).find(captor.capture(), eq(Foo.class.getName()), eq(Foo.class));
assertThat(captor.getValue().getOffset(), is(1L));
assertThat(captor.getValue().getRows(), is(5));
assertThat(captor.getValue().getCriteria(), nullValue());
}
@Test // DATACMNS-525
public void updateShouldReplaceExistingObject() {
template.update("1", FOO_TWO);
verify(adapterMock, times(1)).put("1", FOO_TWO, Foo.class.getName());
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void updateShouldThrowExceptionWhenGivenNullId() {
template.update(null, FOO_ONE);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void updateShouldThrowExceptionWhenGivenNullObject() {
template.update("1", null);
}
@Test // DATACMNS-525
public void updateShouldUseExtractedIdInformation() {
ClassWithStringId source = new ClassWithStringId();
source.id = "some-id";
template.update(source);
verify(adapterMock, times(1)).put(source.id, source, ClassWithStringId.class.getName());
}
@Test(expected = InvalidDataAccessApiUsageException.class) // DATACMNS-525
public void updateShouldThrowErrorWhenIdInformationCannotBeExtracted() {
template.update(FOO_ONE);
}
@Test // DATACMNS-525
public void deleteShouldRemoveObjectCorrectly() {
template.delete("1", Foo.class);
verify(adapterMock, times(1)).delete("1", Foo.class.getName(), Foo.class);
}
@Test // DATACMNS-525
public void deleteRemovesObjectUsingExtractedId() {
ClassWithStringId source = new ClassWithStringId();
source.id = "some-id";
template.delete(source);
verify(adapterMock, times(1)).delete("some-id", ClassWithStringId.class.getName(), ClassWithStringId.class);
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void deleteThrowsExceptionWhenIdCannotBeExctracted() {
template.delete(FOO_ONE);
}
@Test // DATACMNS-525
public void countShouldReturnZeroWhenNoElementsPresent() {
template.count(Foo.class);
}
@Test // DATACMNS-525
public void countShouldReturnCollectionSize() {
when(adapterMock.count(Foo.class.getName())).thenReturn(2L);
assertThat(template.count(Foo.class), is(2L));
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void countShouldThrowErrorOnNullType() {
template.count(null);
}
@Test // DATACMNS-525
public void insertShouldRespectTypeAlias() {
template.insert("1", ALIASED);
verify(adapterMock, times(1)).put("1", ALIASED, "aliased");
}
@Test // DATACMNS-525
public void insertShouldRespectTypeAliasOnSubClass() {
template.insert("1", SUBCLASS_OF_ALIASED);
verify(adapterMock, times(1)).put("1", SUBCLASS_OF_ALIASED, "aliased");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test // DATACMNS-525
public void findAllOfShouldRespectTypeAliasAndFilterNonMatchingTypes() {
Collection foo = Arrays.asList(ALIASED, SUBCLASS_OF_ALIASED);
when(adapterMock.getAllOf("aliased")).thenReturn(foo);
assertThat(template.findAll(SUBCLASS_OF_ALIASED.getClass()), containsInAnyOrder(SUBCLASS_OF_ALIASED));
}
@Test // DATACMNS-525
public void insertSouldRespectTypeAliasAndFilterNonMatching() {
template.insert("1", ALIASED);
assertThat(template.findById("1", SUBCLASS_OF_ALIASED.getClass()), is(Optional.empty()));
}
@Test(expected = IllegalArgumentException.class) // DATACMNS-525
public void setttingNullPersistenceExceptionTranslatorShouldThrowException() {
template.setExceptionTranslator(null);
}
@Test // DATAKV-91
public void shouldNotPublishEventWhenNoApplicationContextSet() {
template.setApplicationEventPublisher(null);
template.insert("1", FOO_ONE);
verifyZeroInteractions(publisherMock);
}
@Test // DATAKV-104
public void shouldNotPublishEventsWhenEventsToPublishIsSetToNull() {
template.setEventTypesToPublish(null);
template.insert("1", FOO_ONE);
verifyZeroInteractions(publisherMock);
}
@Test // DATAKV-104
@SuppressWarnings("rawtypes")
public void shouldNotPublishEventsWhenEventsToPublishIsSetToEmptyList() {
template.setEventTypesToPublish(Collections.<Class<? extends KeyValueEvent>> emptySet());
template.insert("1", FOO_ONE);
verifyZeroInteractions(publisherMock);
}
@Test // DATAKV-104
public void shouldPublishEventsByDefault() {
template.insert("1", FOO_ONE);
verify(publisherMock, atLeastOnce()).publishEvent(Matchers.any(KeyValueEvent.class));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "unchecked", })
public void shouldNotPublishEventWhenNotExplicitlySetForPublication() {
setEventsToPublish(BeforeDeleteEvent.class);
template.insert("1", FOO_ONE);
verifyZeroInteractions(publisherMock);
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "unchecked", "rawtypes" })
public void shouldPublishBeforeInsertEventCorrectly() {
setEventsToPublish(BeforeInsertEvent.class);
template.insert("1", FOO_ONE);
ArgumentCaptor<BeforeInsertEvent> captor = ArgumentCaptor.forClass(BeforeInsertEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "unchecked", "rawtypes" })
public void shouldPublishAfterInsertEventCorrectly() {
setEventsToPublish(AfterInsertEvent.class);
template.insert("1", FOO_ONE);
ArgumentCaptor<AfterInsertEvent> captor = ArgumentCaptor.forClass(AfterInsertEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "unchecked", "rawtypes" })
public void shouldPublishBeforeUpdateEventCorrectly() {
setEventsToPublish(BeforeUpdateEvent.class);
template.update("1", FOO_ONE);
ArgumentCaptor<BeforeUpdateEvent> captor = ArgumentCaptor.forClass(BeforeUpdateEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "unchecked", "rawtypes" })
public void shouldPublishAfterUpdateEventCorrectly() {
setEventsToPublish(AfterUpdateEvent.class);
template.update("1", FOO_ONE);
ArgumentCaptor<AfterUpdateEvent> captor = ArgumentCaptor.forClass(AfterUpdateEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "rawtypes", "unchecked" })
public void shouldPublishBeforeDeleteEventCorrectly() {
setEventsToPublish(BeforeDeleteEvent.class);
template.delete("1", FOO_ONE.getClass());
ArgumentCaptor<BeforeDeleteEvent> captor = ArgumentCaptor.forClass(BeforeDeleteEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "rawtypes", "unchecked" })
public void shouldPublishAfterDeleteEventCorrectly() {
setEventsToPublish(AfterDeleteEvent.class);
when(adapterMock.delete(eq("1"), eq(FOO_ONE.getClass().getName()), eq(Foo.class))).thenReturn(FOO_ONE);
template.delete("1", FOO_ONE.getClass());
ArgumentCaptor<AfterDeleteEvent> captor = ArgumentCaptor.forClass(AfterDeleteEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "rawtypes", "unchecked" })
public void shouldPublishBeforeGetEventCorrectly() {
setEventsToPublish(BeforeGetEvent.class);
when(adapterMock.get(eq("1"), eq(FOO_ONE.getClass().getName()))).thenReturn(FOO_ONE);
template.findById("1", FOO_ONE.getClass());
ArgumentCaptor<BeforeGetEvent> captor = ArgumentCaptor.forClass(BeforeGetEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "rawtypes", "unchecked" })
public void shouldPublishAfterGetEventCorrectly() {
setEventsToPublish(AfterGetEvent.class);
when(adapterMock.get(eq("1"), eq(FOO_ONE.getClass().getName()), eq(Foo.class))).thenReturn(FOO_ONE);
template.findById("1", FOO_ONE.getClass());
ArgumentCaptor<AfterGetEvent> captor = ArgumentCaptor.forClass(AfterGetEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKey(), is((Serializable) "1"));
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
assertThat(captor.getValue().getPayload(), is((Object) FOO_ONE));
}
@Test // DATAKV-91, DATAKV-104
@SuppressWarnings({ "rawtypes", "unchecked" })
public void shouldPublishDropKeyspaceEventCorrectly() {
setEventsToPublish(AfterDropKeySpaceEvent.class);
template.delete(FOO_ONE.getClass());
ArgumentCaptor<AfterDropKeySpaceEvent> captor = ArgumentCaptor.forClass(AfterDropKeySpaceEvent.class);
verify(publisherMock, times(1)).publishEvent(captor.capture());
verifyNoMoreInteractions(publisherMock);
assertThat(captor.getValue().getKeyspace(), is(Foo.class.getName()));
}
@Test // DATAKV-129
public void insertShouldRespectTypeAliasUsingAliasFor() {
template.insert("1", ALIASED_USING_ALIAS_FOR);
verify(adapterMock, times(1)).put("1", ALIASED_USING_ALIAS_FOR, "aliased");
}
@SuppressWarnings("rawtypes")
private void setEventsToPublish(Class<? extends KeyValueEvent>... events) {
template.setEventTypesToPublish(new HashSet<>(Arrays.asList(events)));
}
static class Foo {
String foo;
public Foo(String foo) {
this.foo = foo;
}
public String getFoo() {
return foo;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.foo);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Foo)) {
return false;
}
Foo other = (Foo) obj;
return ObjectUtils.nullSafeEquals(this.foo, other.foo);
}
}
class Bar {
String bar;
public Bar(String bar) {
this.bar = bar;
}
public String getBar() {
return bar;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.bar);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Bar)) {
return false;
}
Bar other = (Bar) obj;
return ObjectUtils.nullSafeEquals(this.bar, other.bar);
}
}
static class ClassWithStringId {
@Id String id;
String value;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.id);
result = prime * result + ObjectUtils.nullSafeHashCode(this.value);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ClassWithStringId)) {
return false;
}
ClassWithStringId other = (ClassWithStringId) obj;
if (!ObjectUtils.nullSafeEquals(this.id, other.id)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(this.value, other.value)) {
return false;
}
return true;
}
}
}