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