SimpleKeyValueRepositoryUnitTests.java

/*
 * Copyright 2014-2025 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
 *
 *      https://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.repository;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.support.PersistentEntityInformation;

/**
 * @author Christoph Strobl
 * @author Eugene Nikiforov
 * @author Jens Schauder
 * @author Mark Paluch
 */
@ExtendWith(MockitoExtension.class)
class SimpleKeyValueRepositoryUnitTests {

	private SimpleKeyValueRepository<Foo, String> repo;
	private @Mock KeyValueOperations opsMock;
	private KeyValueMappingContext<?, ?> context;

	@BeforeEach
	void setUp() {

		this.context = new KeyValueMappingContext<>();

		EntityInformation<Foo, String> ei = getEntityInformationFor(Foo.class);
		repo = new SimpleKeyValueRepository<>(ei, opsMock);
	}

	@Test // DATACMNS-525
	void saveNewWithNumericId() {

		EntityInformation<WithNumericId, ?> ei = getEntityInformationFor(WithNumericId.class);
		SimpleKeyValueRepository<WithNumericId, ?> temp = new SimpleKeyValueRepository<>(ei, opsMock);

		WithNumericId withNumericId = new WithNumericId();
		temp.save(withNumericId);

		verify(opsMock, times(1)).insert(eq(withNumericId));
	}

	@Test // DATACMNS-525
	void testDoubleSave() {

		Foo foo = new Foo("one");

		repo.save(foo);

		foo.id = "1";
		repo.save(foo);
		verify(opsMock, times(1)).insert(eq(foo));
		verify(opsMock, times(1)).update(eq(foo.getId()), eq(foo));
	}

	@Test // DATACMNS-525
	void multipleSave() {

		Foo one = new Foo("one");
		Foo two = new Foo("two");

		repo.saveAll(Arrays.asList(one, two));
		verify(opsMock, times(1)).insert(eq(one));
		verify(opsMock, times(1)).insert(eq(two));
	}

	@Test // DATACMNS-525
	void deleteEntity() {

		Foo one = new Foo("one");
		one.id = "1";
		repo.save(one);
		repo.delete(one);

		verify(opsMock, times(1)).delete(eq(one));
	}

	@Test // DATACMNS-525
	void deleteById() {

		repo.deleteById("one");

		verify(opsMock, times(1)).delete(eq("one"), eq(Foo.class));
	}

	@Test // DATAKV-330
	void deleteAllById() {

		repo.deleteAllById(Arrays.asList("one", "two"));

		verify(opsMock, times(1)).delete(eq("one"), eq(Foo.class));
		verify(opsMock, times(1)).delete(eq("two"), eq(Foo.class));
	}

	@Test // DATACMNS-525
	void deleteAll() {

		repo.deleteAll();

		verify(opsMock, times(1)).delete(eq(Foo.class));
	}

	@Test // DATACMNS-525
	@SuppressWarnings("unchecked")
	void findAllIds() {

		when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.empty());
		repo.findAllById(Arrays.asList("one", "two", "three"));

		verify(opsMock, times(3)).findById(anyString(), eq(Foo.class));
	}

	@Test // DATAKV-186
	@SuppressWarnings("unchecked")
	void existsByIdReturnsFalseForEmptyOptional() {

		when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.empty());
		assertThat(repo.existsById("one")).isFalse();
	}

	@Test // DATAKV-186
	@SuppressWarnings("unchecked")
	void existsByIdReturnsTrueWhenOptionalValuePresent() {

		when(opsMock.findById(any(), any(Class.class))).thenReturn(Optional.of(new Foo()));
		assertThat(repo.existsById("one")).isTrue();
	}

	@Test // DATACMNS-525
	void findAllWithPageableShouldDelegateToOperationsCorrectlyWhenPageableDoesNotContainSort() {

		repo.findAll(PageRequest.of(10, 15));

		verify(opsMock, times(1)).findInRange(eq(150L), eq(15), eq(Sort.unsorted()), eq(Foo.class));
	}

	@Test // DATACMNS-525
	void findAllWithPageableShouldDelegateToOperationsCorrectlyWhenPageableContainsSort() {

		Sort sort = Sort.by("for", "bar");
		repo.findAll(PageRequest.of(10, 15, sort));

		verify(opsMock, times(1)).findInRange(eq(150L), eq(15), eq(sort), eq(Foo.class));
	}

	@Test // DATACMNS-525
	void findAllShouldFallbackToFindAllOfWhenGivenNullPageable() {

		repo.findAll(Pageable.unpaged());

		verify(opsMock, times(1)).findAll(eq(Foo.class));
	}

	@SuppressWarnings("unchecked")
	private <T, S> EntityInformation<T, S> getEntityInformationFor(Class<T> type) {

		PersistentEntity<T, ?> requiredPersistentEntity = (PersistentEntity<T, ?>) context
				.getRequiredPersistentEntity(type);

		return new PersistentEntityInformation<>(requiredPersistentEntity);
	}

	static class Foo {

		private @Id String id;
		private Long longValue;
		private String name;
		private Bar bar;

		Foo(String name) {
			this.name = name;
		}

		public Foo() {}

		public String getId() {
			return this.id;
		}

		public Long getLongValue() {
			return this.longValue;
		}

		public String getName() {
			return this.name;
		}

		public Bar getBar() {
			return this.bar;
		}

		public void setId(String id) {
			this.id = id;
		}

		public void setLongValue(Long longValue) {
			this.longValue = longValue;
		}

		public void setName(String name) {
			this.name = name;
		}

		public void setBar(Bar bar) {
			this.bar = bar;
		}
	}

	private static class Bar {

		private String bar;

		public String getBar() {
			return this.bar;
		}

		public void setBar(String bar) {
			this.bar = bar;
		}
	}

	@Persistent
	static class WithNumericId {

		@Id Integer id;
	}
}