From 681bb3f341db2c5a7e7fadac9466ccff297bcc15 Mon Sep 17 00:00:00 2001 From: Serhii Senkiv Date: Tue, 3 Jun 2025 12:58:13 +0200 Subject: [PATCH] Tested BookService, BookController, CategoryService and CategoryController. --- pom.xml | 5 + .../service/impl/BookServiceImpl.java | 1 - .../controller/BookControllerTest.java | 181 ++++++++++ .../controller/CategoryControllerTest.java | 172 ++++++++++ .../bookstore/service/BookServiceTest.java | 309 ++++++++++++++++++ .../service/CategoryServiceTest.java | 197 +++++++++++ 6 files changed, 864 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/senkiv/bookstore/controller/BookControllerTest.java create mode 100644 src/test/java/com/senkiv/bookstore/controller/CategoryControllerTest.java create mode 100644 src/test/java/com/senkiv/bookstore/service/BookServiceTest.java create mode 100644 src/test/java/com/senkiv/bookstore/service/CategoryServiceTest.java 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); + } +}