diff --git a/docker-compose.yml b/docker-compose.yml
index f76996d..3cd2481 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,7 +33,7 @@ services:
SPRING_DATASOURCE_USERNAME: "${MYSQLDB_USER}"
SPRING_DATASOURCE_PASSWORD: "${MYSQLDB_PASSWORD}"
SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: "org.hibernate.dialect.MySQL8Dialect"
- JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
+ JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT}"
depends_on:
mysqldb:
condition: service_healthy
diff --git a/pom.xml b/pom.xml
index 30ff2e8..aaaf362 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,11 +19,35 @@
UTF-8
0.2.0
1.5.5.Final
+ 1.20.6
0.12.6
4.27.0
1.18.32
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ mysql
+ ${testcontainers.version}
+ test
+
org.springframework.boot
spring-boot-starter-data-jpa
diff --git a/src/main/java/mate/academy/bookshop/dto/book/BookDto.java b/src/main/java/mate/academy/bookshop/dto/book/BookDto.java
index c3b138e..d146557 100644
--- a/src/main/java/mate/academy/bookshop/dto/book/BookDto.java
+++ b/src/main/java/mate/academy/bookshop/dto/book/BookDto.java
@@ -3,8 +3,10 @@
import java.math.BigDecimal;
import java.util.Set;
import lombok.Data;
+import lombok.experimental.Accessors;
@Data
+@Accessors(chain = true)
public class BookDto {
private Long id;
diff --git a/src/main/java/mate/academy/bookshop/dto/book/BookDtoWithoutCategoryIds.java b/src/main/java/mate/academy/bookshop/dto/book/BookDtoWithoutCategoryIds.java
index 4101a00..964c825 100644
--- a/src/main/java/mate/academy/bookshop/dto/book/BookDtoWithoutCategoryIds.java
+++ b/src/main/java/mate/academy/bookshop/dto/book/BookDtoWithoutCategoryIds.java
@@ -2,8 +2,10 @@
import java.math.BigDecimal;
import lombok.Data;
+import lombok.experimental.Accessors;
@Data
+@Accessors(chain = true)
public class BookDtoWithoutCategoryIds {
private Long id;
diff --git a/src/main/java/mate/academy/bookshop/dto/book/CreateBookRequestDto.java b/src/main/java/mate/academy/bookshop/dto/book/CreateBookRequestDto.java
index 13b43b9..12acd9a 100644
--- a/src/main/java/mate/academy/bookshop/dto/book/CreateBookRequestDto.java
+++ b/src/main/java/mate/academy/bookshop/dto/book/CreateBookRequestDto.java
@@ -7,10 +7,12 @@
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
+import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.constraints.URL;
@Data
+@Accessors(chain = true)
public class CreateBookRequestDto {
@NotNull
@Size(min = 1, max = 100,
diff --git a/src/main/java/mate/academy/bookshop/dto/category/CategoryRequestDto.java b/src/main/java/mate/academy/bookshop/dto/category/CategoryRequestDto.java
index fd7963d..5060aa6 100644
--- a/src/main/java/mate/academy/bookshop/dto/category/CategoryRequestDto.java
+++ b/src/main/java/mate/academy/bookshop/dto/category/CategoryRequestDto.java
@@ -3,8 +3,10 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
+import lombok.experimental.Accessors;
@Data
+@Accessors(chain = true)
public class CategoryRequestDto {
@NotBlank
@Size(min = 2, max = 100,
diff --git a/src/main/java/mate/academy/bookshop/dto/category/CategoryResponseDto.java b/src/main/java/mate/academy/bookshop/dto/category/CategoryResponseDto.java
index 3f9ad0b..b1c3c03 100644
--- a/src/main/java/mate/academy/bookshop/dto/category/CategoryResponseDto.java
+++ b/src/main/java/mate/academy/bookshop/dto/category/CategoryResponseDto.java
@@ -1,8 +1,10 @@
package mate.academy.bookshop.dto.category;
import lombok.Data;
+import lombok.experimental.Accessors;
@Data
+@Accessors(chain = true)
public class CategoryResponseDto {
private Long id;
diff --git a/src/main/java/mate/academy/bookshop/exceptions/EntityNotFoundException.java b/src/main/java/mate/academy/bookshop/exceptions/EntityNotFoundException.java
index 8587cb0..2ee4a1b 100644
--- a/src/main/java/mate/academy/bookshop/exceptions/EntityNotFoundException.java
+++ b/src/main/java/mate/academy/bookshop/exceptions/EntityNotFoundException.java
@@ -1,5 +1,9 @@
package mate.academy.bookshop.exceptions;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) {
super(message);
diff --git a/src/test/java/mate/academy/bookshop/config/CustomMySqlContainer.java b/src/test/java/mate/academy/bookshop/config/CustomMySqlContainer.java
new file mode 100644
index 0000000..f3256b6
--- /dev/null
+++ b/src/test/java/mate/academy/bookshop/config/CustomMySqlContainer.java
@@ -0,0 +1,33 @@
+package mate.academy.bookshop.config;
+
+import org.testcontainers.containers.MySQLContainer;
+
+public class CustomMySqlContainer extends MySQLContainer {
+ private static final String IMAGE_VERSION = "mysql:8.0";
+ private static CustomMySqlContainer container;
+
+ private CustomMySqlContainer() {
+ super(IMAGE_VERSION);
+ }
+
+ public static CustomMySqlContainer getInstance() {
+ if (container == null) {
+ container = new CustomMySqlContainer();
+ }
+ 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/mate/academy/bookshop/controller/BookControllerTest.java b/src/test/java/mate/academy/bookshop/controller/BookControllerTest.java
new file mode 100644
index 0000000..701bf0f
--- /dev/null
+++ b/src/test/java/mate/academy/bookshop/controller/BookControllerTest.java
@@ -0,0 +1,390 @@
+package mate.academy.bookshop.controller;
+
+import static org.apache.commons.lang3.builder.EqualsBuilder.reflectionEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.status;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import javax.sql.DataSource;
+import lombok.SneakyThrows;
+import mate.academy.bookshop.dto.book.BookDto;
+import mate.academy.bookshop.dto.book.CreateBookRequestDto;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+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.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.jdbc.datasource.init.ScriptUtils;
+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 org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class BookControllerTest {
+
+ protected static MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeAll
+ static void beforeAll(
+ @Autowired DataSource dataSource,
+ @Autowired WebApplicationContext webApplicationContext
+ ) throws SQLException {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .apply(springSecurity())
+ .build();
+ teardown(dataSource);
+ try (Connection connection = dataSource.getConnection()) {
+ connection.setAutoCommit(true);
+ ScriptUtils.executeSqlScript(
+ connection,
+ new ClassPathResource("database/books/add-three-default-books.sql")
+ );
+ ScriptUtils.executeSqlScript(
+ connection,
+ new ClassPathResource("database/category/add-category.sql")
+ );
+ }
+ }
+
+ @AfterAll
+ static void afterAll(@Autowired DataSource dataSource) {
+ teardown(dataSource);
+ }
+
+ @SneakyThrows
+ static void teardown(DataSource dataSource) {
+ try (Connection connection = dataSource.getConnection()) {
+ connection.setAutoCommit(true);
+ ScriptUtils.executeSqlScript(
+ connection,
+ new ClassPathResource("database/books/remove-all-books.sql")
+ );
+ ScriptUtils.executeSqlScript(
+ connection,
+ new ClassPathResource("database/category/remove-all-categories.sql")
+ );
+ }
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Find all books: returns all books successfully")
+ public void findAll_GivenBooks_SuccessAndReturnAllBooks() throws Exception {
+ // Arrange
+ List expectedBookDto = new ArrayList<>();
+
+ expectedBookDto.add(new BookDto()
+ .setId(1L)
+ .setTitle("The Great Adventure")
+ .setAuthor("John Doe")
+ .setIsbn("978-3-16-148410-0")
+ .setPrice(new BigDecimal("19.99"))
+ .setDescription("An amazing journey")
+ .setCoverImage("https://example.com/image1.jpg")
+ .setCategoryIds(Collections.emptySet())
+ );
+
+ expectedBookDto.add(new BookDto()
+ .setId(2L)
+ .setTitle("Mystery of the Night")
+ .setAuthor("Jane Smith")
+ .setIsbn("978-1-23-456789-7")
+ .setPrice(new BigDecimal("25.50"))
+ .setDescription("A thrilling mystery")
+ .setCoverImage("https://example.com/image2.jpg")
+ .setCategoryIds(Collections.emptySet())
+ );
+
+ expectedBookDto.add(new BookDto()
+ .setId(3L)
+ .setTitle("Programming in Java")
+ .setAuthor("Alice Johnson")
+ .setIsbn("978-0-12-345678-9")
+ .setPrice(new BigDecimal("35.00"))
+ .setDescription("Learn Java from scratch")
+ .setCoverImage("https://example.com/image3.jpg")
+ .setCategoryIds(Collections.emptySet())
+ );
+
+ // Act
+ MvcResult result = mockMvc.perform(get("/books")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ String jsonResponse = result.getResponse().getContentAsString();
+ JsonNode rootNode = objectMapper.readTree(jsonResponse);
+ JsonNode contentNode = rootNode.path("content");
+
+ BookDto[] actual = objectMapper.treeToValue(contentNode, BookDto[].class);
+
+ assertEquals(3, actual.length);
+ for (int i = 0; i < expectedBookDto.size(); i++) {
+ BookDto expected = expectedBookDto.get(i);
+ BookDto actualDto = actual[i];
+
+ assertEquals(expected.getId(), actualDto.getId());
+ assertEquals(expected.getTitle(), actualDto.getTitle());
+ assertEquals(expected.getAuthor(), actualDto.getAuthor());
+ assertEquals(expected.getIsbn(), actualDto.getIsbn());
+
+ BigDecimal expectedPrice = expected.getPrice().stripTrailingZeros();
+ BigDecimal actualPrice = actualDto.getPrice().stripTrailingZeros();
+ assertEquals(expectedPrice, actualPrice);
+
+ assertEquals(expected.getDescription(), actualDto.getDescription());
+ assertEquals(expected.getCoverImage(), actualDto.getCoverImage());
+ assertEquals(expected.getCategoryIds(), actualDto.getCategoryIds());
+ }
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Find book by ID: returns book successfully for valid ID")
+ public void findById_ValidBookId_SuccessAndReturnBookDto() throws Exception {
+ // Arrange
+ Long validBookId = 1L;
+
+ BookDto expected = new BookDto()
+ .setId(1L)
+ .setTitle("The Great Adventure")
+ .setAuthor("John Doe")
+ .setIsbn("978-3-16-148410-0")
+ .setPrice(new BigDecimal("19.99").setScale(2, RoundingMode.HALF_UP))
+ .setDescription("An amazing journey")
+ .setCoverImage("https://example.com/image1.jpg")
+ .setCategoryIds(Collections.emptySet());
+
+ // Act
+ MvcResult result = mockMvc.perform(get("/books/{id}", validBookId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ BookDto actualDto = objectMapper
+ .readValue(result.getResponse().getContentAsString(), BookDto.class);
+
+ assertEquals(expected, actualDto);
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Sql(scripts = "classpath:database/books/remove-abetka-and-relationship.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @Test
+ @DisplayName("Create book: creates and returns book successfully for valid request")
+ public void create_ValidRequestDto_SuccessAndReturnBookDto() throws Exception {
+ // Arrange
+ CreateBookRequestDto requestDto = new CreateBookRequestDto()
+ .setTitle("Abetka")
+ .setAuthor("Hryhoriy Falkovych")
+ .setIsbn("978-3-16-148410-0")
+ .setPrice(new BigDecimal("50.00"))
+ .setDescription("A classic Ukrainian alphabet book in"
+ + " verse with bright illustrations for children.")
+ .setCoverImage("https://example.com/image1.jpg")
+ .setCategories(List.of(1L));
+
+ BookDto expectedDto = new BookDto()
+ .setId(4L)
+ .setTitle("Abetka")
+ .setAuthor("Hryhoriy Falkovych")
+ .setIsbn("978-3-16-148410-0")
+ .setPrice(new BigDecimal("50.00"))
+ .setDescription("A classic Ukrainian alphabet book in verse "
+ + "with bright illustrations for children.")
+ .setCoverImage("https://example.com/image1.jpg")
+ .setCategoryIds(Set.of(1L));
+
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(post("/books")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ BookDto actualDto = objectMapper
+ .readValue(mvcResult.getResponse().getContentAsString(), BookDto.class);
+
+ reflectionEquals(expectedDto, actualDto, "id");
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Sql(scripts = "classpath:database/books/add-abetka-book.sql",
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "classpath:database/books/remove-abetka-and-relationship.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @Test
+ @DisplayName("Delete book: deletes book successfully for valid ID")
+ public void delete_ValidBookId_Success() throws Exception {
+ // Arrange
+ Long validBookId = 4L;
+ int expectedStatusCode = 204;
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(delete("/books/{id}", validBookId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent())
+ .andReturn();
+
+ // Assert
+ int actualStatusCode = mvcResult.getResponse().getStatus();
+ assertEquals(expectedStatusCode, actualStatusCode);
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Test
+ @Sql(scripts = "classpath:database/books/add-abetka-book.sql",
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "classpath:database/books/remove-abetka-and-relationship.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @DisplayName("Update book: updates and returns book successfully for valid ID and request")
+ public void update_ValidBookIdAndBookRequestDto_SuccessAndReturnBookDto() throws Exception {
+ // Arrange
+ MvcResult getResult = mockMvc.perform(get("/books")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ String jsonResponse = getResult.getResponse().getContentAsString();
+ JsonNode rootNode = objectMapper.readTree(jsonResponse);
+ JsonNode contentNode = rootNode.path("content");
+
+ BookDto[] actual = objectMapper.treeToValue(contentNode, BookDto[].class);
+ Long validId = Arrays.stream(actual).toList().get(actual.length - 1).getId();
+
+ CreateBookRequestDto requestDto = new CreateBookRequestDto()
+ .setTitle("Updated Title")
+ .setAuthor("Updated Author")
+ .setPrice(new BigDecimal("50.00"))
+ .setDescription("A classic Ukrainian alphabet book in verse "
+ + "with bright illustrations for children.")
+ .setCoverImage("https://example.com/image1.jpg")
+ .setCategories(List.of(1L));
+
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(put("/books/{id}", validId)
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ BookDto actualDto = objectMapper
+ .readValue(mvcResult.getResponse().getContentAsString(), BookDto.class);
+
+ assertEquals(requestDto.getTitle(), actualDto.getTitle());
+ assertEquals(requestDto.getAuthor(), actualDto.getAuthor());
+ assertEquals(requestDto.getPrice(), actualDto.getPrice());
+ assertEquals(requestDto.getDescription(), actualDto.getDescription());
+ assertEquals(requestDto.getCoverImage(), actualDto.getCoverImage());
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Test
+ @DisplayName("Update book: throws not found exception for invalid ID")
+ public void update_InvalidBookId_ThrowNotFound() throws Exception {
+ // Arrange
+ Long invalidId = 999L;
+ int expected = 404;
+
+ CreateBookRequestDto requestDto = new CreateBookRequestDto()
+ .setTitle("Non-existent Book")
+ .setAuthor("Unknown Author")
+ .setPrice(new BigDecimal("100.00"))
+ .setDescription("This book does not exist.")
+ .setCoverImage("https://example.com/non-existent.jpg")
+ .setCategories(List.of(1L));
+
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(put("/books/{id}", invalidId)
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound())
+ .andReturn();
+
+ // Assert
+ int actual = mvcResult.getResponse().getStatus();
+ assertEquals(expected, actual);
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Find book by ID: throws not found exception for invalid ID")
+ public void findById_InvalidBookId_ThrowNotFound() throws Exception {
+ // Arrange
+ Long invalidId = 999L;
+ int expected = 404;
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(get("/books/{id}", invalidId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound())
+ .andReturn();
+
+ // Assert
+ int actual = mvcResult.getResponse().getStatus();
+ assertEquals(expected, actual);
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Test
+ @DisplayName("Create book: throws bad request exception for invalid request")
+ public void create_InvalidRequestDto_ThrowException() throws Exception {
+ // Arrange
+ int expected = 400;
+
+ CreateBookRequestDto createBookRequestDto = new CreateBookRequestDto()
+ .setTitle("Non-existent Book")
+ .setAuthor("Unknown Author")
+ .setPrice(new BigDecimal("100.00"))
+ .setDescription("This book does not exist.")
+ .setCoverImage("https://example.com/non-existent.jpg");
+
+ String jsonRequest = objectMapper.writeValueAsString(createBookRequestDto);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(post("/books")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest())
+ .andReturn();
+
+ // Assert
+ int actual = mvcResult.getResponse().getStatus();
+ assertEquals(expected, actual);
+ }
+}
diff --git a/src/test/java/mate/academy/bookshop/controller/CategoryControllerTest.java b/src/test/java/mate/academy/bookshop/controller/CategoryControllerTest.java
new file mode 100644
index 0000000..d52c52a
--- /dev/null
+++ b/src/test/java/mate/academy/bookshop/controller/CategoryControllerTest.java
@@ -0,0 +1,274 @@
+package mate.academy.bookshop.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.status;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import javax.sql.DataSource;
+import lombok.SneakyThrows;
+import mate.academy.bookshop.dto.category.CategoryResponseDto;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+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.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.jdbc.datasource.init.ScriptUtils;
+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 org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class CategoryControllerTest {
+
+ protected static MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeAll
+ static void beforeAll(
+ @Autowired WebApplicationContext webApplicationContext,
+ @Autowired DataSource dataSource
+ ) throws SQLException {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .apply(springSecurity())
+ .build();
+ teardown(dataSource);
+ try (Connection connection = dataSource.getConnection()) {
+ connection.setAutoCommit(true);
+ ScriptUtils.executeSqlScript(connection,
+ new ClassPathResource("database/category/add-three-default-categories.sql"));
+ }
+ }
+
+ @AfterAll
+ static void afterAll(
+ @Autowired DataSource dataSource
+ ) {
+ teardown(dataSource);
+ }
+
+ @SneakyThrows
+ static void teardown(DataSource dataSource) {
+ try (Connection connection = dataSource.getConnection()) {
+ connection.setAutoCommit(true);
+ ScriptUtils.executeSqlScript(
+ connection,
+ new ClassPathResource("database/category/remove-all-categories.sql")
+ );
+ }
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Get all categories: returns all categories successfully")
+ public void getAll_GivenCategories_SuccessAndReturnAllCategories() throws Exception {
+ // Arrange
+ List expectedCategories = List.of(
+ new CategoryResponseDto()
+ .setId(1L)
+ .setName("Non-Fiction")
+ .setDescription("Books based on facts and real events"),
+ new CategoryResponseDto()
+ .setId(2L)
+ .setName("Science Fiction")
+ .setDescription("Books with futuristic and scientific themes"),
+ new CategoryResponseDto()
+ .setId(3L)
+ .setName("Mystery")
+ .setDescription("Books involving suspense, crime, or detective stories")
+ );
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(get("/category")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ String jsonResponse = mvcResult.getResponse().getContentAsString();
+ JsonNode rootNode = objectMapper.readTree(jsonResponse);
+ JsonNode contentNode = rootNode.path("content");
+
+ CategoryResponseDto[] actualCategories = objectMapper
+ .treeToValue(contentNode, CategoryResponseDto[].class);
+
+ assertEquals(3, actualCategories.length);
+ assertEquals(expectedCategories, Arrays.stream(actualCategories).toList());
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Get category by ID: returns category successfully for valid ID")
+ public void getCategoryById_ValidCategoryId_SuccessAndReturnCategoryResponseDto()
+ throws Exception {
+ // Arrange
+ Long validCategoryId = 2L;
+ CategoryResponseDto expectedCategory = new CategoryResponseDto()
+ .setId(2L)
+ .setName("Science Fiction")
+ .setDescription("Books with futuristic and scientific themes");
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(get("/category/{id}", validCategoryId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ CategoryResponseDto actualCategory = objectMapper
+ .readValue(mvcResult.getResponse().getContentAsString(), CategoryResponseDto.class);
+
+ assertEquals(expectedCategory, actualCategory);
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Sql(scripts = "classpath:database/category/remove-fantasy-category.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @Test
+ @DisplayName("Create category: creates and returns category successfully for valid request")
+ public void createCategory_ValidCategoryRequestDto_SuccessAndReturnCategoryResponseDto()
+ throws Exception {
+ // Arrange
+ Long fantasyCategoryId = 4L;
+ CategoryResponseDto expectedCategory = new CategoryResponseDto()
+ .setId(fantasyCategoryId)
+ .setName("Fantasy")
+ .setDescription("Book with features magic, mythical creatures, epic adventures");
+ String jsonRequest = objectMapper.writeValueAsString(expectedCategory);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(post("/category")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ CategoryResponseDto actualCategory = objectMapper
+ .readValue(mvcResult.getResponse().getContentAsString(), CategoryResponseDto.class);
+
+ assertEquals(expectedCategory.getName(), actualCategory.getName());
+ assertEquals(expectedCategory.getDescription(), actualCategory.getDescription());
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Sql(scripts = "classpath:database/category/add-fantasy-category.sql",
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "classpath:database/category/remove-update-fantasy-category.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @Test
+ @DisplayName("Update category: updates and returns category"
+ + " successfully for valid ID and request")
+ public void updateCategory_ValidCategoryIdAndRequestDto_SuccessAndReturnResponseDto()
+ throws Exception {
+ // Arrange
+ Long fantasyCategoryId = 4L;
+ CategoryResponseDto expectedCategory = new CategoryResponseDto()
+ .setId(fantasyCategoryId)
+ .setName("Update Fantasy")
+ .setDescription("Update Book with features magic,"
+ + " mythical creatures, epic adventures");
+ String jsonRequest = objectMapper.writeValueAsString(expectedCategory);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(put("/category/{id}", fantasyCategoryId)
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Assert
+ CategoryResponseDto actualCategory = objectMapper
+ .readValue(mvcResult.getResponse().getContentAsString(), CategoryResponseDto.class);
+
+ assertEquals(expectedCategory.getName(), actualCategory.getName());
+ assertEquals(expectedCategory.getDescription(), actualCategory.getDescription());
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Sql(scripts = "classpath:database/category/add-fantasy-category.sql",
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "classpath:database/category/remove-fantasy-category.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @Test
+ @DisplayName("Delete category: deletes category successfully for valid ID")
+ public void deleteCategory_ValidCategoryId_Success() throws Exception {
+ // Arrange
+ Long fantasyCategoryId = 4L;
+ int expectedStatusCode = 204;
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(delete("/category/{id}", fantasyCategoryId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent())
+ .andReturn();
+
+ // Assert
+ int actualStatusCode = mvcResult.getResponse().getStatus();
+ assertEquals(expectedStatusCode, actualStatusCode);
+ }
+
+ @WithMockUser(username = "user", roles = {"USER"})
+ @Test
+ @DisplayName("Get category by ID: returns 404 for invalid category ID")
+ public void getCategoryById_InvalidCategoryId_ThrowNotFound() throws Exception {
+ // Arrange
+ Long invalidId = 999L;
+ int expectedStatusCode = 404;
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(get("/category/{id}", invalidId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound())
+ .andReturn();
+ int actualStatusCode = mvcResult.getResponse().getStatus();
+
+ // Assert
+ assertEquals(expectedStatusCode, actualStatusCode);
+ }
+
+ @WithMockUser(username = "admin", roles = {"ADMIN"})
+ @Test
+ @DisplayName("Update category: returns 404 for invalid category ID")
+ public void updateCategory_InvalidCategoryId_ThrowNotFound() throws Exception {
+ // Arrange
+ Long invalidId = 999L;
+ int expectedStatusCode = 404;
+
+ CategoryResponseDto categoryResponseDto = new CategoryResponseDto()
+ .setId(5L)
+ .setName("Non-existent category")
+ .setDescription("This category does not exist");
+
+ String jsonRequest = objectMapper.writeValueAsString(categoryResponseDto);
+
+ // Act
+ MvcResult mvcResult = mockMvc.perform(put("/category/{id}", invalidId)
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound())
+ .andReturn();
+ int actualStatusCode = mvcResult.getResponse().getStatus();
+
+ // Assert
+ assertEquals(expectedStatusCode, actualStatusCode);
+ }
+}
diff --git a/src/test/java/mate/academy/bookshop/service/book/BookServiceImplTest.java b/src/test/java/mate/academy/bookshop/service/book/BookServiceImplTest.java
new file mode 100644
index 0000000..80ec75a
--- /dev/null
+++ b/src/test/java/mate/academy/bookshop/service/book/BookServiceImplTest.java
@@ -0,0 +1,257 @@
+package mate.academy.bookshop.service.book;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+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 mate.academy.bookshop.dto.book.BookDto;
+import mate.academy.bookshop.dto.book.BookDtoWithoutCategoryIds;
+import mate.academy.bookshop.dto.book.BookSearchParameters;
+import mate.academy.bookshop.dto.book.CreateBookRequestDto;
+import mate.academy.bookshop.exceptions.EntityNotFoundException;
+import mate.academy.bookshop.mapper.impl.BookMapperImpl;
+import mate.academy.bookshop.model.Book;
+import mate.academy.bookshop.repository.BookRepository;
+import mate.academy.bookshop.repository.specification.book.BookSpecificationBuilder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+
+@ExtendWith(MockitoExtension.class)
+class BookServiceImplTest {
+ @InjectMocks
+ private BookServiceImpl bookService;
+ @Mock
+ private BookRepository bookRepository;
+ @Mock
+ private BookMapperImpl bookMapper;
+ @Mock
+ private BookSpecificationBuilder bookSpecificationBuilder;
+
+ private Book book;
+ private Book oldBook;
+ private BookDto bookDto;
+ private CreateBookRequestDto createBookRequestDto;
+ private BookDtoWithoutCategoryIds bookDtoWithoutCategoryIds;
+
+ @BeforeEach
+ void setUp() {
+ book = new Book();
+ book.setId(1L);
+ book.setTitle("Title");
+ book.setAuthor("Author");
+ book.setIsbn("978-966-97824-4-1");
+ book.setPrice(new BigDecimal(100));
+ book.setDescription("A thrilling journey");
+ book.setCoverImage("https://www.google.com/search");
+
+ oldBook = new Book();
+ oldBook.setId(1L);
+ oldBook.setTitle("Old Title");
+ oldBook.setAuthor("Old Author");
+ oldBook.setIsbn("978-966-97824-4-1");
+ oldBook.setPrice(new BigDecimal(200));
+ oldBook.setDescription("A old thrilling journey");
+ oldBook.setCoverImage("https://www.google.com/search");
+
+ bookDto = new BookDto()
+ .setId(book.getId())
+ .setTitle(book.getTitle())
+ .setAuthor(book.getAuthor())
+ .setIsbn(book.getIsbn())
+ .setPrice(book.getPrice())
+ .setDescription(book.getDescription())
+ .setCoverImage(book.getCoverImage());
+
+ createBookRequestDto = new CreateBookRequestDto()
+ .setTitle(book.getTitle())
+ .setAuthor(book.getAuthor())
+ .setIsbn(book.getIsbn())
+ .setPrice(book.getPrice())
+ .setDescription(book.getDescription())
+ .setCoverImage(book.getCoverImage())
+ .setCategories(List.of(1L, 2L));
+
+ bookDtoWithoutCategoryIds = new BookDtoWithoutCategoryIds()
+ .setId(book.getId())
+ .setTitle(book.getTitle())
+ .setAuthor(book.getAuthor())
+ .setIsbn(book.getIsbn())
+ .setPrice(book.getPrice())
+ .setDescription(book.getDescription())
+ .setCoverImage(book.getCoverImage());
+ }
+
+ @Test
+ @DisplayName("Save book with valid request DTO returns book DTO")
+ void save_WithValidRequestDto_ReturnBookDto() {
+ when(bookMapper.toModel(createBookRequestDto)).thenReturn(book);
+ when(bookRepository.save(book)).thenReturn(book);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto expectedDto = bookDto;
+ BookDto actualDto = bookService.save(createBookRequestDto);
+
+ assertThat(actualDto).isEqualTo(expectedDto);
+ verify(bookMapper).toModel(createBookRequestDto);
+ verify(bookRepository).save(book);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Find all books with valid pageable returns all books")
+ void findAll_ValidPageable_ReturnAllBooks() {
+ Pageable pageable = PageRequest.of(0, 10);
+ PageImpl bookPage = new PageImpl<>(List.of(book), pageable, 1);
+
+ when(bookRepository.findAll(pageable)).thenReturn(bookPage);
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto expectedDto = bookDto;
+ Page actualBookDtoPage = bookService.findAll(pageable);
+
+ assertThat(actualBookDtoPage.getContent()).hasSize(1).contains(expectedDto);
+ verify(bookRepository).findAll(pageable);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Find book by valid ID returns book DTO")
+ void findBookById_ValidLongId_ReturnBookDto() {
+ when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto expectedDto = bookDto;
+ BookDto actualBookDtoById = bookService.findBookById(1L);
+
+ assertThat(actualBookDtoById).isEqualTo(expectedDto);
+ verify(bookRepository).findById(1L);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Find book by invalid ID throws EntityNotFoundException")
+ void findBookById_InvalidLongId_ThrowEntityNotFoundException() {
+ Long invalidId = 2L;
+ when(bookRepository.findById(invalidId)).thenReturn(Optional.empty());
+
+ EntityNotFoundException exception = assertThrows(EntityNotFoundException.class,
+ () -> bookService.findBookById(invalidId));
+
+ String expectedMessage = "Can't find book by id: " + invalidId;
+ String actualMessage = exception.getMessage();
+
+ assertThat(expectedMessage).isEqualTo(actualMessage);
+ verify(bookRepository).findById(invalidId);
+ }
+
+ @Test
+ @DisplayName("Delete book by valid ID removes it from the database")
+ void deleteById_ValidLongId_DeletesBook() {
+ Long validId = 3L;
+
+ bookService.deleteById(validId);
+
+ verify(bookRepository).deleteById(validId);
+ }
+
+ @Test
+ @DisplayName("Update book with valid ID and request DTO returns updated book DTO")
+ void updateById_ValidIdAndBookRequestDto_ReturnBookDto() {
+ Long validId = 1L;
+ when(bookRepository.findById(validId)).thenReturn(Optional.of(oldBook));
+ when(bookMapper.toDto(oldBook)).thenReturn(bookDto);
+ doNothing().when(bookMapper).updateModelFromDto(createBookRequestDto, oldBook);
+
+ BookDto expectedDto = bookDto;
+ BookDto actualBookDto = bookService.updateById(validId, createBookRequestDto);
+
+ assertThat(actualBookDto).isEqualTo(expectedDto);
+ verify(bookRepository).findById(validId);
+ verify(bookMapper).updateModelFromDto(createBookRequestDto, oldBook);
+ verify(bookRepository).save(oldBook);
+ verify(bookMapper).toDto(oldBook);
+ }
+
+ @Test
+ @DisplayName("Update book with invalid ID throws EntityNotFoundException")
+ void updateById_InvalidId_ThrowEntityNotFoundException() {
+ Long invalidId = 4L;
+ when(bookRepository.findById(invalidId)).thenReturn(Optional.empty());
+
+ EntityNotFoundException exception = assertThrows(EntityNotFoundException.class,
+ () -> bookService.updateById(invalidId, createBookRequestDto));
+
+ String expectedMessage = "Can't find book by id: " + invalidId;
+ String actualMessage = exception.getMessage();
+
+ assertThat(expectedMessage).isEqualTo(actualMessage);
+ verify(bookRepository).findById(invalidId);
+ }
+
+ @Test
+ @DisplayName("Search books with valid parameters returns list of book DTOs")
+ void search_WithValidParameters_ReturnListBookDto() {
+ BookSearchParameters parameters = new BookSearchParameters(
+ new String[]{"Title"},
+ new String[]{"Author"}
+ );
+ Specification mockSpecification = mock(Specification.class);
+
+ when(bookSpecificationBuilder.build(parameters)).thenReturn(mockSpecification);
+ when(bookRepository.findAll(mockSpecification)).thenReturn(List.of(book));
+ when(bookMapper.toDto(book)).thenReturn(bookDto);
+
+ BookDto expectedDto = bookDto;
+ List actualSearch = bookService.search(parameters);
+
+ assertThat(actualSearch).hasSize(1).contains(expectedDto);
+ verify(bookSpecificationBuilder).build(parameters);
+ verify(bookRepository).findAll(mockSpecification);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("Get books by valid category ID returns list of book DTOs without category IDs")
+ void getBooksByCategoryId_WithValidLongId_ReturnListBookDtoWithoutCategoryIds() {
+ Long categoryId = 1L;
+ when(bookRepository.findAllByCategoriesId(categoryId)).thenReturn(List.of(book));
+ when(bookMapper.toDtoWithoutCategories(book)).thenReturn(bookDtoWithoutCategoryIds);
+
+ BookDtoWithoutCategoryIds expectedDtoWithoutCategoryIds = bookDtoWithoutCategoryIds;
+ List actualBooksByCategoryId = bookService
+ .getBooksByCategoryId(categoryId);
+
+ assertThat(actualBooksByCategoryId).hasSize(1).contains(expectedDtoWithoutCategoryIds);
+ verify(bookRepository).findAllByCategoriesId(categoryId);
+ verify(bookMapper).toDtoWithoutCategories(book);
+ }
+
+ @Test
+ @DisplayName("Get books by invalid category ID returns empty list")
+ void getBooksByCategoryId_NoBooksFound_ReturnEmptyList() {
+ Long invalidCategoryId = 3L;
+ when(bookRepository.findAllByCategoriesId(invalidCategoryId)).thenReturn(List.of());
+
+ List actualBooksByCategoryId = bookService
+ .getBooksByCategoryId(invalidCategoryId);
+
+ assertThat(actualBooksByCategoryId).isEmpty();
+ verify(bookRepository).findAllByCategoriesId(invalidCategoryId);
+ }
+}
diff --git a/src/test/java/mate/academy/bookshop/service/category/CategoryServiceImplTest.java b/src/test/java/mate/academy/bookshop/service/category/CategoryServiceImplTest.java
new file mode 100644
index 0000000..a723106
--- /dev/null
+++ b/src/test/java/mate/academy/bookshop/service/category/CategoryServiceImplTest.java
@@ -0,0 +1,199 @@
+package mate.academy.bookshop.service.category;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import mate.academy.bookshop.dto.category.CategoryRequestDto;
+import mate.academy.bookshop.dto.category.CategoryResponseDto;
+import mate.academy.bookshop.exceptions.EntityNotFoundException;
+import mate.academy.bookshop.mapper.impl.CategoryMapperImpl;
+import mate.academy.bookshop.model.Category;
+import mate.academy.bookshop.repository.CategoryRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+@ExtendWith(MockitoExtension.class)
+class CategoryServiceImplTest {
+ @InjectMocks
+ private CategoryServiceImpl categoryService;
+
+ @Mock
+ private CategoryRepository categoryRepository;
+
+ @Mock
+ private CategoryMapperImpl categoryMapper;
+
+ private Category category;
+
+ private Category oldCategory;
+
+ private CategoryRequestDto categoryRequestDto;
+
+ private CategoryResponseDto categoryResponseDto;
+
+ @BeforeEach
+ void setUp() {
+ category = new Category();
+ category.setId(1L);
+ category.setName("Comedy");
+ category.setDescription("A little funny book");
+
+ oldCategory = new Category();
+ oldCategory.setId(1L);
+ oldCategory.setName("Comedy old version");
+ oldCategory.setDescription("A old funny book");
+
+ categoryRequestDto = new CategoryRequestDto()
+ .setName(category.getName())
+ .setDescription(category.getDescription());
+
+ categoryResponseDto = new CategoryResponseDto()
+ .setId(category.getId())
+ .setName(category.getName())
+ .setDescription(category.getDescription());
+ }
+
+ @Test
+ @DisplayName("Save category with valid request DTO returns response DTO")
+ public void save_WithValidRequestDto_ReturnResponseDto() {
+ when(categoryMapper.toEntity(categoryRequestDto)).thenReturn(category);
+ when(categoryRepository.save(category)).thenReturn(category);
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto expectedDto = categoryResponseDto;
+ CategoryResponseDto actualDto = categoryService.save(categoryRequestDto);
+
+ assertNotNull(actualDto);
+ assertThat(actualDto).isEqualTo(expectedDto);
+
+ verify(categoryMapper).toEntity(categoryRequestDto);
+ verify(categoryRepository).save(category);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Find all categories with valid pageable returns all categories")
+ public void findAll_ValidPageable_ReturnAllCategories() {
+ Pageable pageable = PageRequest.of(0, 10);
+ PageImpl categoryPage = new PageImpl<>(List.of(category), pageable, 1);
+
+ when(categoryRepository.findAll(pageable)).thenReturn(categoryPage);
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto expectedDto = categoryResponseDto;
+ Page actualPageCategory = categoryService.findAll(pageable);
+
+ assertNotNull(actualPageCategory);
+ assertThat(actualPageCategory.getContent()).hasSize(1).contains(expectedDto);
+
+ verify(categoryRepository).findAll(pageable);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Get category by valid ID returns category response DTO")
+ public void getById_ValidCategoryId_ReturnCategoryResponseDto() {
+ Long validId = 1L;
+
+ when(categoryRepository.findById(validId)).thenReturn(Optional.of(category));
+ when(categoryMapper.toDto(category)).thenReturn(categoryResponseDto);
+
+ CategoryResponseDto expectedDto = categoryResponseDto;
+ CategoryResponseDto actualCategoryById = categoryService.getById(validId);
+
+ assertThat(expectedDto).isEqualTo(actualCategoryById);
+
+ verify(categoryRepository).findById(validId);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Get category by invalid ID throws EntityNotFoundException")
+ public void getById_InvalidCategoryId_ThrowEntityNotFoundException() {
+ Long invalidId = 2L;
+
+ when(categoryRepository.findById(invalidId)).thenReturn(Optional.empty());
+
+ EntityNotFoundException entityNotFoundException = assertThrows(
+ EntityNotFoundException.class,
+ () -> categoryService.getById(invalidId));
+
+ String expectedMessage = "Can't find category by id: " + invalidId;
+ String actualMessage = entityNotFoundException.getMessage();
+
+ assertEquals(expectedMessage, actualMessage);
+
+ verify(categoryRepository).findById(invalidId);
+ }
+
+ @Test
+ @DisplayName("Update category with valid ID and "
+ + "request DTO returns updated category response DTO")
+ public void update_WithValidLongIdAndCategoryRequestDto_ReturnCategoryResponseDto() {
+ Long validId = 1L;
+
+ when(categoryRepository.findById(validId)).thenReturn(Optional.of(oldCategory));
+ doNothing().when(categoryMapper)
+ .updateModelFromDto(categoryRequestDto, oldCategory);
+ when(categoryMapper.toDto(oldCategory)).thenReturn(categoryResponseDto);
+ when(categoryRepository.save(oldCategory)).thenReturn(oldCategory);
+
+ CategoryResponseDto expectedDto = categoryResponseDto;
+ CategoryResponseDto actualUpdateCategoryResponseDto = categoryService
+ .update(validId, categoryRequestDto);
+
+ assertNotNull(actualUpdateCategoryResponseDto);
+ assertEquals(expectedDto.getName(), actualUpdateCategoryResponseDto.getName());
+ assertEquals(expectedDto.getDescription(),
+ actualUpdateCategoryResponseDto.getDescription());
+
+ verify(categoryRepository).findById(validId);
+ verify(categoryMapper).updateModelFromDto(categoryRequestDto, oldCategory);
+ verify(categoryMapper).toDto(oldCategory);
+ verify(categoryRepository).save(oldCategory);
+ }
+
+ @Test
+ @DisplayName("Update category with invalid ID throws EntityNotFoundException")
+ public void update_WithInvalidLongId_ThrowEntityNotFoundException() {
+ Long invalidId = 2L;
+
+ when(categoryRepository.findById(invalidId)).thenReturn(Optional.empty());
+ EntityNotFoundException entityNotFoundException = assertThrows(
+ EntityNotFoundException.class,
+ () -> categoryService.update(invalidId, categoryRequestDto));
+
+ String expectedMessage = "Can't find category by id: " + invalidId;
+ String actualMessage = entityNotFoundException.getMessage();
+
+ assertEquals(expectedMessage, actualMessage);
+
+ verify(categoryRepository).findById(invalidId);
+ }
+
+ @Test
+ @DisplayName("Delete category by valid ID removes it from the database")
+ public void deleteById_WithValidLongId_DeletedFromDb() {
+ Long validId = 1L;
+
+ categoryService.deleteById(validId);
+
+ verify(categoryRepository).deleteById(validId);
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 7db61f2..c779a2d 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,11 +1,6 @@
-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.37:///book_shop_test
+spring.datasource.username=test
+spring.datasource.password=test
-spring.jpa.hibernate.ddl-auto=create-drop
-spring.jpa.show-sql=true
-
-jwt.expiration = 30000000
-jwt.secret = my-very-long-secret-key-that-i-should-store-safely
+jwt.secret=my-very-long-secret-key-that-i-should-store-safely
+jwt.expiration=30000000
diff --git a/src/test/resources/database/books/add-abetka-book.sql b/src/test/resources/database/books/add-abetka-book.sql
new file mode 100644
index 0000000..ca14153
--- /dev/null
+++ b/src/test/resources/database/books/add-abetka-book.sql
@@ -0,0 +1,7 @@
+DELETE
+FROM books
+WHERE title = 'Abetka';
+INSERT INTO books (id, title, author, isbn, price, description, cover_image, is_deleted)
+VALUES (4, 'Abetka', 'Hryhoriy Falkovych', '978-617-585-236-3', 120.00,
+ 'A classic Ukrainian alphabet book in verse with bright illustrations for children.',
+ 'https://example.com/abetka-cover.jpg', false);
diff --git a/src/test/resources/database/books/add-three-default-books.sql b/src/test/resources/database/books/add-three-default-books.sql
new file mode 100644
index 0000000..80dee62
--- /dev/null
+++ b/src/test/resources/database/books/add-three-default-books.sql
@@ -0,0 +1,6 @@
+DELETE FROM books;
+INSERT INTO books (id, title, author, isbn, price, description, cover_image, is_deleted)
+VALUES
+ (1, 'The Great Adventure', 'John Doe', '978-3-16-148410-0', 19.99, 'An amazing journey', 'https://example.com/image1.jpg', false),
+ (2, 'Mystery of the Night', 'Jane Smith', '978-1-23-456789-7', 25.50, 'A thrilling mystery', 'https://example.com/image2.jpg', false),
+ (3, 'Programming in Java', 'Alice Johnson', '978-0-12-345678-9', 35.00, 'Learn Java from scratch', 'https://example.com/image3.jpg', false);
diff --git a/src/test/resources/database/books/remove-abetka-and-relationship.sql b/src/test/resources/database/books/remove-abetka-and-relationship.sql
new file mode 100644
index 0000000..a08f3d3
--- /dev/null
+++ b/src/test/resources/database/books/remove-abetka-and-relationship.sql
@@ -0,0 +1,2 @@
+DELETE FROM books_categories WHERE book_id = (SELECT id FROM books WHERE title = 'Abetka');
+DELETE FROM books WHERE title = 'Abetka';
\ No newline at end of file
diff --git a/src/test/resources/database/books/remove-all-books.sql b/src/test/resources/database/books/remove-all-books.sql
new file mode 100644
index 0000000..9f26616
--- /dev/null
+++ b/src/test/resources/database/books/remove-all-books.sql
@@ -0,0 +1 @@
+DELETE FROM books;
\ No newline at end of file
diff --git a/src/test/resources/database/category/add-category.sql b/src/test/resources/database/category/add-category.sql
new file mode 100644
index 0000000..c567e1c
--- /dev/null
+++ b/src/test/resources/database/category/add-category.sql
@@ -0,0 +1 @@
+INSERT INTO categories (id, name, description, is_deleted) VALUES (1, 'Fiction','Fictional books', false);
diff --git a/src/test/resources/database/category/add-fantasy-category.sql b/src/test/resources/database/category/add-fantasy-category.sql
new file mode 100644
index 0000000..80ae9e9
--- /dev/null
+++ b/src/test/resources/database/category/add-fantasy-category.sql
@@ -0,0 +1,2 @@
+INSERT INTO categories (id, name, description, is_deleted)
+VALUES (4, 'Fantasy', 'Book with features magic, mythical creatures, epic adventures', false);
diff --git a/src/test/resources/database/category/add-three-default-categories.sql b/src/test/resources/database/category/add-three-default-categories.sql
new file mode 100644
index 0000000..44d30c3
--- /dev/null
+++ b/src/test/resources/database/category/add-three-default-categories.sql
@@ -0,0 +1,5 @@
+DELETE FROM categories;
+INSERT INTO categories (id, name, description, is_deleted) VALUES
+ (1, 'Non-Fiction', 'Books based on facts and real events', false),
+ (2, 'Science Fiction', 'Books with futuristic and scientific themes', false),
+ (3, 'Mystery', 'Books involving suspense, crime, or detective stories', false);
diff --git a/src/test/resources/database/category/remove-all-categories.sql b/src/test/resources/database/category/remove-all-categories.sql
new file mode 100644
index 0000000..3ce60ce
--- /dev/null
+++ b/src/test/resources/database/category/remove-all-categories.sql
@@ -0,0 +1 @@
+DELETE FROM categories;
\ No newline at end of file
diff --git a/src/test/resources/database/category/remove-fantasy-category.sql b/src/test/resources/database/category/remove-fantasy-category.sql
new file mode 100644
index 0000000..86a76ae
--- /dev/null
+++ b/src/test/resources/database/category/remove-fantasy-category.sql
@@ -0,0 +1 @@
+DELETE FROM categories WHERE name = 'Fantasy'
\ No newline at end of file
diff --git a/src/test/resources/database/category/remove-update-fantasy-category.sql b/src/test/resources/database/category/remove-update-fantasy-category.sql
new file mode 100644
index 0000000..9b1a8e2
--- /dev/null
+++ b/src/test/resources/database/category/remove-update-fantasy-category.sql
@@ -0,0 +1 @@
+DELETE FROM categories WHERE name = 'Update Fantasy'
\ No newline at end of file