diff --git a/pom.xml b/pom.xml index 7d4ba60..ad87c94 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,10 @@ org.springframework.boot spring-boot-docker-compose + + org.springframework.security + spring-security-test + org.junit.jupiter junit-jupiter diff --git a/src/main/java/mate/academy/bookstore/controller/BookController.java b/src/main/java/mate/academy/bookstore/controller/BookController.java index 74033ba..6e56c30 100644 --- a/src/main/java/mate/academy/bookstore/controller/BookController.java +++ b/src/main/java/mate/academy/bookstore/controller/BookController.java @@ -9,7 +9,9 @@ import mate.academy.bookstore.dto.book.BookSearchParametersDto; import mate.academy.bookstore.dto.book.CreateBookRequestDto; import mate.academy.bookstore.service.BookService; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -33,7 +35,9 @@ public class BookController { @GetMapping @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get all books", description = "Get a list of all available books") - public List getAll(Pageable pageable) { + public List getAll(@ParameterObject + @PageableDefault(sort = {"price", "title"}, value = 5) + Pageable pageable) { return bookService.findAll(pageable); } diff --git a/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java b/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java index ea2900e..4d15ccd 100644 --- a/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java +++ b/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java @@ -3,7 +3,9 @@ import java.util.List; import java.util.Optional; import mate.academy.bookstore.model.Book; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -15,9 +17,15 @@ public interface BookRepository extends JpaRepository, JpaSpecificat @Query("FROM Book b JOIN FETCH b.categories") List findAllBooks(Pageable pageable); + @EntityGraph(attributePaths = {"categories"}) + Page findAll(Specification spec, Pageable pageable); + @EntityGraph(attributePaths = {"categories"}) Optional findBookByIsbn(String isbn); + @Query("FROM Book b JOIN FETCH b.categories WHERE b.id = :id") + Optional findBookById(Long id); + @Query("SELECT b FROM Book b LEFT JOIN FETCH b.categories c WHERE c.id = :categoryId") List findAllBooksByCategoryId(@Param("categoryId") Long categoryId, Pageable pageable); } diff --git a/src/main/java/mate/academy/bookstore/repository/book/specification/PriceSpecificationProvider.java b/src/main/java/mate/academy/bookstore/repository/book/specification/PriceSpecificationProvider.java index f29fb3e..b9d5650 100644 --- a/src/main/java/mate/academy/bookstore/repository/book/specification/PriceSpecificationProvider.java +++ b/src/main/java/mate/academy/bookstore/repository/book/specification/PriceSpecificationProvider.java @@ -20,8 +20,11 @@ public String getKey() { @Override public Specification getSpecification(BookSearchParametersDto params) { - int minPrice = Optional.ofNullable(params.minPrice()).orElse(DEFAULT_MIN_PRICE); - int maxPrice = Optional.ofNullable(params.maxPrice()).orElse(DEFAULT_MAX_PRICE); + int paramsMinPrice = Optional.ofNullable(params.minPrice()).orElse(DEFAULT_MIN_PRICE); + int paramsMaxPrice = Optional.ofNullable(params.maxPrice()).orElse(DEFAULT_MAX_PRICE); + + int minPrice = Math.max(paramsMinPrice, DEFAULT_MIN_PRICE); + int maxPrice = (paramsMaxPrice > paramsMinPrice) ? paramsMaxPrice : DEFAULT_MAX_PRICE; return ((root, query, criteriaBuilder) -> criteriaBuilder.between(root.get(getKey()), minPrice, maxPrice)); diff --git a/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java index ba3972d..1761233 100644 --- a/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java +++ b/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java @@ -52,7 +52,7 @@ public List findAll(Pageable pageable) { } public BookDto findById(Long id) { - Book book = bookRepository.findById(id).orElseThrow( + Book book = bookRepository.findBookById(id).orElseThrow( () -> new EntityNotFoundException("Book not found by id " + id)); return bookMapper.toDto(book); } diff --git a/src/test/java/mate/academy/bookstore/controller/BookControllerTest.java b/src/test/java/mate/academy/bookstore/controller/BookControllerTest.java index c13dbca..82e3c1c 100644 --- a/src/test/java/mate/academy/bookstore/controller/BookControllerTest.java +++ b/src/test/java/mate/academy/bookstore/controller/BookControllerTest.java @@ -1,74 +1,169 @@ package mate.academy.bookstore.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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 java.math.BigDecimal; +import java.util.Collections; +import java.util.Set; +import mate.academy.bookstore.dto.book.BookDto; +import mate.academy.bookstore.dto.book.BookSearchParametersDto; +import mate.academy.bookstore.dto.book.CreateBookRequestDto; +import mate.academy.bookstore.service.BookService; +import org.junit.jupiter.api.BeforeAll; +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.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BookControllerTest { + private static final String ID = "/{id}"; + private static final String TITLE_PARAM_NAME = "title"; + private static final String AUTHOR_PARAM_NAME = "author"; + private static final String ISBN_PARAM_NAME = "isbn"; + private static final String PRICE_PARAM_NAME = "price"; + private static final String BASE_URL = "/books"; + private static final String SEARCH_URL = BASE_URL + "/search"; + private static final String ADMIN_ROLE = "ADMIN"; + private static final String USER_ROLE = "USER"; + private static final String VALID_BOOK_TITLE = "Kobzar"; + private static final String VALID_BOOK_AUTHOR = "Taras Shevchenko"; + private static final String VALID_BOOK_ISBN = "978-0-7847-5628-7"; + private static final BigDecimal VALID_BOOK_PRICE = BigDecimal.valueOf(14.99); + private static final Set VALID_CATEGORY_ID = Set.of(1L); + private static final Long VALID_BOOK_ID = 1L; + private static final String TITLE_0_EXPRESSION = "$[0].title"; + private static final String TITLE_EXPRESSION = "$.title"; -class BookControllerTest { + private static MockMvc mockMvc; - @Test - void getAll_ValidRequest_True() { - } - - @Test - void getAll_InvalidRequest_False() { - } - - @Test - void getBookById_ValidId_True() { - } - - @Test - void getBookById_InvalidId_False() { - } - - @Test - void getBookById_NullId_False() { - } - - @Test - void createBook_ValidBook_True() { - } - - @Test - void createBook_InvalidBook_False() { - } - - @Test - void createBook_NullBook_False() { - } + @MockBean + private BookService bookService; - @Test - void updateBook_ValidBook_True() { - } - - @Test - void updateBook_InvalidBook_False() { - } + @Autowired + private ObjectMapper objectMapper; - @Test - void updateBook_NullBook_False() { - } + private BookDto bookDto; + private CreateBookRequestDto createBookDto; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext context) { + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @BeforeEach + void setup() { + bookDto = new BookDto(); + bookDto.setId(VALID_BOOK_ID); + bookDto.setTitle(VALID_BOOK_TITLE); + bookDto.setAuthor(VALID_BOOK_AUTHOR); + bookDto.setIsbn(VALID_BOOK_ISBN); + bookDto.setPrice(VALID_BOOK_PRICE); + + createBookDto = new CreateBookRequestDto(); + createBookDto.setTitle(VALID_BOOK_TITLE); + createBookDto.setAuthor(VALID_BOOK_AUTHOR); + createBookDto.setIsbn(VALID_BOOK_ISBN); + createBookDto.setCategoryIds(VALID_CATEGORY_ID); + createBookDto.setPrice(VALID_BOOK_PRICE); + } + + @Test + @WithMockUser(roles = {USER_ROLE, ADMIN_ROLE}) + @DisplayName("Get all books (valid request)") + void getAllBooks_ValidRequest_ReturnsListOfBooks() throws Exception { + when(bookService.findAll(any(Pageable.class))).thenReturn( + Collections.singletonList(bookDto)); + + mockMvc.perform(get(BASE_URL)) + .andExpect(status().isOk()) + .andExpect(jsonPath(TITLE_0_EXPRESSION).value(VALID_BOOK_TITLE)); + } + + @Test + @WithMockUser(roles = {USER_ROLE, ADMIN_ROLE}) + @DisplayName("Get book by id (valid request)") + void getBookById_ValidRequest_ReturnsBookDto() throws Exception { + when(bookService.findById(anyLong())).thenReturn(bookDto); + + mockMvc.perform(get(BASE_URL + ID, VALID_BOOK_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath(TITLE_EXPRESSION).value(VALID_BOOK_TITLE)); + } + + @Test + @WithMockUser(roles = ADMIN_ROLE) + @DisplayName("Create book (valid request)") + void createBook_ValidRequest_ReturnsCreatedBookDto() throws Exception { + when(bookService.save(any(CreateBookRequestDto.class))).thenReturn(bookDto); + + String requestContent = objectMapper.writeValueAsString(createBookDto); + + mockMvc.perform(post(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(requestContent)) + .andExpect(status().isCreated()) + .andExpect(jsonPath(TITLE_EXPRESSION).value(VALID_BOOK_TITLE)); + } @Test - void deleteBook_ValidId_True() { - } + @WithMockUser(roles = ADMIN_ROLE) + @DisplayName("Update book (valid request)") + void updateBook_ValidRequest_ReturnsUpdatedBookDto() throws Exception { + when(bookService.updateById(anyLong(), any(CreateBookRequestDto.class))).thenReturn( + bookDto); - @Test - void deleteBook_InvalidId_False() { - } + String requestContent = objectMapper.writeValueAsString(createBookDto); - @Test - void deleteBook_NullId_False() { + mockMvc.perform(put(BASE_URL + ID, VALID_BOOK_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(requestContent)) + .andExpect(status().isAccepted()) + .andExpect(jsonPath(TITLE_EXPRESSION).value(VALID_BOOK_TITLE)); } @Test - void searchBook_ValidCriteria_True() { + @WithMockUser(roles = ADMIN_ROLE) + @DisplayName("Delete book (valid request)") + void deleteBook_ValidRequest_ReturnsNoContent() throws Exception { + mockMvc.perform(delete(BASE_URL + ID, VALID_BOOK_ID)) + .andExpect(status().isNoContent()); } @Test - void searchBook_InvalidCriteria_False() { - } + @WithMockUser(roles = {USER_ROLE, ADMIN_ROLE}) + @DisplayName("Search books (valid request)") + void searchBooks_ValidRequest_ReturnsListOfBooks() throws Exception { + when(bookService.search(any(BookSearchParametersDto.class), any(Pageable.class))) + .thenReturn(Collections.singletonList(bookDto)); - @Test - void searchBook_EmptyCriteria_False() { + mockMvc.perform(get(SEARCH_URL) + .param(TITLE_PARAM_NAME, VALID_BOOK_TITLE) + .param(AUTHOR_PARAM_NAME, VALID_BOOK_AUTHOR) + .param(ISBN_PARAM_NAME, VALID_BOOK_ISBN) + .param(PRICE_PARAM_NAME, VALID_BOOK_PRICE.toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath(TITLE_0_EXPRESSION).value(VALID_BOOK_TITLE)); } } diff --git a/src/test/java/mate/academy/bookstore/controller/CategoryControllerTest.java b/src/test/java/mate/academy/bookstore/controller/CategoryControllerTest.java index 77e7cc8..0fad4e0 100644 --- a/src/test/java/mate/academy/bookstore/controller/CategoryControllerTest.java +++ b/src/test/java/mate/academy/bookstore/controller/CategoryControllerTest.java @@ -1,74 +1,110 @@ package mate.academy.bookstore.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +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.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import mate.academy.bookstore.dto.book.BookDtoWithoutCategoryIds; +import mate.academy.bookstore.dto.category.CategoryDto; +import mate.academy.bookstore.service.BookService; +import mate.academy.bookstore.service.CategoryService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class CategoryControllerTest { + private static final Long VALID_CATEGORY_ID = 1L; + private static final String VALID_CATEGORY_NAME = "Fiction"; + private static final String CATEGORIES_URL = "/categories"; + private static final String CATEGORIES_ID_URL = "/categories/{id}"; + private static final String CATEGORIES_ID_BOOKS_URL = "/categories/{id}/books"; + private static final String EXPRESSION = "$"; + private static final String NAME_EXPRESSION = "$.name"; + private static final String ADMIN_ROLE = "ADMIN"; + private static final String USER_ROLE = "USER"; + private static MockMvc mockMvc; - @Test - void createCategory_ValidCategory_True() { - } - - @Test - void createCategory_InvalidCategory_False() { - } - - @Test - void createCategory_NullCategory_False() { - } - - @Test - void getAll_ValidRequest_True() { - } - - @Test - void getAll_InvalidRequest_False() { - } + @Mock + private CategoryService categoryService; - @Test - void getCategoryById_ValidId_True() { - } + @Mock + private BookService bookService; - @Test - void getCategoryById_InvalidId_False() { + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext context) { + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); } @Test - void getCategoryById_NullId_False() { - } + @WithMockUser(roles = ADMIN_ROLE) + @DisplayName("Create new category") + void create_ValidRequest_ReturnsCreatedCategory() throws Exception { + CategoryDto categoryDto = new CategoryDto(); + categoryDto.setName(VALID_CATEGORY_NAME); - @Test - void updateCategory_ValidCategory_True() { - } + when(categoryService.save(any(CategoryDto.class))).thenReturn(categoryDto); - @Test - void updateCategory_InvalidCategory_False() { + mockMvc.perform(post(CATEGORIES_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(categoryDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath(NAME_EXPRESSION).value(VALID_CATEGORY_NAME)); } @Test - void updateCategory_NullCategory_False() { - } + @WithMockUser(roles = USER_ROLE) + @DisplayName("Get all categories") + void getAll_ValidRequest_ReturnsListOfCategories() throws Exception { + List categories = List.of(new CategoryDto()); - @Test - void deleteCategory_ValidId_True() { - } + when(categoryService.findAll(any())).thenReturn(categories); - @Test - void deleteCategory_InvalidId_False() { + mockMvc.perform(get(CATEGORIES_URL)) + .andExpect(status().isOk()) + .andExpect(jsonPath(EXPRESSION).exists()); } @Test - void deleteCategory_NullId_False() { + @WithMockUser(roles = USER_ROLE) + @DisplayName("Delete category by id with invalid role") + void deleteCategory_WIthInValidRoleUser_Forbidden() throws Exception { + mockMvc.perform(delete(CATEGORIES_ID_URL, VALID_CATEGORY_ID).with(csrf())) + .andExpect(status().isForbidden()); } @Test - void getBooksByCategoryId_ValidId_True() { - } + @WithMockUser(roles = {USER_ROLE, ADMIN_ROLE}) + @DisplayName("Get all books by category id") + void getBooksByCategoryId_ValidRequest_ReturnsListOfBooks() throws Exception { + List books = List.of(new BookDtoWithoutCategoryIds()); - @Test - void getBooksByCategoryId_InvalidId_False() { - } + when(bookService.getAllBookByCategoryId(eq(VALID_CATEGORY_ID), + any(Pageable.class))).thenReturn(books); - @Test - void getBooksByCategoryId_NullId_False() { + mockMvc.perform(get(CATEGORIES_ID_BOOKS_URL, VALID_CATEGORY_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath(EXPRESSION).exists()); } } diff --git a/src/test/java/mate/academy/bookstore/repository/book/BookRepositoryTest.java b/src/test/java/mate/academy/bookstore/repository/book/BookRepositoryTest.java index 09dedf3..afbfca3 100644 --- a/src/test/java/mate/academy/bookstore/repository/book/BookRepositoryTest.java +++ b/src/test/java/mate/academy/bookstore/repository/book/BookRepositoryTest.java @@ -1,64 +1,167 @@ package mate.academy.bookstore.repository.book; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; +import mate.academy.bookstore.model.Book; +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.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @DataJpaTest class BookRepositoryTest { + private static final String DELETE_DATA_FROM_DB = "classpath:database/delete-data-from-db.sql"; + private static final String INSERT_DATA_INTO_DB = "classpath:database/insert-data-into-db.sql"; + private static final Long VALID_BOOK_ID_KOBZAR = 1L; + private static final String VALID_BOOK_TITLE_KOBZAR = "Kobzar"; + private static final String VALID_BOOK_ISBN_KOBZAR = "978-1-1516-4732-0"; + private static final String INVALID_BOOK_ISBN = "978-11-1516-4732-02"; + private static final long INVALID_ID = -1L; + private static final long VALID_CATEGORY_ID = 1L; + private static final long EMPTY_CATEGORY_ID = 4L; + private static final int PAGE_NUMBER = 0; + private static final int PAGE_SIZE = 5; + private static final String EMPTY_STRING = ""; + private static final String VALID_BOOK_ISBN_NOT_IN_DB = "978-1-2345-6789-0"; @Autowired private BookRepository bookRepository; @Test - void findAllBooks_ValidRequest_True() { + @DisplayName("Find all books (valid request)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findAllBooks_ValidRequest_ReturnsNonEmptyListOfBooks() { + List books = bookRepository.findAllBooks(PageRequest.of(PAGE_NUMBER, PAGE_SIZE)); + assertNotNull(books); + assertFalse(books.isEmpty()); } @Test - void findAllBooks_InvalidRequest_False() { + @DisplayName("Find a book by isbn - (valid isbn)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookByIsbn_ValidIsbn_ReturnsBookWithMatchingIsbn() { + Optional bookOptional = bookRepository.findBookByIsbn(VALID_BOOK_ISBN_KOBZAR); + assertTrue(bookOptional.isPresent()); + assertEquals(VALID_BOOK_ID_KOBZAR, bookOptional.get().getId()); + assertEquals(VALID_BOOK_ISBN_KOBZAR, bookOptional.get().getIsbn()); } @Test - void findBookByIsbn_GivenValidIsbn_True() { + @DisplayName("Find a book by isbn - (invalid isbn)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookByIsbn_InvalidIsbn_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookByIsbn(INVALID_BOOK_ISBN); + assertFalse(bookOptional.isPresent()); } @Test - void findBookByIsbn_GivenInvalidIsbn_False() { + @DisplayName("Find a book by isbn - (empty isbn)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookByIsbn_EmptyIsbn_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookByIsbn(EMPTY_STRING); + assertFalse(bookOptional.isPresent()); } @Test - void findBookByIsbn_EmptyIsbn_False() { + @DisplayName("Find a book by isbn - (null isbn)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookByIsbn_NullIsbn_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookByIsbn(null); + assertFalse(bookOptional.isPresent()); } @Test - void findBookByIsbn_NullIsbn_False() { + @DisplayName("Find book by id (existing book)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookById_ValidId_ReturnsBookWithMatchingId() { + Book book = bookRepository.findBookById(VALID_BOOK_ID_KOBZAR).orElse(null); + assertNotNull(book); + assertEquals(VALID_BOOK_TITLE_KOBZAR, book.getTitle()); } @Test - void findAllBooksByCategoryId_ValidCategoryId_True() { + @DisplayName("Find a book by isbn - (valid isbn, not in DB)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookByIsbn_ValidIsbnBookNotInRepository_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookByIsbn(VALID_BOOK_ISBN_NOT_IN_DB); + assertFalse(bookOptional.isPresent()); } @Test - void findAllBooksByCategoryId_InvalidCategoryId_False() { + @DisplayName("Find book by id (non existing book)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookById_InvalidId_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookById(INVALID_ID); + assertFalse(bookOptional.isPresent()); } @Test - void findAllBooksByCategoryId_EmptyCategoryId_False() { + @DisplayName("Find book by id (null id)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findBookById_NullId_ReturnsEmptyOptional() { + Optional bookOptional = bookRepository.findBookById(null); + assertFalse(bookOptional.isPresent()); } @Test - void findAllBooksByCategoryId_NullCategoryId_False() { + @DisplayName("Find all book by category id (valid id)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findAllBooksByCategoryId_ValidCategoryId_ReturnsNonEmptyListOfBooks() { + List books = bookRepository.findAllBooksByCategoryId( + VALID_CATEGORY_ID, PageRequest.of(PAGE_NUMBER, PAGE_SIZE)); + assertNotNull(books); + assertFalse(books.isEmpty()); } @Test - void findAllBooks_NoBooksInRepository_False() { + @DisplayName("Find all book by category id (invalid id") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findAllBooksByCategoryId_InvalidCategoryId_ReturnsEmptyList() { + List books = bookRepository.findAllBooksByCategoryId( + INVALID_ID, PageRequest.of(PAGE_NUMBER, PAGE_SIZE)); + assertNotNull(books); + assertTrue(books.isEmpty()); } @Test - void findAllBooksByCategoryId_CategoryHasNoBooks_False() { + @DisplayName("Find all book by category id (empty id") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findAllBooksByCategoryId_NullCategoryId_ReturnsEmptyList() { + List books = bookRepository.findAllBooksByCategoryId( + null, PageRequest.of(PAGE_NUMBER, PAGE_SIZE)); + assertNotNull(books); + assertTrue(books.isEmpty()); } @Test - void findBookByIsbn_ValidIsbnBookNotInRepository_False() { + @DisplayName("Find all book by category id (no books in category") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findAllBooksByCategoryId_CategoryHasNoBooks_ReturnsEmptyList() { + List books = bookRepository.findAllBooksByCategoryId( + EMPTY_CATEGORY_ID, PageRequest.of(PAGE_NUMBER, PAGE_SIZE)); + assertNotNull(books); + assertTrue(books.isEmpty()); } } diff --git a/src/test/java/mate/academy/bookstore/repository/category/CategoryRepositoryTest.java b/src/test/java/mate/academy/bookstore/repository/category/CategoryRepositoryTest.java index 765762b..c53932a 100644 --- a/src/test/java/mate/academy/bookstore/repository/category/CategoryRepositoryTest.java +++ b/src/test/java/mate/academy/bookstore/repository/category/CategoryRepositoryTest.java @@ -1,5 +1,10 @@ package mate.academy.bookstore.repository.category; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import mate.academy.bookstore.model.Category; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,38 +17,68 @@ class CategoryRepositoryTest { private static final String DELETE_DATA_FROM_DB = "classpath:database/delete-data-from-db.sql"; private static final String INSERT_DATA_INTO_DB = "classpath:database/insert-data-into-db.sql"; + private static final String VALID_CATEGORY_NAME = "Poetry"; + private static final String INVALID_CATEGORY_NAME = "Coco Jamboo"; + private static final String VALID_CATEGORY_NAME_DIFFERENT_CASES = "pOetRy"; + private static final String CATEGORY_NAME_SPECIAL_CHARACTERS = "Po*try"; + private static final String EMPTY_STRING = ""; @Autowired private CategoryRepository categoryRepository; @Test - @DisplayName("Find a book by name") + @DisplayName("Find a category by name - (valid name)") @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - void findByName_ValidName_True() { - } - - @Test - void findByName_InvalidName_False() { + void findByName_ValidName_ReturnsCategory() { + Category category = categoryRepository.findByName(VALID_CATEGORY_NAME); + assertNotNull(category); + assertEquals(VALID_CATEGORY_NAME, category.getName()); } @Test - void findByName_EmptyName_False() { + @DisplayName("Find a category by name - (invalid name)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findByName_InvalidName_ReturnsNull() { + Category category = categoryRepository.findByName(INVALID_CATEGORY_NAME); + assertNull(category); } @Test - void findByName_NullName_False() { + @DisplayName("Find a category by name - (empty name)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findByName_EmptyName_ReturnsNull() { + Category category = categoryRepository.findByName(EMPTY_STRING); + assertNull(category); } @Test - void findByName_NameWithDifferentCase_False() { + @DisplayName("Find a category by name - (null name)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findByName_NullName_ReturnsNull() { + Category category = categoryRepository.findByName(null); + assertNull(category); } @Test - void findByName_SpecialCharactersInName_False() { + @DisplayName("Find a category by name - (name with different case)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findByName_NameWithDifferentCase_ReturnsNull() { + Category expected = categoryRepository.findByName(VALID_CATEGORY_NAME); + Category actual = categoryRepository.findByName(VALID_CATEGORY_NAME_DIFFERENT_CASES); + assertEquals(expected, actual); } @Test - void findByName_MultipleCategoriesWithSameName_True() { + @DisplayName("Find a category by name - (name with special characters)") + @Sql(scripts = {DELETE_DATA_FROM_DB, INSERT_DATA_INTO_DB}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void findByName_SpecialCharactersInName_ReturnsNull() { + Category category = categoryRepository.findByName(CATEGORY_NAME_SPECIAL_CHARACTERS); + assertNull(category); } } diff --git a/src/test/java/mate/academy/bookstore/service/BookServiceTest.java b/src/test/java/mate/academy/bookstore/service/BookServiceTest.java deleted file mode 100644 index 495dfc9..0000000 --- a/src/test/java/mate/academy/bookstore/service/BookServiceTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package mate.academy.bookstore.service; - -import org.junit.jupiter.api.Test; - -class BookServiceTest { - - @Test - void save_ValidBook_True() { - } - - @Test - void save_InvalidBook_False() { - } - - @Test - void save_NullBook_False() { - } - - @Test - void findAll_ValidRequest_True() { - } - - @Test - void findAll_InvalidRequest_False() { - } - - @Test - void findById_ValidId_True() { - } - - @Test - void findById_InvalidId_False() { - } - - @Test - void findById_NullId_False() { - } - - @Test - void search_ValidCriteria_True() { - } - - @Test - void search_InvalidCriteria_False() { - } - - @Test - void search_EmptyCriteria_False() { - } - - @Test - void search_NullCriteria_False() { - } - - @Test - void updateById_ValidBook_True() { - } - - @Test - void updateById_InvalidBook_False() { - } - - @Test - void updateById_NullBook_False() { - } - - @Test - void delete_ValidId_True() { - } - - @Test - void delete_InvalidId_False() { - } - - @Test - void delete_NullId_False() { - } - - @Test - void getAllBookByCategoryId_ValidCategoryId_True() { - } - - @Test - void getAllBookByCategoryId_InvalidCategoryId_False() { - } - - @Test - void getAllBookByCategoryId_NullCategoryId_False() { - } -} diff --git a/src/test/java/mate/academy/bookstore/service/CategoryServiceTest.java b/src/test/java/mate/academy/bookstore/service/CategoryServiceTest.java deleted file mode 100644 index a3250f4..0000000 --- a/src/test/java/mate/academy/bookstore/service/CategoryServiceTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package mate.academy.bookstore.service; - -import org.junit.jupiter.api.Test; - -class CategoryServiceTest { - - @Test - void findAll_ValidRequest_True() { - } - - @Test - void findAll_InvalidRequest_False() { - } - - @Test - void getById_ValidId_True() { - } - - @Test - void getById_InvalidId_False() { - } - - @Test - void getById_NullId_False() { - } - - @Test - void save_ValidCategory_True() { - } - - @Test - void save_InvalidCategory_False() { - } - - @Test - void save_NullCategory_False() { - } - - @Test - void update_ValidCategory_True() { - } - - @Test - void update_InvalidCategory_False() { - } - - @Test - void update_NullCategory_False() { - } - - @Test - void delete_ValidId_True() { - } - - @Test - void delete_InvalidId_False() { - } - - @Test - void delete_NullId_False() { - } -} diff --git a/src/test/java/mate/academy/bookstore/service/impl/BookServiceImplTest.java b/src/test/java/mate/academy/bookstore/service/impl/BookServiceImplTest.java new file mode 100644 index 0000000..baf0e52 --- /dev/null +++ b/src/test/java/mate/academy/bookstore/service/impl/BookServiceImplTest.java @@ -0,0 +1,281 @@ +package mate.academy.bookstore.service.impl; + +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.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import mate.academy.bookstore.dto.book.BookDto; +import mate.academy.bookstore.dto.book.BookDtoWithoutCategoryIds; +import mate.academy.bookstore.dto.book.BookSearchParametersDto; +import mate.academy.bookstore.dto.book.CreateBookRequestDto; +import mate.academy.bookstore.exception.DuplicateIsbnException; +import mate.academy.bookstore.exception.EntityNotFoundException; +import mate.academy.bookstore.mapper.BookMapper; +import mate.academy.bookstore.model.Book; +import mate.academy.bookstore.repository.book.BookRepository; +import mate.academy.bookstore.repository.book.BookSpecificationBuilder; +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.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +class BookServiceImplTest { + private static final String VALID_BOOK_TITLE = "Kobzar"; + private static final String VALID_BOOK_AUTHOR = "Taras Shevchenko"; + private static final String VALID_BOOK_ISBN = "978-0-7847-5628-7"; + private static final BigDecimal VALID_BOOK_PRICE = BigDecimal.valueOf(14.99); + private static final Set VALID_CATEGORY_ID = Set.of(1L); + private static final String VALID_BOOK_DESCRIPTION = "Awesome description..."; + private static final String VALID_BOOK_COVER_IMAGE = "kobzar.png"; + private static final Long VALID_BOOK_ID = 1L; + private static final int EXPECTED_LIST_SIZE = 1; + private static final int PAGE_NUMBER = 0; + private static final int PAGE_SIZE = 5; + private static final int VALID_MIN_PRICE = 10; + private static final int VALID_MAX_PRICE = 20; + private static final int NEGATIVE_PRICE = -10; + private static final int ZERO_MAX_PRICE = 0; + + @Mock + private BookRepository bookRepository; + + @InjectMocks + private BookServiceImpl bookService; + + @Mock + private BookMapper bookMapper; + + @Mock + private BookSpecificationBuilder specificationBuilder; + + @Test + @DisplayName("Save a valid book (return BookDto)") + void save_ValidBook_ReturnBookDto() { + CreateBookRequestDto createBookDto = getCreateBookRequestDto(); + BookDto bookDto = getBookDto(createBookDto); + Book newBook = new Book(); + + when(bookMapper.toEntity(createBookDto)).thenReturn(newBook); + when(bookRepository.save(newBook)).thenReturn(newBook); + when(bookMapper.toDto(newBook)).thenReturn(bookDto); + + BookDto savedBook = bookService.save(createBookDto); + + assertEquals(VALID_BOOK_TITLE, savedBook.getTitle()); + assertEquals(VALID_BOOK_AUTHOR, savedBook.getAuthor()); + } + + @Test + @DisplayName("Save a book with duplicate isbn (throws exception)") + void save_BookWithDuplicate_Isbn_ThrowException() { + CreateBookRequestDto createBookDto = getCreateBookRequestDto(); + String expectedErrorMessage = "Book with ISBN " + VALID_BOOK_ISBN + " already exists"; + + when(bookRepository.findBookByIsbn(VALID_BOOK_ISBN)).thenReturn(Optional.of(new Book())); + + DuplicateIsbnException exception = + assertThrows(DuplicateIsbnException.class, () -> bookService.save(createBookDto)); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Find all books (return list of BookDto)") + void findAll_ValidRequest_ReturnListOfBookDto() { + Pageable pageable = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); + Book book = new Book(); + book.setId(VALID_BOOK_ID); + BookDto bookDto = getBookDto(getCreateBookRequestDto()); + + when(bookRepository.findAllBooks(pageable)).thenReturn(List.of(book)); + when(bookMapper.toDto(book)).thenReturn(bookDto); + + List books = bookService.findAll(pageable); + + assertEquals(EXPECTED_LIST_SIZE, books.size()); + assertEquals(VALID_BOOK_TITLE, books.getFirst().getTitle()); + } + + @Test + @DisplayName("Find book by id (return BookDto)") + void findById_ValidRequest_ReturnBookDto() { + Book book = new Book(); + book.setId(VALID_BOOK_ID); + BookDto bookDto = getBookDto(getCreateBookRequestDto()); + + when(bookRepository.findBookById(VALID_BOOK_ID)).thenReturn(Optional.of(book)); + when(bookMapper.toDto(book)).thenReturn(bookDto); + + BookDto foundBook = bookService.findById(VALID_BOOK_ID); + + assertEquals(VALID_BOOK_TITLE, foundBook.getTitle()); + } + + @Test + @DisplayName("Find book by id (throws EntityNotFoundException)") + void findById_NonExistentBook_ThrowException() { + when(bookRepository.findBookById(VALID_BOOK_ID)).thenReturn(Optional.empty()); + String expectedErrorMessage = "Book not found by id " + VALID_BOOK_ID; + + EntityNotFoundException exception = assertThrows(EntityNotFoundException.class, () -> + bookService.findById(VALID_BOOK_ID)); + + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Update book by id (return updated BookDto)") + void updateById_ValidRequest_ReturnBookDto() { + Book existingBook = new Book(); + existingBook.setId(VALID_BOOK_ID); + existingBook.setIsbn(VALID_BOOK_ISBN); + + Book updatedBook = new Book(); + updatedBook.setId(VALID_BOOK_ID); + CreateBookRequestDto updateBookDto = getCreateBookRequestDto(); + BookDto updatedBookDto = getBookDto(updateBookDto); + + when(bookRepository.findById(VALID_BOOK_ID)).thenReturn(Optional.of(existingBook)); + when(bookMapper.toEntity(updateBookDto)).thenReturn(updatedBook); + when(bookRepository.save(updatedBook)).thenReturn(updatedBook); + when(bookMapper.toDto(updatedBook)).thenReturn(updatedBookDto); + + BookDto result = bookService.updateById(VALID_BOOK_ID, updateBookDto); + + assertEquals(VALID_BOOK_TITLE, result.getTitle()); + assertEquals(VALID_BOOK_AUTHOR, result.getAuthor()); + } + + @Test + @DisplayName("Delete book by id") + void delete_ValidRequest_DeleteBook() { + bookService.delete(VALID_BOOK_ID); + verify(bookRepository, times(1)).deleteById(VALID_BOOK_ID); + } + + @Test + @DisplayName("Search books with parameters (return list of BookDto)") + void search_ValidParameters_ReturnListOfBookDto() { + BookSearchParametersDto params = getSearchParams(VALID_MIN_PRICE, VALID_MAX_PRICE); + Book book = getMockBook(); + Pageable pageable = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); + BookDto bookDto = getBookDto(getCreateBookRequestDto()); + Specification spec = (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + + when(specificationBuilder.build(params)).thenReturn(spec); + when(bookRepository.findAll(spec, pageable)).thenReturn(new PageImpl<>(List.of(book))); + when(bookMapper.toDto(book)).thenReturn(bookDto); + + List books = bookService.search(params, pageable); + + assertEquals(EXPECTED_LIST_SIZE, books.size()); + assertEquals(VALID_BOOK_TITLE, books.getFirst().getTitle()); + } + + @Test + @DisplayName("Search books with negative price (ignore negative min price)") + void search_NegativeMinPrice_IgnoreNegativeMinPrice() { + BookSearchParametersDto params = getSearchParams(NEGATIVE_PRICE, VALID_MAX_PRICE); + Book book = getMockBook(); + Pageable pageable = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); + BookDto bookDto = getBookDto(getCreateBookRequestDto()); + Specification spec = (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + + when(specificationBuilder.build(params)).thenReturn(spec); + when(bookRepository.findAll(spec, pageable)).thenReturn(new PageImpl<>(List.of(book))); + when(bookMapper.toDto(book)).thenReturn(bookDto); + + List books = assertDoesNotThrow(() -> bookService.search(params, pageable)); + assertEquals(EXPECTED_LIST_SIZE, books.size()); + } + + @Test + @DisplayName("Search books with negative price (ignore smaller max price)") + void search_ZeroMaxPrice_IgnoreSmallerMaxPrice() { + BookSearchParametersDto params = getSearchParams(NEGATIVE_PRICE, ZERO_MAX_PRICE); + Book book = getMockBook(); + Pageable pageable = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); + BookDto bookDto = getBookDto(getCreateBookRequestDto()); + Specification spec = (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + + when(specificationBuilder.build(params)).thenReturn(spec); + when(bookRepository.findAll(spec, pageable)).thenReturn(new PageImpl<>(List.of(book))); + when(bookMapper.toDto(book)).thenReturn(bookDto); + + List books = assertDoesNotThrow(() -> bookService.search(params, pageable)); + assertEquals(EXPECTED_LIST_SIZE, books.size()); + } + + @Test + @DisplayName("Get all books by category id (return list of BookDtoWithoutCategoryIds)") + void getAllBookByCategoryId() { + Pageable pageable = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); + Book book = new Book(); + BookDtoWithoutCategoryIds bookDtoWithoutCategoryIds = new BookDtoWithoutCategoryIds(); + bookDtoWithoutCategoryIds.setTitle(VALID_BOOK_TITLE); + bookDtoWithoutCategoryIds.setAuthor(VALID_BOOK_AUTHOR); + + when(bookRepository.findAllBooksByCategoryId(VALID_BOOK_ID, pageable)) + .thenReturn(List.of(book)); + when(bookMapper.toDtoWithoutCategories(book)).thenReturn(bookDtoWithoutCategoryIds); + + List books = + bookService.getAllBookByCategoryId(VALID_BOOK_ID, pageable); + + assertEquals(EXPECTED_LIST_SIZE, books.size()); + assertEquals(VALID_BOOK_TITLE, books.getFirst().getTitle()); + } + + private CreateBookRequestDto getCreateBookRequestDto() { + CreateBookRequestDto createBookDto = new CreateBookRequestDto(); + createBookDto.setTitle(VALID_BOOK_TITLE); + createBookDto.setAuthor(VALID_BOOK_AUTHOR); + createBookDto.setIsbn(VALID_BOOK_ISBN); + createBookDto.setPrice(VALID_BOOK_PRICE); + createBookDto.setCategoryIds(VALID_CATEGORY_ID); + createBookDto.setDescription(VALID_BOOK_DESCRIPTION); + createBookDto.setCoverImage(VALID_BOOK_COVER_IMAGE); + return createBookDto; + } + + private BookDto getBookDto(CreateBookRequestDto dto) { + BookDto bookDto = new BookDto(); + bookDto.setTitle(dto.getTitle()); + bookDto.setAuthor(dto.getAuthor()); + bookDto.setIsbn(dto.getIsbn()); + bookDto.setPrice(dto.getPrice()); + bookDto.setDescription(dto.getDescription()); + bookDto.setCoverImage(dto.getCoverImage()); + return bookDto; + } + + private Book getMockBook() { + Book book = new Book(); + book.setTitle(VALID_BOOK_TITLE); + book.setAuthor(VALID_BOOK_AUTHOR); + return book; + } + + private static BookSearchParametersDto getSearchParams(int minPrice, int maxPrice) { + return new BookSearchParametersDto( + new String[]{VALID_BOOK_AUTHOR}, + VALID_BOOK_TITLE, + VALID_BOOK_ISBN, + minPrice, + maxPrice + ); + } +} diff --git a/src/test/java/mate/academy/bookstore/service/impl/CategoryServiceImplTest.java b/src/test/java/mate/academy/bookstore/service/impl/CategoryServiceImplTest.java new file mode 100644 index 0000000..677c8d9 --- /dev/null +++ b/src/test/java/mate/academy/bookstore/service/impl/CategoryServiceImplTest.java @@ -0,0 +1,162 @@ +package mate.academy.bookstore.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import mate.academy.bookstore.dto.category.CategoryDto; +import mate.academy.bookstore.exception.DuplicateEntityException; +import mate.academy.bookstore.exception.EntityNotFoundException; +import mate.academy.bookstore.mapper.CategoryMapper; +import mate.academy.bookstore.model.Category; +import mate.academy.bookstore.repository.category.CategoryRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +class CategoryServiceImplTest { + private static final Long VALID_CATEGORY_ID = 1L; + private static final String VALID_CATEGORY_NAME = "Fiction"; + private static final int PAGE_SIZE = 5; + private static final String NEW_VALID_CATEGORY_NAME = "New Fiction"; + private static final int EXPECTED_LIST_SIZE = 1; + private static final int NUMBER_OF_INVOCATIONS = 1; + + @Mock + private CategoryRepository categoryRepository; + + @Mock + private CategoryMapper categoryMapper; + + @InjectMocks + private CategoryServiceImpl categoryService; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Save a valid category (return CategoryDto)") + void save_ValidCategory_ReturnCategoryDto() { + CategoryDto categoryDto = getCategoryDto(); + Category category = getCategory(); + + when(categoryMapper.toEntity(categoryDto)).thenReturn(category); + when(categoryRepository.findByName(VALID_CATEGORY_NAME)).thenReturn(null); + when(categoryRepository.save(category)).thenReturn(category); + when(categoryMapper.toDto(category)).thenReturn(categoryDto); + + CategoryDto savedCategory = categoryService.save(categoryDto); + + assertEquals(VALID_CATEGORY_NAME, savedCategory.getName()); + } + + @Test + @DisplayName("Save a category with duplicate name (throws exception)") + void save_CategoryWithDuplicateName_ThrowException() { + CategoryDto categoryDto = getCategoryDto(); + String expectedErrorMessage = + "Category with name " + VALID_CATEGORY_NAME + " already exists"; + + when(categoryRepository.findByName(VALID_CATEGORY_NAME)).thenReturn(new Category()); + + DuplicateEntityException exception = assertThrows(DuplicateEntityException.class, () -> + categoryService.save(categoryDto)); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Find all categories (return list of CategoryDto)") + void findAll_ValidRequest_ReturnListOfCategoryDto() { + Pageable pageable = Pageable.ofSize(PAGE_SIZE); + Category category = getCategory(); + CategoryDto categoryDto = getCategoryDto(); + + when(categoryRepository.findAll(pageable)).thenReturn(new PageImpl<>(List.of(category))); + when(categoryMapper.toDto(category)).thenReturn(categoryDto); + + List categories = categoryService.findAll(pageable); + + assertEquals(EXPECTED_LIST_SIZE, categories.size()); + assertEquals(VALID_CATEGORY_NAME, categories.getFirst().getName()); + } + + @Test + @DisplayName("Find category by id (return CategoryDto)") + void getById_ValidRequest_ReturnCategoryDto() { + Category category = getCategory(); + CategoryDto categoryDto = getCategoryDto(); + + when(categoryRepository.findById(VALID_CATEGORY_ID)).thenReturn(Optional.of(category)); + when(categoryMapper.toDto(category)).thenReturn(categoryDto); + + CategoryDto foundCategory = categoryService.getById(VALID_CATEGORY_ID); + + assertEquals(VALID_CATEGORY_NAME, foundCategory.getName()); + } + + @Test + @DisplayName("Find category by id (throws exception)") + void getById_NonExistentCategory_ThrowException() { + when(categoryRepository.findById(VALID_CATEGORY_ID)).thenReturn(Optional.empty()); + String expectedErrorMessage = "Category not found by id " + VALID_CATEGORY_ID; + + EntityNotFoundException exception = assertThrows(EntityNotFoundException.class, () -> + categoryService.getById(VALID_CATEGORY_ID)); + + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Update category (return updated CategoryDto)") + void update_ValidRequest_ReturnCategoryDto() { + Category category = getCategory(); + CategoryDto updateCategoryDto = new CategoryDto(); + updateCategoryDto.setName(NEW_VALID_CATEGORY_NAME); + + when(categoryRepository.findById(VALID_CATEGORY_ID)).thenReturn(Optional.of(category)); + when(categoryRepository.findByName(updateCategoryDto.getName())).thenReturn(null); + when(categoryMapper.toEntity(updateCategoryDto)).thenReturn(category); + when(categoryRepository.save(category)).thenReturn(category); + when(categoryMapper.toDto(category)).thenReturn(updateCategoryDto); + + CategoryDto result = categoryService.update(VALID_CATEGORY_ID, updateCategoryDto); + assertEquals(updateCategoryDto.getName(), result.getName()); + } + + @Test + @DisplayName("Delete category by id") + void delete_ValidRequest_DeleteCategory() { + Category category = getCategory(); + category.setId(VALID_CATEGORY_ID); + + when(categoryRepository.findById(VALID_CATEGORY_ID)).thenReturn(Optional.of(category)); + + categoryService.delete(VALID_CATEGORY_ID); + verify(categoryRepository, times(NUMBER_OF_INVOCATIONS)).deleteById(VALID_CATEGORY_ID); + } + + private static Category getCategory() { + Category category = new Category(); + category.setId(VALID_CATEGORY_ID); + category.setName(VALID_CATEGORY_NAME); + return category; + } + + private static CategoryDto getCategoryDto() { + CategoryDto categoryDto = new CategoryDto(); + categoryDto.setId(VALID_CATEGORY_ID); + categoryDto.setName(VALID_CATEGORY_NAME); + return categoryDto; + } +} diff --git a/src/test/resources/database/insert-data-into-db.sql b/src/test/resources/database/insert-data-into-db.sql index a42708e..3551351 100644 --- a/src/test/resources/database/insert-data-into-db.sql +++ b/src/test/resources/database/insert-data-into-db.sql @@ -1,20 +1,18 @@ --- Inserting data into the books table -INSERT INTO books (title, author, isbn, price, description, cover_image) +INSERT INTO books (id, title, author, isbn, price, description) VALUES - ('Kobzar', 'Taras Shevchenko', '978-1-1516-4732-0', 34.99, 'Kobzar, Taras Shevchenko.', 'kobzar.jpg'), - ('Earth', 'Olha Kobylianska', '978-7-3664-5711-2', 14.99, 'Earth, Olha Kobylianska', 'earth.jpg'), - ('Tiger hunters', 'Ivan Bahriany', '978-8-9176-0894-6', 16.99, 'Tiger hunters, Ivan Bahriany', 'tiger-hunters.jpg'), - ('Marusja Churai', 'Lesya Ukrainka', '978-0-8386-9622-4', 9.99, 'Marusja Churai, Lesya Ukrainka', 'marusja-churai.jpg'), - ('The Tale of Igor\'s Campaign', 'Anonymous', '978-6-3292-3392-3', 25.99, 'The Tale of Igor\'s Campaign, Anonymous', 'igor-campaign.jpg'); + (1, 'Kobzar', 'Taras Shevchenko', '978-1-1516-4732-0', 34.99, 'Awesome description...'), + (2, 'Earth', 'Olha Kobylianska', '978-7-3664-5711-2', 14.99, 'Awesome description...'), + (3, 'Tiger hunters', 'Ivan Bahriany', '978-8-9176-0894-6', 16.99, 'Awesome description...'), + (4, 'Marusja Churai', 'Lesya Ukrainka', '978-0-8386-9622-4', 9.99, 'Awesome description...'), + (5, 'Haidamaky', 'Taras Shevchenko', '978-6-3292-3392-3', 25.99, 'Awesome description...'); --- Inserting data into the categories table -INSERT INTO categories (name, description) +INSERT INTO categories (id, name, description) VALUES - ('Poetry', 'Literary genre that uses language for its poetic...'), - ('Novel', 'Prose literary genre that describes various situations...'), - ('Epic', 'Literary genre that describes great events, historical episodes...'); + (1, 'Poetry', 'Awesome description...'), + (2, 'Novel', 'Awesome description...'), + (3, 'Epic', 'Awesome description...'), + (4, 'Drama', 'Awesome description...'); --- Inserting data into the books_categories table INSERT INTO books_categories (book_id, category_id) VALUES (1, 1),