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;