diff --git a/pom.xml b/pom.xml index f7c1ebe..0c8ef70 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ checkstyle.xml 1.5.5.Final 0.2.0 + 1.21.3 @@ -147,8 +148,45 @@ spring-boot-docker-compose + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + mysql + test + + + + org.springframework.security + spring-security-test + 7.0.2 + test + + + + org.mockito + mockito-core + 5.20.0 + test + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + @@ -177,8 +215,8 @@ org.apache.maven.plugins maven-compiler-plugin - ${java.version} - ${java.version} + 19 + 19 org.projectlombok @@ -209,6 +247,17 @@ liquibase-maven-plugin ${liquibase.version} + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + -javaagent:${settings.localRepository}/org/mockito/mockito-core/5.20.0/mockito-core-5.20.0.jar + + + diff --git a/src/main/java/com/springm/store/controller/CategoryController.java b/src/main/java/com/springm/store/controller/CategoryController.java index b15ebd5..9cebb69 100644 --- a/src/main/java/com/springm/store/controller/CategoryController.java +++ b/src/main/java/com/springm/store/controller/CategoryController.java @@ -44,7 +44,7 @@ public ResponseEntity createCategory( @GetMapping @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @Operation(summary = "Get all categories", description = "Fetch all categories") - public ResponseEntity> getAll() { + public ResponseEntity> getAllCategories() { return new ResponseEntity>( categoryService.findAll(), HttpStatus.OK @@ -56,7 +56,7 @@ public ResponseEntity> getAll() { @Operation(summary = "Get a category by ID", description = "Get a category with specified ID") public ResponseEntity getCategoryById(@PathVariable Long id) { return new ResponseEntity( - categoryService.getById(id), + categoryService.findById(id), HttpStatus.OK ); } diff --git a/src/main/java/com/springm/store/service/CategoryService.java b/src/main/java/com/springm/store/service/CategoryService.java index 9529d59..3779cda 100644 --- a/src/main/java/com/springm/store/service/CategoryService.java +++ b/src/main/java/com/springm/store/service/CategoryService.java @@ -8,7 +8,7 @@ public interface CategoryService { List findAll(); - CategoryDto getById(Long id); + CategoryDto findById(Long id); CategoryDto save(CreateCategoryRequestDto createCategoryRequestDto); diff --git a/src/main/java/com/springm/store/service/impl/CategoryServiceImpl.java b/src/main/java/com/springm/store/service/impl/CategoryServiceImpl.java index 7550201..ecf2d67 100644 --- a/src/main/java/com/springm/store/service/impl/CategoryServiceImpl.java +++ b/src/main/java/com/springm/store/service/impl/CategoryServiceImpl.java @@ -31,7 +31,7 @@ public List findAll() { } @Override - public CategoryDto getById(Long id) { + public CategoryDto findById(Long id) { Category category = categoryRepository.findById(id) .orElseThrow( () -> new EntityNotFoundException("Category with id [" @@ -50,8 +50,8 @@ public CategoryDto save(CreateCategoryRequestDto createCategoryRequestDto) { public CategoryDto update(Long id, CreateCategoryRequestDto changedCategoryDto) { Category existingCategory = categoryRepository.findById(id) .orElseThrow( - () -> new EntityNotFoundException("Category with id: " - + id + " not found!")); + () -> new EntityNotFoundException("Category with id [" + + id + "] not found!")); categoryMapper.updateCategoryFromDto(changedCategoryDto, existingCategory); categoryRepository.save(existingCategory); diff --git a/src/main/java/com/springm/store/validation/book/TitleValidator.java b/src/main/java/com/springm/store/validation/book/TitleValidator.java index d490208..fbd33ba 100644 --- a/src/main/java/com/springm/store/validation/book/TitleValidator.java +++ b/src/main/java/com/springm/store/validation/book/TitleValidator.java @@ -4,7 +4,7 @@ import jakarta.validation.ConstraintValidatorContext; public class TitleValidator implements ConstraintValidator { - private static final int MINIMUM_TITLE_LENGTH = 8; + private static final int MINIMUM_TITLE_LENGTH = 4; @Override public boolean isValid(String title, ConstraintValidatorContext constraintValidatorContext) { diff --git a/src/test/java/com/springm/store/config/CustomContainer.java b/src/test/java/com/springm/store/config/CustomContainer.java new file mode 100644 index 0000000..227edba --- /dev/null +++ b/src/test/java/com/springm/store/config/CustomContainer.java @@ -0,0 +1,33 @@ +package com.springm.store.config; + +import org.testcontainers.containers.MySQLContainer; + +public class CustomContainer extends MySQLContainer { + private static final String DB_IMAGE = "mysql:9.5.0"; + + private static CustomContainer container; + + private CustomContainer() { + super(DB_IMAGE); + } + + public static synchronized CustomContainer getInstance() { + if (container == null) { + container = new CustomContainer(); + } + return container; + } + + @Override + public void start() { + super.start(); + System.setProperty("TEST_DB_URL", container.getJdbcUrl()); + System.setProperty("TEST_DB_USERNAME", container.getUsername()); + System.setProperty("TEST_DB_PASSWORD", container.getPassword()); + } + + @Override + public void stop() { + super.stop(); + } +} diff --git a/src/test/java/com/springm/store/controller/BookControllerTest.java b/src/test/java/com/springm/store/controller/BookControllerTest.java new file mode 100644 index 0000000..377ac22 --- /dev/null +++ b/src/test/java/com/springm/store/controller/BookControllerTest.java @@ -0,0 +1,170 @@ +package com.springm.store.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.springm.store.dto.book.BookDto; +import com.springm.store.dto.book.CreateBookRequestDto; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; +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.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +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.status; +import static org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@WithMockUser(username = "admin", roles = {"ADMIN"}) +@Sql(scripts = {"classpath:database/books/add-items-to-categories-table.sql", + "classpath:database/books/add-three-items-to-books-table.sql", + "classpath:database/books/assign-categories-for-books.sql"}) +@Sql(scripts = "classpath:database/books/clear-all-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +class BookControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("Add a new book") + void createBook_ValidRequestDto_Success() throws Exception { + + CreateBookRequestDto bookRequestDto = new CreateBookRequestDto(); + bookRequestDto.setTitle("Dune"); + bookRequestDto.setAuthor("Frank Gerbert"); + bookRequestDto.setIsbn("978-0316597011"); + bookRequestDto.setPrice(BigDecimal.valueOf(35.32)); + bookRequestDto.setCategoryIds(Set.of(1L)); + + BookDto expected = new BookDto(); + expected.setTitle(bookRequestDto.getTitle()); + expected.setAuthor(bookRequestDto.getAuthor()); + expected.setIsbn(bookRequestDto.getIsbn()); + expected.setPrice(bookRequestDto.getPrice()); + expected.setCategoryIds(bookRequestDto.getCategoryIds()); + + String jsonRequest = objectMapper.writeValueAsString(bookRequestDto); + + MvcResult result = mockMvc.perform( + post("/books") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + BookDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), BookDto.class); + + assertTrue( + reflectionEquals(expected, actual, "id") + ); + } + + @Test + @DisplayName("Find all available books") + void findAll_ValidItems_ShouldReturnAllBooks() throws Exception { + MvcResult result = mockMvc.perform(get("/books") + .param("page", "0") + .param("size", "4") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + int expectedElementsCount = 3; + String jsonResponse = result.getResponse().getContentAsString(); + + Map responseMap = objectMapper.readValue( + jsonResponse, + new TypeReference<>() { + }); + List actualList = objectMapper.convertValue( + responseMap.get("content"), + new TypeReference>() { + } + ); + + assertThat(actualList) + .extracting(BookDto::getTitle) + .containsExactly("Kobzar", "Harry Potter", "The Witcher"); + assertEquals(expectedElementsCount, responseMap.get("totalElements")); + assertEquals(expectedElementsCount, ((List) responseMap.get("content")).size()); + } + + @Test + @DisplayName("Returns book with right id") + void getBookById_ValidItems_ShouldReturnBook() throws Exception { + MvcResult result = mockMvc.perform( + get("/books/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + + assertTrue(jsonResponse.contains("Kobzar")); + assertTrue(jsonResponse.contains("Taras Shevchenko")); + assertTrue(jsonResponse.contains("978-1909156548")); + } + + @Test + @DisplayName("Updates book by id") + void updateBookById_ValidInput_Success() throws Exception { + CreateBookRequestDto bookRequestDto = new CreateBookRequestDto(); + bookRequestDto.setTitle("The World of Ice and Fire"); + bookRequestDto.setAuthor("George R. R. Martin"); + bookRequestDto.setIsbn("978-0316597101"); + bookRequestDto.setPrice(BigDecimal.valueOf(69.99)); + bookRequestDto.setCategoryIds(Set.of(1L, 2L)); + + String jsonRequest = objectMapper.writeValueAsString(bookRequestDto); + + MvcResult result = mockMvc.perform( + put("/books/2") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + BookDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + BookDto.class + ); + + BookDto expected = new BookDto(); + expected.setId(2L); + expected.setTitle(bookRequestDto.getTitle()); + expected.setAuthor(bookRequestDto.getAuthor()); + expected.setIsbn(bookRequestDto.getIsbn()); + expected.setPrice(bookRequestDto.getPrice()); + expected.setCategoryIds(bookRequestDto.getCategoryIds()); + + assertTrue(reflectionEquals(expected, actual, "id")); + } + + @Test + @DisplayName("Deletes book by id") + void deleteBookById_ValidInput_Success() throws Exception { + mockMvc.perform( + delete("/books/3") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/com/springm/store/controller/CategoryControllerTest.java b/src/test/java/com/springm/store/controller/CategoryControllerTest.java new file mode 100644 index 0000000..c9a2e70 --- /dev/null +++ b/src/test/java/com/springm/store/controller/CategoryControllerTest.java @@ -0,0 +1,166 @@ +package com.springm.store.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.springm.store.dto.category.CategoryDto; +import com.springm.store.dto.category.CreateCategoryRequestDto; +import java.util.List; +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.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +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.status; +import static org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@WithMockUser(username = "admin", roles = {"ADMIN"}) +@Sql(scripts = {"classpath:database/books/categories/add-five-items-to-categories-table.sql", + "classpath:database/books/add-three-items-to-books-table.sql", + "classpath:database/books/assign-categories-for-books.sql"}) +@Sql(scripts = "classpath:database/books/clear-all-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +class CategoryControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("Create category") + void createCategory_ValidInput_Success() throws Exception { + CreateCategoryRequestDto categoryRequestDto = new CreateCategoryRequestDto(); + categoryRequestDto.setName("Novels"); + categoryRequestDto.setDescription("novels"); + + CategoryDto expected = new CategoryDto(); + expected.setName(categoryRequestDto.getName()); + expected.setDescription(categoryRequestDto.getDescription()); + + String jsonRequest = objectMapper.writeValueAsString(categoryRequestDto); + + MvcResult result = mockMvc.perform( + post("/categories") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + + CategoryDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), CategoryDto.class); + + assertTrue(reflectionEquals(expected, actual, "id")); + } + + @Test + @DisplayName("Get all existing categories") + void getAllCategories_ValidItems_ShouldReturnAllCategories() throws Exception { + MvcResult result = mockMvc.perform(get("/categories") + .param("page", "0") + .param("size", "5") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + + List actualList = objectMapper.readValue( + jsonResponse, + new TypeReference>() { + } + ); + assertThat(actualList) + .extracting(CategoryDto::getName) + .containsExactly("Fantasy", "Classic", + "Poetry", "History", "Fantastic"); + } + + @Test + @DisplayName("Return category by id") + void getCategoryById_ValidId_Success() throws Exception { + MvcResult result = mockMvc.perform( + get("/categories/4") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + + assertTrue(jsonResponse.contains("History")); + } + + @Test + @DisplayName("Update category by id") + void updateCategory_ValidInput_Success() throws Exception { + CreateCategoryRequestDto categoryRequestDto = new CreateCategoryRequestDto(); + categoryRequestDto.setName("Military"); + categoryRequestDto.setDescription("Category about military things"); + + String jsonRequest = objectMapper.writeValueAsString(categoryRequestDto); + + MvcResult result = mockMvc.perform( + put("/categories/3") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andReturn(); + + CategoryDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CategoryDto.class + ); + + CategoryDto expected = new CategoryDto(); + expected.setId(3L); + expected.setName(categoryRequestDto.getName()); + expected.setDescription(categoryRequestDto.getDescription()); + + assertEquals(expected.getName(), actual.getName()); + } + + @Test + @DisplayName("Delete category by id") + void deleteCategoryById_ValidId_Success() throws Exception { + mockMvc.perform( + delete("/categories/2") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("Get all books that belong to specific category") + void getBooksByCategoryId_ValidInput_Success() throws Exception { + MvcResult result = mockMvc.perform( + get("/categories/1/books") + .param("page", "0") + .param("size", "5") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + + List actual = objectMapper.readValue( + jsonResponse, + new TypeReference>() { + } + ); + + assertEquals(3, actual.size()); + } + +} diff --git a/src/test/java/com/springm/store/repository/BookRepositoryTest.java b/src/test/java/com/springm/store/repository/BookRepositoryTest.java new file mode 100644 index 0000000..a0ead05 --- /dev/null +++ b/src/test/java/com/springm/store/repository/BookRepositoryTest.java @@ -0,0 +1,70 @@ +package com.springm.store.repository; + +import com.springm.store.model.Book; +import com.springm.store.model.Category; +import com.springm.store.repository.book.BookRepository; +import com.springm.store.repository.category.CategoryRepository; +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +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.testcontainers.junit.jupiter.Testcontainers; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DataJpaTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class BookRepositoryTest { + + @Autowired + private BookRepository bookRepository; + + @Autowired + private CategoryRepository categoryRepository; + + @Test + @DisplayName("Find all books that belongs to category with ID: 1") + void findAllByCategoryId_EqualsOne_ReturnsListWithOneBook() { + Category category = new Category(); + category.setName("Fantastic"); + category.setDescription("Fantastic books"); + categoryRepository.save(category); + + Book book = new Book(); + book.setTitle("Harry Potter"); + book.setAuthor("J. K. Rowling"); + book.setPrice(BigDecimal.TEN); + book.setIsbn("978-0547928213"); + book.setDescription("Such a good book about a boy that survived"); + book.setCoverImage("harryPotter.png"); + book.setCategories(Set.of(category)); + bookRepository.save(book); + + List actual = bookRepository.findAllByCategories_Id(1L); + + assertEquals(1, actual.size()); + } + + @Test + @DisplayName("Returns empty list, when tries to find by non-existing category.") + void findAllByCategoryId_EqualsNonExistingId_ReturnsEmptyList() { + Book book = new Book(); + book.setTitle("Harry Potter"); + book.setAuthor("J. K. Rowling"); + book.setPrice(BigDecimal.TEN); + book.setIsbn("978-0547928213"); + book.setDescription("Such a good book about a boy that survived"); + book.setCoverImage("harryPotter.png"); + book.setCategories(Set.of()); + bookRepository.save(book); + + List actual = bookRepository.findAllByCategories_Id(2L); + + assertEquals(0, actual.size()); + } + +} diff --git a/src/test/java/com/springm/store/service/BookServiceTest.java b/src/test/java/com/springm/store/service/BookServiceTest.java new file mode 100644 index 0000000..3d48cc6 --- /dev/null +++ b/src/test/java/com/springm/store/service/BookServiceTest.java @@ -0,0 +1,185 @@ +package com.springm.store.service; + +import com.springm.store.dto.book.BookDto; +import com.springm.store.dto.book.CreateBookRequestDto; +import com.springm.store.mapper.BookMapper; +import com.springm.store.model.Book; +import com.springm.store.model.Category; +import com.springm.store.repository.book.BookRepository; +import com.springm.store.repository.book.BookSpecificationBuilder; +import com.springm.store.repository.category.CategoryRepository; +import com.springm.store.service.impl.BookServiceImpl; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.Set; +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.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.test.context.bean.override.mockito.MockitoBean; +import org.testcontainers.junit.jupiter.Testcontainers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; + +@SpringBootTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class BookServiceTest { + @Autowired + private BookServiceImpl bookService; + + @MockitoBean + private BookRepository bookRepository; + + @MockitoBean + private CategoryRepository categoryRepository; + + @MockitoBean + private BookMapper bookMapper; + + @MockitoBean + private BookSpecificationBuilder bookSpecificationBuilder; + + @Test + @DisplayName("Save book with valid input") + void save_ValidInput_Success() { + Category category = new Category(); + category.setId(1L); + category.setName("Poetry"); + + when(categoryRepository.findById(1L)) + .thenReturn(Optional.of(category)); + + CreateBookRequestDto requestDto = new CreateBookRequestDto(); + requestDto.setTitle("Kobzar"); + requestDto.setAuthor("Taras Shevchenko"); + requestDto.setPrice(BigDecimal.valueOf(24)); + requestDto.setCategoryIds(Set.of(1L)); + + Book bookEntity = new Book(); + bookEntity.setTitle("Kobzar"); + bookEntity.setAuthor("Taras Shevchenko"); + bookEntity.setPrice(BigDecimal.valueOf(24)); + bookEntity.setCategories(Set.of(category)); + + BookDto expected = new BookDto(); + expected.setTitle(bookEntity.getTitle()); + expected.setAuthor(bookEntity.getAuthor()); + expected.setPrice(bookEntity.getPrice()); + expected.setCategoryIds(requestDto.getCategoryIds()); + + when(bookMapper.toModel(any(CreateBookRequestDto.class))) + .thenReturn(bookEntity); + + when(bookRepository.save(any(Book.class))) + .thenReturn(bookEntity); + + when(bookMapper.toDto(any(Book.class))) + .thenReturn(expected); + BookDto actual = bookService.save(requestDto); + + assertTrue(reflectionEquals(expected, actual, "id")); + + } + + @Test + @DisplayName("Find book by id, returns BookDto") + void findById_ValidInput_Success() { + + Book bookEntity = new Book(); + bookEntity.setId(1L); + bookEntity.setTitle("Kobzar"); + bookEntity.setAuthor("Taras Shevchenko"); + + BookDto bookDto = new BookDto(); + bookDto.setId(1L); + bookDto.setTitle("Kobzar"); + bookDto.setAuthor("Taras Shevchenko"); + + when(bookRepository.findById(1L)) + .thenReturn(Optional.of(bookEntity)); + + when(bookMapper.toDto(bookEntity)) + .thenReturn(bookDto); + + BookDto actual = bookService.findById(1L); + + assertTrue(reflectionEquals(bookDto, actual, "id")); + } + + @Test + @DisplayName("Find all books") + void findAll_Pageable_ReturnsAllBooks() { + Pageable pageable = PageRequest.of(0, 1); + + Book book = new Book(); + book.setId(1L); + + Page bookPage = + new PageImpl<>(List.of(book), pageable, 2); + + when(bookRepository.findAll(pageable)) + .thenReturn(bookPage); + + when(bookMapper.toDto(book)) + .thenReturn(new BookDto()); + + Page result = bookService.findAll(pageable); + + assertEquals(1, result.getContent().size()); + assertEquals(2, result.getTotalElements()); + } + + @Test + @DisplayName("Updates book with valid id and input dto") + void updateBookById_ValidInput_Success() { + Book bookEntity = new Book(); + bookEntity.setId(1L); + bookEntity.setTitle("Harry Potter"); + bookEntity.setAuthor("J. K. Rowling"); + + when(bookRepository.findById(1L)) + .thenReturn(Optional.of(bookEntity)); + + CreateBookRequestDto changedBookDto = new CreateBookRequestDto(); + changedBookDto.setTitle("The World of Ice and Fire"); + changedBookDto.setAuthor("George R. R. Martin"); + + BookDto bookDto = new BookDto(); + bookDto.setId(1L); + bookDto.setTitle("The World of Ice and Fire"); + bookDto.setAuthor("George R. R. Martin"); + + when(bookMapper.toDto(bookEntity)) + .thenReturn(bookDto); + + BookDto actual = bookService.updateBookById(1L, changedBookDto); + + assertTrue(reflectionEquals(bookDto, actual, "id")); + } + + @Test + @DisplayName("Deletes books with valid id") + void deleteBookById_ValidInput_Success() { + doNothing() + .when(bookRepository) + .deleteById(1L); + + bookService.deleteBookById(1L); + + verify(bookRepository) + .deleteById(1L); + } + +} diff --git a/src/test/java/com/springm/store/service/CategoryServiceTest.java b/src/test/java/com/springm/store/service/CategoryServiceTest.java new file mode 100644 index 0000000..abfb884 --- /dev/null +++ b/src/test/java/com/springm/store/service/CategoryServiceTest.java @@ -0,0 +1,130 @@ +package com.springm.store.service; + +import com.springm.store.dto.category.CategoryDto; +import com.springm.store.dto.category.CreateCategoryRequestDto; +import com.springm.store.mapper.CategoryMapper; +import com.springm.store.model.Category; +import com.springm.store.repository.category.CategoryRepository; +import com.springm.store.service.impl.CategoryServiceImpl; +import java.util.List; +import java.util.Optional; +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.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.testcontainers.junit.jupiter.Testcontainers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals; + +@SpringBootTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class CategoryServiceTest { + @Autowired + private CategoryServiceImpl categoryService; + + @Autowired + private CategoryMapper categoryMapper; + + @MockitoBean + private CategoryRepository categoryRepository; + + @Test + @DisplayName("Saves category with valid input") + void save_ValidInput_Success() { + CreateCategoryRequestDto categoryRequestDto = new CreateCategoryRequestDto(); + categoryRequestDto.setName("Poetry"); + + Category category = new Category(); + category.setId(0L); + category.setName("Poetry"); + + CategoryDto expected = new CategoryDto(); + expected.setId(1L); + expected.setName("Poetry"); + + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + CategoryDto actual = categoryService.save(categoryRequestDto); + + assertTrue(reflectionEquals(expected, actual, "id")); + } + + @Test + @DisplayName("Find category by id, returns CategoryDto") + void findById_ValidInput_Success() { + Category category = new Category(); + category.setId(1L); + category.setName("Fiction"); + + when(categoryRepository.findById(1L)).thenReturn(Optional.of(category)); + + CategoryDto expected = new CategoryDto(); + expected.setId(1L); + expected.setName("Fiction"); + + CategoryDto actual = categoryService.findById(1L); + + assertTrue(reflectionEquals(expected, actual, "id")); + } + + @Test + @DisplayName("Returns all categories that created") + void findAll_ReturnsAllCategories() { + Category category1 = new Category(); + category1.setId(1L); + category1.setName("Poetry"); + + Category category2 = new Category(); + category2.setId(2L); + category2.setName("Fiction"); + + List categories = List.of(category1, category2); + + when(categoryRepository.findAll()).thenReturn(categories); + + List actualList = categoryService.findAll(); + + assertThat(actualList) + .extracting(CategoryDto::getName) + .containsExactly("Poetry", "Fiction"); + } + + @Test + @DisplayName("Receives category id that must be updated and new name and description for replace") + void update_ValidInput_Success() { + Category category = new Category(); + category.setId(1L); + category.setName("Fiction"); + + when(categoryRepository.findById(1L)).thenReturn(Optional.of(category)); + + CreateCategoryRequestDto categoryRequestDto = new CreateCategoryRequestDto(); + categoryRequestDto.setName("History"); + + CategoryDto expected = new CategoryDto(); + expected.setName("History"); + + CategoryDto actual = categoryService.update(1L, categoryRequestDto); + + assertTrue(reflectionEquals(expected, actual, "id")); + } + + @Test + @DisplayName("Deletes category by id") + void deleteById_ValidInput_Success() { + doNothing().when(categoryRepository).deleteById(1L); + + categoryService.deleteById(1L); + + verify(categoryRepository).deleteById(1L); + } + +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..4446ada --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,4 @@ +spring: + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 6ad9bb5..3c86135 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,8 +1,7 @@ -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password=password -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.datasource.url=jdbc:tc:mysql:8.0:///test +spring.datasource.username=test +spring.datasource.password=test +spring.jpa.hibernate.ddl-auto=create-drop -jwt.expiration=500000 -jwt.secret=imustspendmoretimeonmateacademytillendofthe2025 +jwt.expiration=36000 +jwt.secret=sdawaedsadwad12312dsadczxczu6t@312 diff --git a/src/test/resources/database/books/add-items-to-categories-table.sql b/src/test/resources/database/books/add-items-to-categories-table.sql new file mode 100644 index 0000000..12050b1 --- /dev/null +++ b/src/test/resources/database/books/add-items-to-categories-table.sql @@ -0,0 +1,2 @@ +insert into categories (id, name, is_deleted) values (1, 'Fantasy', 0); +insert into categories (id, name, is_deleted) values (2, 'Classic', 0); \ No newline at end of file diff --git a/src/test/resources/database/books/add-three-items-to-books-table.sql b/src/test/resources/database/books/add-three-items-to-books-table.sql new file mode 100644 index 0000000..af6a9c4 --- /dev/null +++ b/src/test/resources/database/books/add-three-items-to-books-table.sql @@ -0,0 +1,3 @@ +insert into books (id, title, author, isbn, price, is_deleted) values (1, 'Kobzar', 'Taras Shevchenko', '978-1909156548', 49.99, 0); +insert into books (id, title, author, isbn, price, is_deleted) values (2, 'Harry Potter', 'J. K. Rowling', '978-1408855652', 39.99, 0); +insert into books (id, title, author, isbn, price, is_deleted) values (3, 'The Witcher', 'Andrzej Sapkowski', '978-0316597739', 29.99, 0); diff --git a/src/test/resources/database/books/assign-categories-for-books.sql b/src/test/resources/database/books/assign-categories-for-books.sql new file mode 100644 index 0000000..f58b632 --- /dev/null +++ b/src/test/resources/database/books/assign-categories-for-books.sql @@ -0,0 +1,5 @@ +insert into books_categories (book_id, category_id) values (1, 1); +insert into books_categories (book_id, category_id) values (2, 1); +insert into books_categories (book_id, category_id) values (2, 2); +insert into books_categories (book_id, category_id) values (3, 1); +insert into books_categories (book_id, category_id) values (3, 2); diff --git a/src/test/resources/database/books/categories/add-five-items-to-categories-table.sql b/src/test/resources/database/books/categories/add-five-items-to-categories-table.sql new file mode 100644 index 0000000..9f8a2dc --- /dev/null +++ b/src/test/resources/database/books/categories/add-five-items-to-categories-table.sql @@ -0,0 +1,5 @@ +insert into categories (id, name, is_deleted) values (1, 'Fantasy', 0); +insert into categories (id, name, is_deleted) values (2, 'Classic', 0); +insert into categories (id, name, is_deleted) values (3, 'Poetry', 0); +insert into categories (id, name, is_deleted) values (4, 'History', 0); +insert into categories (id, name, is_deleted) values (5, 'Fantastic', 0); \ No newline at end of file diff --git a/src/test/resources/database/books/clear-all-tables.sql b/src/test/resources/database/books/clear-all-tables.sql new file mode 100644 index 0000000..8352479 --- /dev/null +++ b/src/test/resources/database/books/clear-all-tables.sql @@ -0,0 +1,5 @@ +delete from books_categories; + +delete from categories; + +delete from books;