diff --git a/pom.xml b/pom.xml
index 2e70a16..c0a17b8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,6 +91,11 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.security
+ spring-security-test
+ test
+
io.jsonwebtoken
jjwt-api
diff --git a/src/main/java/com/senkiv/bookstore/service/impl/BookServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/BookServiceImpl.java
index 12d4d86..955f4e5 100644
--- a/src/main/java/com/senkiv/bookstore/service/impl/BookServiceImpl.java
+++ b/src/main/java/com/senkiv/bookstore/service/impl/BookServiceImpl.java
@@ -40,7 +40,6 @@ public BookDto save(CreateBookRequestDto bookDto) {
public Page findAll(Pageable pageable) {
return bookRepository.findAll(pageable)
.map(mapper::toDto);
-
}
@Override
diff --git a/src/test/java/com/senkiv/bookstore/controller/BookControllerTest.java b/src/test/java/com/senkiv/bookstore/controller/BookControllerTest.java
new file mode 100644
index 0000000..a3df5f8
--- /dev/null
+++ b/src/test/java/com/senkiv/bookstore/controller/BookControllerTest.java
@@ -0,0 +1,181 @@
+package com.senkiv.bookstore.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.senkiv.bookstore.dto.BookDto;
+import com.senkiv.bookstore.dto.BookSearchParametersDto;
+import com.senkiv.bookstore.dto.CreateBookRequestDto;
+import com.senkiv.bookstore.service.BookService;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class BookControllerTest {
+ @Autowired
+ private MockMvc mockMvc;
+ @Autowired
+ private ObjectMapper objectMapper;
+ @MockitoBean
+ private BookService bookService;
+ private BookDto bookDto;
+ private CreateBookRequestDto createBookRequestDto;
+ private Set categoryIds;
+ private Pageable pageable;
+
+ @BeforeEach
+ void setUp() {
+ categoryIds = Set.of(1L, 2L);
+ bookDto = new BookDto();
+ bookDto.setId(1L);
+ bookDto.setTitle("Test Book");
+ bookDto.setAuthor("Test Author");
+ bookDto.setIsbn("978-3-16-148410-0");
+ bookDto.setPrice(new BigDecimal("29.99"));
+ bookDto.setDescription("Test Description");
+ bookDto.setCoverImage("test-cover.jpg");
+ bookDto.setCategories(categoryIds);
+ createBookRequestDto = new CreateBookRequestDto(
+ "Test Book",
+ "Test Author",
+ "978-3-16-148410-0",
+ new BigDecimal("29.99"),
+ "Test Description",
+ "test-cover.jpg",
+ categoryIds
+ );
+ pageable = PageRequest.of(0, 10);
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Get all books should return page of book DTOs")
+ void getAll_ReturnsPageOfBookDto() throws Exception {
+ List books = List.of(bookDto);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ when(bookService.findAll(any(Pageable.class))).thenReturn(bookPage);
+
+ mockMvc.perform(get("/books")
+ .param("page", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].id").value(bookDto.getId()))
+ .andExpect(jsonPath("$.content[0].title").value(bookDto.getTitle()))
+ .andExpect(jsonPath("$.content[0].author").value(bookDto.getAuthor()))
+ .andExpect(jsonPath("$.content[0].isbn").value(bookDto.getIsbn()))
+ .andExpect(jsonPath("$.totalElements").value(1));
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Get book by ID should return book DTO")
+ void getBookById_ReturnsBookDto() throws Exception {
+ when(bookService.findById(1L)).thenReturn(bookDto);
+
+ mockMvc.perform(get("/books/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(bookDto.getId()))
+ .andExpect(jsonPath("$.title").value(bookDto.getTitle()))
+ .andExpect(jsonPath("$.author").value(bookDto.getAuthor()))
+ .andExpect(jsonPath("$.isbn").value(bookDto.getIsbn()));
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Create book should return created book DTO")
+ void createBook_ReturnsCreatedBookDto() throws Exception {
+ when(bookService.save(any(CreateBookRequestDto.class))).thenReturn(bookDto);
+
+ mockMvc.perform(post("/books")
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(createBookRequestDto)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id").value(bookDto.getId()))
+ .andExpect(jsonPath("$.title").value(bookDto.getTitle()))
+ .andExpect(jsonPath("$.author").value(bookDto.getAuthor()))
+ .andExpect(jsonPath("$.isbn").value(bookDto.getIsbn()));
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Update book should return updated book DTO")
+ void updateBook_ReturnsUpdatedBookDto() throws Exception {
+ when(bookService.updateById(eq(1L), any(CreateBookRequestDto.class))).thenReturn(bookDto);
+
+ mockMvc.perform(put("/books/1")
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(createBookRequestDto)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(bookDto.getId()))
+ .andExpect(jsonPath("$.title").value(bookDto.getTitle()))
+ .andExpect(jsonPath("$.author").value(bookDto.getAuthor()))
+ .andExpect(jsonPath("$.isbn").value(bookDto.getIsbn()));
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Delete book should return no content")
+ void deleteBook_ReturnsNoContent() throws Exception {
+ doNothing().when(bookService).deleteById(1L);
+
+ mockMvc.perform(delete("/books/1")
+ .with(csrf()))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Search books should return page of book DTOs")
+ void searchBooks_ReturnsPageOfBookDto() throws Exception {
+ List books = List.of(bookDto);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ BookSearchParametersDto searchParams = new BookSearchParametersDto(
+ List.of("Test"),
+ List.of("Author"),
+ Collections.emptyList()
+ );
+
+ when(bookService.searchByParams(any(Pageable.class), any(BookSearchParametersDto.class)))
+ .thenReturn(bookPage);
+
+ mockMvc.perform(get("/books/search")
+ .param("page", "0")
+ .param("size", "10")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(searchParams)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].id").value(bookDto.getId()))
+ .andExpect(jsonPath("$.content[0].title").value(bookDto.getTitle()))
+ .andExpect(jsonPath("$.content[0].author").value(bookDto.getAuthor()))
+ .andExpect(jsonPath("$.totalElements").value(1));
+ }
+}
diff --git a/src/test/java/com/senkiv/bookstore/controller/CategoryControllerTest.java b/src/test/java/com/senkiv/bookstore/controller/CategoryControllerTest.java
new file mode 100644
index 0000000..f6d30aa
--- /dev/null
+++ b/src/test/java/com/senkiv/bookstore/controller/CategoryControllerTest.java
@@ -0,0 +1,172 @@
+package com.senkiv.bookstore.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.senkiv.bookstore.dto.BookDtoWithoutCategories;
+import com.senkiv.bookstore.dto.CategoryRequestDto;
+import com.senkiv.bookstore.dto.CategoryResponseDto;
+import com.senkiv.bookstore.service.BookService;
+import com.senkiv.bookstore.service.CategoryService;
+import java.math.BigDecimal;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class CategoryControllerTest {
+ @Autowired
+ private MockMvc mockMvc;
+ @Autowired
+ private ObjectMapper objectMapper;
+ @MockitoBean
+ private CategoryService categoryService;
+ @MockitoBean
+ private BookService bookService;
+ private CategoryResponseDto categoryResponseDto;
+ private CategoryRequestDto categoryRequestDto;
+ private BookDtoWithoutCategories bookDtoWithoutCategories;
+ private Pageable pageable;
+
+ @BeforeEach
+ void setUp() {
+ categoryResponseDto = new CategoryResponseDto(
+ 1L,
+ "Test Category",
+ "Test Description"
+ );
+ categoryRequestDto = new CategoryRequestDto(
+ "Test Category",
+ "Test Description"
+ );
+ bookDtoWithoutCategories = new BookDtoWithoutCategories(
+ 1L,
+ "Test Book",
+ "Test Author",
+ "978-3-16-148410-0",
+ new BigDecimal("29.99"),
+ "Test Description",
+ "test-cover.jpg"
+ );
+ pageable = PageRequest.of(0, 10);
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Create category should return created category DTO")
+ void createCategory_ReturnsCreatedCategoryDto() throws Exception {
+ when(categoryService.save(any(CategoryRequestDto.class))).thenReturn(categoryResponseDto);
+
+ mockMvc.perform(post("/categories")
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(categoryRequestDto)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id").value(categoryResponseDto.id()))
+ .andExpect(jsonPath("$.name").value(categoryResponseDto.name()))
+ .andExpect(jsonPath("$.description").value(categoryResponseDto.description()));
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Get all categories should return page of category DTOs")
+ void getAll_ReturnsPageOfCategoryDto() throws Exception {
+ List categories = List.of(categoryResponseDto);
+ Page categoryPage = new PageImpl<>(categories, pageable,
+ categories.size());
+ when(categoryService.findAll(any(Pageable.class))).thenReturn(categoryPage);
+
+ mockMvc.perform(get("/categories")
+ .param("page", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].id").value(categoryResponseDto.id()))
+ .andExpect(jsonPath("$.content[0].name").value(categoryResponseDto.name()))
+ .andExpect(jsonPath("$.content[0].description").value(
+ categoryResponseDto.description()))
+ .andExpect(jsonPath("$.totalElements").value(1));
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Get category by ID should return category DTO")
+ void getCategoryById_ReturnsCategoryDto() throws Exception {
+ when(categoryService.findById(1L)).thenReturn(categoryResponseDto);
+
+ mockMvc.perform(get("/categories/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(categoryResponseDto.id()))
+ .andExpect(jsonPath("$.name").value(categoryResponseDto.name()))
+ .andExpect(jsonPath("$.description").value(categoryResponseDto.description()));
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Update category should return updated category DTO")
+ void updateCategory_ReturnsUpdatedCategoryDto() throws Exception {
+ when(categoryService.updateById(eq(1L), any(CategoryRequestDto.class))).thenReturn(
+ categoryResponseDto);
+
+ mockMvc.perform(put("/categories/1")
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(categoryRequestDto)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(categoryResponseDto.id()))
+ .andExpect(jsonPath("$.name").value(categoryResponseDto.name()))
+ .andExpect(jsonPath("$.description").value(categoryResponseDto.description()));
+ }
+
+ @Test
+ @WithMockUser(roles = "ADMIN")
+ @DisplayName("Delete category should return no content")
+ void deleteCategory_CallsServiceMethod() throws Exception {
+ doNothing().when(categoryService).deleteById(1L);
+
+ mockMvc.perform(delete("/categories/1")
+ .with(csrf()))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @WithMockUser
+ @DisplayName("Get books by category ID should return page of book DTOs without categories")
+ void getBooksByCategoryId_ReturnsPageOfBookDtoWithoutCategories() throws Exception {
+ List books = List.of(bookDtoWithoutCategories);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ when(bookService.searchByCategories(any(Pageable.class), eq(1L))).thenReturn(bookPage);
+
+ mockMvc.perform(get("/categories/1/books")
+ .param("page", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.content[0].id").value(bookDtoWithoutCategories.id()))
+ .andExpect(jsonPath("$.content[0].title").value(bookDtoWithoutCategories.title()))
+ .andExpect(jsonPath("$.content[0].author").value(bookDtoWithoutCategories.author()))
+ .andExpect(jsonPath("$.content[0].isbn").value(bookDtoWithoutCategories.isbn()))
+ .andExpect(jsonPath("$.totalElements").value(1));
+ }
+}
diff --git a/src/test/java/com/senkiv/bookstore/service/BookServiceTest.java b/src/test/java/com/senkiv/bookstore/service/BookServiceTest.java
new file mode 100644
index 0000000..ff7c7eb
--- /dev/null
+++ b/src/test/java/com/senkiv/bookstore/service/BookServiceTest.java
@@ -0,0 +1,309 @@
+package com.senkiv.bookstore.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.senkiv.bookstore.dto.BookDto;
+import com.senkiv.bookstore.dto.BookDtoWithoutCategories;
+import com.senkiv.bookstore.dto.BookSearchParametersDto;
+import com.senkiv.bookstore.dto.CreateBookRequestDto;
+import com.senkiv.bookstore.exception.BookExistsException;
+import com.senkiv.bookstore.mapper.BookMapper;
+import com.senkiv.bookstore.model.Book;
+import com.senkiv.bookstore.model.Category;
+import com.senkiv.bookstore.repository.BookRepository;
+import com.senkiv.bookstore.repository.BookSpecificationBuilder;
+import com.senkiv.bookstore.service.impl.BookServiceImpl;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+
+@ExtendWith(MockitoExtension.class)
+class BookServiceTest {
+ @Mock
+ private BookRepository bookRepository;
+ @Mock
+ private BookMapper bookMapper;
+ @Mock
+ private BookSpecificationBuilder specificationBuilder;
+ @InjectMocks
+ private BookServiceImpl bookService;
+ private Book book;
+ private BookDto bookDto;
+ private CreateBookRequestDto createBookRequestDto;
+ private Set categories;
+ private Set categoryIds;
+ private Pageable pageable;
+
+ @BeforeEach
+ void setUp() {
+ categoryIds = Set.of(1L, 2L);
+ categories = new HashSet<>();
+ Category category1 = new Category();
+ category1.setId(1L);
+ Category category2 = new Category();
+ category2.setId(2L);
+ categories.add(category1);
+ categories.add(category2);
+ book = new Book();
+ book.setId(1L);
+ book.setTitle("Test Book");
+ book.setAuthor("Test Author");
+ book.setIsbn("978-3-16-148410-0");
+ book.setPrice(new BigDecimal("29.99"));
+ book.setDescription("Test Description");
+ book.setCoverImage("test-cover.jpg");
+ book.setCategories(categories);
+ bookDto = new BookDto();
+ bookDto.setId(1L);
+ bookDto.setTitle("Test Book");
+ bookDto.setAuthor("Test Author");
+ bookDto.setIsbn("978-3-16-148410-0");
+ bookDto.setPrice(new BigDecimal("29.99"));
+ bookDto.setDescription("Test Description");
+ bookDto.setCoverImage("test-cover.jpg");
+ bookDto.setCategories(categoryIds);
+ createBookRequestDto = new CreateBookRequestDto(
+ "Test Book",
+ "Test Author",
+ "978-3-16-148410-0",
+ new BigDecimal("29.99"),
+ "Test Description",
+ "test-cover.jpg",
+ categoryIds
+ );
+ pageable = PageRequest.of(0, 10);
+ }
+
+ @Test
+ @DisplayName("Save book with valid data.")
+ void save_ValidBook_ReturnsBookDto() {
+ // Given
+ when(bookRepository.existsBookByIsbn(createBookRequestDto.isbn())).thenReturn(false);
+ when(bookMapper.toModel(createBookRequestDto)).thenReturn(book);
+ when(bookRepository.save(book)).thenReturn(book);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto result = bookService.save(createBookRequestDto);
+
+ assertThat(result).isNotNull();
+ assertEquals(bookDto, result);
+ verify(bookRepository).existsBookByIsbn(createBookRequestDto.isbn());
+ verify(bookMapper).toModel(createBookRequestDto);
+ verify(bookRepository).save(book);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should throw an exception in case of duplication of ISBN.")
+ void save_ExistingIsbn_ThrowsBookExistsException() {
+ when(bookRepository.existsBookByIsbn(createBookRequestDto.isbn())).thenReturn(true);
+
+ assertThrows(BookExistsException.class, () -> bookService.save(createBookRequestDto));
+ verify(bookRepository).existsBookByIsbn(createBookRequestDto.isbn());
+ }
+
+ @Test
+ @DisplayName("Should return all books dto in Pageable")
+ void findAll_ReturnsPageOfBookDto() {
+ List books = List.of(book);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+
+ when(bookRepository.findAll(pageable)).thenReturn(bookPage);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ Page result = bookService.findAll(pageable);
+
+ assertThat(result).isNotNull();
+ assertEquals(1, result.getTotalElements());
+ assertEquals(bookDto, result.getContent().get(0));
+ verify(bookRepository).findAll(pageable);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should return valid BookDto in case of valid Id.")
+ void findById_ExistingId_ReturnsBookDto() {
+ when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto result = bookService.findById(1L);
+
+ assertThat(result).isNotNull();
+ assertEquals(bookDto, result);
+ verify(bookRepository).findById(1L);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should throw an exception in case of non existing id.")
+ void findById_NonExistingId_ThrowsException() {
+ when(bookRepository.findById(999L)).thenReturn(Optional.empty());
+
+ assertThrows(Exception.class, () -> bookService.findById(999L));
+ verify(bookRepository).findById(999L);
+ }
+
+ @Test
+ @DisplayName("Should update Book in case of valid id.")
+ void updateById_ExistingId_ReturnsUpdatedBookDto() {
+ when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
+ when(bookMapper.update(createBookRequestDto, book)).thenReturn(book);
+ when(bookRepository.save(book)).thenReturn(book);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto result = bookService.updateById(1L, createBookRequestDto);
+
+ assertThat(result).isNotNull();
+ assertEquals(bookDto, result);
+ verify(bookRepository).findById(1L);
+ verify(bookMapper).update(createBookRequestDto, book);
+ verify(bookRepository).save(book);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should throw exception in case of invalid id.")
+ void updateById_NonExistingId_ThrowsException() {
+ when(bookRepository.findById(999L)).thenReturn(Optional.empty());
+
+ assertThrows(Exception.class,
+ () -> bookService.updateById(999L, createBookRequestDto));
+ verify(bookRepository).findById(999L);
+ }
+
+ @Test
+ @DisplayName("Should delete book in case of valid id.")
+ void deleteById_CallsRepositoryDeleteById() {
+ doNothing().when(bookRepository).deleteById(1L);
+
+ bookService.deleteById(1L);
+
+ verify(bookRepository, times(1)).deleteById(1L);
+ }
+
+ @Test
+ @DisplayName("Should do nothing in case of invalid id.")
+ void deleteById_NonExistingId_DoesNotThrowException() {
+ doNothing().when(bookRepository).deleteById(999L);
+
+ assertDoesNotThrow(() -> bookService.deleteById(999L));
+ verify(bookRepository).deleteById(999L);
+ }
+
+ @Test
+ @DisplayName("Should return Page in case of valid search parameters.")
+ void searchByParams_ReturnsPageOfBookDto() {
+ List books = List.of(book);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ BookSearchParametersDto searchParams = new BookSearchParametersDto(
+ List.of("Test"),
+ List.of("Author"),
+ Collections.emptyList()
+ );
+ Specification specification = (root, query, criteriaBuilder) -> null;
+
+ when(specificationBuilder.build(searchParams)).thenReturn(specification);
+ when(bookRepository.findAll(specification, pageable)).thenReturn(bookPage);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ Page result = bookService.searchByParams(pageable, searchParams);
+
+ assertThat(result).isNotNull();
+ assertEquals(1, result.getTotalElements());
+ assertEquals(bookDto, result.getContent().get(0));
+ verify(specificationBuilder).build(searchParams);
+ verify(bookRepository).findAll(specification, pageable);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should return Page in case of empty parameters.")
+ void searchByParams_EmptyParameters_ReturnsPageOfBookDto() {
+ List books = List.of(book);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ BookSearchParametersDto emptySearchParams = new BookSearchParametersDto(
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.emptyList()
+ );
+ Specification specification = (root, query, criteriaBuilder) -> null;
+
+ when(specificationBuilder.build(emptySearchParams)).thenReturn(specification);
+ when(bookRepository.findAll(specification, pageable)).thenReturn(bookPage);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ Page result = bookService.searchByParams(pageable, emptySearchParams);
+
+ assertThat(result).isNotNull();
+ assertEquals(1, result.getTotalElements());
+ assertEquals(bookDto, result.getContent().get(0));
+ verify(specificationBuilder).build(emptySearchParams);
+ verify(bookRepository).findAll(specification, pageable);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Should return BookDto without categories.")
+ void searchByCategories_ReturnsPageOfBookDtoWithoutCategories() {
+ List books = List.of(book);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+ BookDtoWithoutCategories bookDtoWithoutCategories = new BookDtoWithoutCategories(
+ 1L,
+ "Test Book",
+ "Test Author",
+ "978-3-16-148410-0",
+ new BigDecimal("29.99"),
+ "Test Description",
+ "test-cover.jpg"
+ );
+
+ when(bookRepository.findByCategoriesId(pageable, 1L)).thenReturn(bookPage);
+ when(bookMapper.toDtoWithoutCategories(book)).thenReturn(bookDtoWithoutCategories);
+
+ Page result = bookService.searchByCategories(pageable, 1L);
+
+ assertThat(result).isNotNull();
+ assertEquals(1, result.getTotalElements());
+ assertEquals(bookDtoWithoutCategories, result.getContent().get(0));
+ verify(bookRepository).findByCategoriesId(pageable, 1L);
+ verify(bookMapper).toDtoWithoutCategories(book);
+ }
+
+ @Test
+ @DisplayName("Should return empty page in case of unknown Category.")
+ void searchByCategories_NonExistingCategoryId_ReturnsEmptyPage() {
+ Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0);
+
+ when(bookRepository.findByCategoriesId(pageable, 999L)).thenReturn(emptyPage);
+
+ Page result = bookService.searchByCategories(pageable, 999L);
+
+ assertThat(result).isNotNull();
+ assertEquals(0, result.getTotalElements());
+ assertTrue(result.getContent().isEmpty());
+ verify(bookRepository).findByCategoriesId(pageable, 999L);
+ }
+}
diff --git a/src/test/java/com/senkiv/bookstore/service/CategoryServiceTest.java b/src/test/java/com/senkiv/bookstore/service/CategoryServiceTest.java
new file mode 100644
index 0000000..4730998
--- /dev/null
+++ b/src/test/java/com/senkiv/bookstore/service/CategoryServiceTest.java
@@ -0,0 +1,197 @@
+package com.senkiv.bookstore.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.senkiv.bookstore.dto.CategoryRequestDto;
+import com.senkiv.bookstore.dto.CategoryResponseDto;
+import com.senkiv.bookstore.exception.CategoryExistsException;
+import com.senkiv.bookstore.mapper.CategoryMapper;
+import com.senkiv.bookstore.model.Book;
+import com.senkiv.bookstore.model.Category;
+import com.senkiv.bookstore.repository.BookRepository;
+import com.senkiv.bookstore.repository.CategoryRepository;
+import com.senkiv.bookstore.service.impl.CategoryServiceImpl;
+import jakarta.persistence.EntityNotFoundException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+@ExtendWith(MockitoExtension.class)
+class CategoryServiceTest {
+ @Mock
+ private CategoryRepository categoryRepository;
+ @Mock
+ private BookRepository bookRepository;
+ @Mock
+ private CategoryMapper categoryMapper;
+ @InjectMocks
+ private CategoryServiceImpl categoryService;
+ private Category category;
+ private CategoryResponseDto categoryResponseDto;
+ private CategoryRequestDto categoryRequestDto;
+ private Pageable pageable;
+
+ @BeforeEach
+ void setUp() {
+ category = new Category();
+ category.setId(1L);
+ category.setName("Test Category");
+ category.setDescription("Test Description");
+ categoryResponseDto = new CategoryResponseDto(
+ 1L,
+ "Test Category",
+ "Test Description"
+ );
+ categoryRequestDto = new CategoryRequestDto(
+ "Test Category",
+ "Test Description"
+ );
+ pageable = PageRequest.of(0, 10);
+ }
+
+ @Test
+ @DisplayName("Save category with valid data.")
+ void save_ValidCategory_ReturnsCategoryResponseDto() {
+ when(categoryRepository.existsByName(categoryRequestDto.name())).thenReturn(false);
+ when(categoryMapper.toModel(categoryRequestDto)).thenReturn(category);
+ when(categoryRepository.save(category)).thenReturn(category);
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto result = categoryService.save(categoryRequestDto);
+
+ assertThat(result).isNotNull();
+ assertEquals(categoryResponseDto, result);
+ verify(categoryRepository).existsByName(categoryRequestDto.name());
+ verify(categoryMapper).toModel(categoryRequestDto);
+ verify(categoryRepository).save(category);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Should throw an exception in case of duplication of name.")
+ void save_ExistingName_ThrowsCategoryExistsException() {
+ when(categoryRepository.existsByName(categoryRequestDto.name())).thenReturn(true);
+
+ assertThrows(CategoryExistsException.class, () -> categoryService.save(categoryRequestDto));
+ verify(categoryRepository).existsByName(categoryRequestDto.name());
+ }
+
+ @Test
+ @DisplayName("Should return all categories dto in Pageable")
+ void findAll_ReturnsPageOfCategoryResponseDto() {
+ List categories = List.of(category);
+ Page categoryPage = new PageImpl<>(categories, pageable, categories.size());
+
+ when(categoryRepository.findAll(pageable)).thenReturn(categoryPage);
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ Page result = categoryService.findAll(pageable);
+
+ assertThat(result).isNotNull();
+ assertEquals(1, result.getTotalElements());
+ assertEquals(categoryResponseDto, result.getContent().get(0));
+ verify(categoryRepository).findAll(pageable);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Should return valid CategoryResponseDto in case of valid Id.")
+ void findById_ExistingId_ReturnsCategoryResponseDto() {
+ when(categoryRepository.findById(1L)).thenReturn(Optional.of(category));
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto result = categoryService.findById(1L);
+
+ assertThat(result).isNotNull();
+ assertEquals(categoryResponseDto, result);
+ verify(categoryRepository).findById(1L);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Should throw an exception in case of non existing id.")
+ void findById_NonExistingId_ThrowsEntityNotFoundException() {
+ when(categoryRepository.findById(999L)).thenReturn(Optional.empty());
+
+ assertThrows(EntityNotFoundException.class, () -> categoryService.findById(999L));
+ verify(categoryRepository).findById(999L);
+ }
+
+ @Test
+ @DisplayName("Should update Category in case of valid id.")
+ void updateById_ExistingId_ReturnsUpdatedCategoryResponseDto() {
+ when(categoryRepository.findById(1L)).thenReturn(Optional.of(category));
+ when(categoryMapper.update(categoryRequestDto, category)).thenReturn(category);
+ when(categoryRepository.save(category)).thenReturn(category);
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto result = categoryService.updateById(1L, categoryRequestDto);
+
+ assertThat(result).isNotNull();
+ assertEquals(categoryResponseDto, result);
+ verify(categoryRepository).findById(1L);
+ verify(categoryMapper).update(categoryRequestDto, category);
+ verify(categoryRepository).save(category);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Should throw exception in case of invalid id.")
+ void updateById_NonExistingId_ThrowsEntityNotFoundException() {
+ when(categoryRepository.findById(999L)).thenReturn(Optional.empty());
+
+ assertThrows(EntityNotFoundException.class,
+ () -> categoryService.updateById(999L, categoryRequestDto));
+ verify(categoryRepository).findById(999L);
+ }
+
+ @Test
+ @DisplayName("Should delete category and update books in case of valid id.")
+ void deleteById_ExistingId_DeletesCategoryAndUpdateBooks() {
+ Book book = new Book();
+ Set categories = new HashSet<>();
+ categories.add(category);
+ book.setCategories(categories);
+ List books = List.of(book);
+
+ when(categoryRepository.findCategoryById(1L)).thenReturn(Optional.of(category));
+ when(bookRepository.findByCategoriesId(1L)).thenReturn(books);
+ when(bookRepository.saveAll(books)).thenReturn(books);
+ doNothing().when(categoryRepository).deleteById(1L);
+
+ categoryService.deleteById(1L);
+
+ verify(categoryRepository).findCategoryById(1L);
+ verify(bookRepository).findByCategoriesId(1L);
+ verify(bookRepository).saveAll(books);
+ verify(categoryRepository).deleteById(1L);
+ assertTrue(book.getCategories().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should throw exception in case of invalid id.")
+ void deleteById_NonExistingId_ThrowsCategoryExistsException() {
+ when(categoryRepository.findCategoryById(999L)).thenReturn(Optional.empty());
+
+ assertThrows(CategoryExistsException.class, () -> categoryService.deleteById(999L));
+ verify(categoryRepository).findCategoryById(999L);
+ }
+}