diff --git a/pom.xml b/pom.xml
index 1971709..0f766ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
0.2.0
1.5.5.Final
0.11.5
+ 1.19.0
@@ -73,11 +74,6 @@
org.springframework.boot
spring-boot-starter-validation
-
- com.h2database
- h2
- test
-
org.liquibase
liquibase-core
@@ -105,24 +101,57 @@
jjwt-api
${jjwt.version}
-
io.jsonwebtoken
jjwt-impl
${jjwt.version}
runtime
-
io.jsonwebtoken
jjwt-jackson
${jjwt.version}
runtime
-
+
+ org.springframework.data
+ spring-data-jdbc
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ mysql
+ test
+
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
-
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ 1.21.3
+ pom
+ import
+
+
+
+
+
org.springframework.boot
diff --git a/src/main/java/bookrepo/repository/book/BookRepository.java b/src/main/java/bookrepo/repository/book/BookRepository.java
index e15232b..c350afd 100644
--- a/src/main/java/bookrepo/repository/book/BookRepository.java
+++ b/src/main/java/bookrepo/repository/book/BookRepository.java
@@ -2,9 +2,16 @@
import bookrepo.model.Book;
import java.util.List;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
public interface BookRepository extends JpaRepository, JpaSpecificationExecutor {
List findAllByCategories_Id(Long categoryId);
+
+ @Query("SELECT b FROM Book b JOIN FETCH b.categories WHERE b.id = :id")
+ Optional findByIdWithCategories(@Param("id") Long id);
+
}
diff --git a/src/test/java/bookrepo/BookRepoApplicationTests.java b/src/test/java/bookrepo/BookRepoApplicationTests.java
index eb72a70..1f378f0 100644
--- a/src/test/java/bookrepo/BookRepoApplicationTests.java
+++ b/src/test/java/bookrepo/BookRepoApplicationTests.java
@@ -2,10 +2,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
-@ActiveProfiles("test")
class BookRepoApplicationTests {
@Test
void contextLoads() {
diff --git a/src/test/java/bookrepo/config/CustomMySqlContainer.java b/src/test/java/bookrepo/config/CustomMySqlContainer.java
new file mode 100644
index 0000000..93f22c2
--- /dev/null
+++ b/src/test/java/bookrepo/config/CustomMySqlContainer.java
@@ -0,0 +1,33 @@
+package bookrepo.config;
+
+import org.testcontainers.containers.MySQLContainer;
+
+public class CustomMySqlContainer extends MySQLContainer {
+ private static final String DB_IMAGE = "mysql:8";
+
+ private static CustomMySqlContainer mysqlContainer;
+
+ private CustomMySqlContainer() {
+ super(DB_IMAGE);
+ }
+
+ public static synchronized CustomMySqlContainer getInstance() {
+ if (mysqlContainer == null) {
+ mysqlContainer = new CustomMySqlContainer();
+ }
+ return mysqlContainer;
+ }
+
+ @Override
+ public void start() {
+ super.start();
+ System.setProperty("TEST_DB_URL", mysqlContainer.getJdbcUrl());
+ System.setProperty("TEST_DB_USERNAME", mysqlContainer.getUsername());
+ System.setProperty("TEST_DB_PASSWORD", mysqlContainer.getPassword());
+ }
+
+ @Override
+ public void stop() {
+ }
+}
+
diff --git a/src/test/java/bookrepo/controller/BookControllerTest.java b/src/test/java/bookrepo/controller/BookControllerTest.java
new file mode 100644
index 0000000..cd98829
--- /dev/null
+++ b/src/test/java/bookrepo/controller/BookControllerTest.java
@@ -0,0 +1,453 @@
+package bookrepo.controller;
+
+import static bookrepo.util.TestUtil.createBookRequestDto;
+import static bookrepo.util.TestUtil.createValidBookRequestDto;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+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 bookrepo.dto.book.BookDto;
+import bookrepo.dto.book.BookDtoWithoutCategoryIds;
+import bookrepo.dto.book.CreateBookRequestDto;
+import bookrepo.mapper.BookMapper;
+import bookrepo.model.Book;
+import bookrepo.model.Category;
+import bookrepo.repository.book.BookRepository;
+import bookrepo.repository.category.CategoryRepository;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.util.List;
+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.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Sql(scripts = {
+ "classpath:database/delete-data-from-tables.sql",
+ "classpath:database/book/add-books-to-table.sql",
+ "classpath:database/category/add-categories-to-category-table.sql",
+ "classpath:database/add-categories-to-books.sql"
+}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+@Sql(scripts = "classpath:database/delete-data-from-tables.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+class BookControllerTest {
+
+ protected static MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Autowired
+ private CategoryRepository categoryRepository;
+
+ @Autowired
+ private BookMapper bookMapper;
+
+ @BeforeAll
+ static void beforeAll(@Autowired WebApplicationContext applicationContext) {
+ mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @AfterAll
+ static void afterAll(@Autowired BookRepository bookRepository,
+ @Autowired CategoryRepository categoryRepository) {
+ bookRepository.deleteAll();
+ categoryRepository.deleteAll();
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get all books")
+ void findAll_WithUserRole_ShouldReturnPageOfBooks() throws Exception {
+ // Given
+ BookDto expectedDto = bookRepository.findByIdWithCategories(1L)
+ .map(bookMapper::toDto)
+ .orElseThrow();
+
+ // When
+ MvcResult result = mockMvc.perform(get("/books")
+ .param("page", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ String responseContent = result.getResponse().getContentAsString();
+
+ JsonNode root = objectMapper.readTree(responseContent);
+ List books = objectMapper.readValue(
+ root.get("content").toString(),
+ new TypeReference>() {
+ }
+ );
+
+ assertThat(books).isNotEmpty();
+ BookDto actualDto = books.get(0);
+
+ assertThat(actualDto.getId()).isEqualTo(expectedDto.getId());
+ assertThat(actualDto.getTitle()).isEqualTo("Effective Java");
+ assertThat(actualDto.getAuthor()).isEqualTo("Joshua Bloch");
+ assertThat(actualDto.getIsbn()).isEqualTo("9780134685991");
+ assertThat(actualDto.getPrice()).isEqualTo(BigDecimal.valueOf(49.99));
+ assertThat(actualDto.getCategoryIds()).containsExactlyInAnyOrder(1L, 2L);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get book by ID")
+ void getBookById_WithUserRole_ShouldReturnBook() throws Exception {
+ // Given
+ BookDto expectedDto = bookRepository.findByIdWithCategories(1L)
+ .map(bookMapper::toDto)
+ .orElseThrow();
+
+ // When
+ MvcResult result = mockMvc.perform(get("/books/{id}", expectedDto.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDto actualDto = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDto.class
+ );
+ assertEquals(expectedDto, actualDto);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Search books")
+ void searchBooks_WithUserRole_ShouldReturnBooks() throws Exception {
+ // When
+ MvcResult result = mockMvc.perform(get("/books/search")
+ .param("titles", "Effective Java")
+ .param("authors", "Joshua Bloch"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDto[] books = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDto[].class
+ );
+ assertNotNull(books);
+ assertEquals(1, books.length);
+ assertEquals("Effective Java", books[0].getTitle());
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Create a new book")
+ void createBook_ValidRequestDto_Success() throws Exception {
+ // Given
+ long initialCount = bookRepository.count();
+ CreateBookRequestDto requestDto = createValidBookRequestDto();
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // When
+ MvcResult result = mockMvc.perform(
+ post("/books")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ // Then
+ BookDto actual = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDto.class
+ );
+ assertThat(actual)
+ .usingRecursiveComparison()
+ .ignoringFields("id", "categoryIds")
+ .isEqualTo(bookMapper.toDto(bookMapper.toModel(requestDto)));
+
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Delete book by ID")
+ void deleteBookById_WithAdminRole_ShouldDeleteBook() throws Exception {
+ // Given
+ long initialCount = bookRepository.count();
+ Book book = bookRepository.findAll().get(0);
+
+ // When
+ mockMvc.perform(delete("/books/{id}", book.getId()))
+ .andExpect(status().isNoContent());
+
+ // Then
+ assertEquals(initialCount - 1, bookRepository.count());
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Update book")
+ void updateBook_WithAdminRole_ShouldUpdateBook() throws Exception {
+ // Given
+ Book book = bookRepository.findAll().get(0);
+ CreateBookRequestDto requestDto = createBookRequestDto();
+ requestDto.setTitle("Updated Title");
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // When
+ MvcResult result = mockMvc.perform(
+ put("/books/{id}", book.getId())
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDto actual = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDto.class
+ );
+ assertNotNull(actual);
+ assertEquals("Updated Title", actual.getTitle());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Create book with USER role should return forbidden")
+ void createBook_WithUserRole_ShouldReturnForbidden() throws Exception {
+ CreateBookRequestDto requestDto = createBookRequestDto();
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ mockMvc.perform(
+ post("/books")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Delete book with USER role should return forbidden")
+ void deleteBook_WithUserRole_ShouldReturnForbidden() throws Exception {
+ mockMvc.perform(delete("/books/{id}", 1L))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @DisplayName("Access without authentication should return unauthorized")
+ void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception {
+ mockMvc.perform(get("/books"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get books by category ID")
+ void getBooksByCategoryId_WithUserRole_ShouldReturnBooks() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+
+ // When
+ MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDtoWithoutCategoryIds[] books = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDtoWithoutCategoryIds[].class
+ );
+ assertNotNull(books);
+ assertEquals(1, books.length);
+ assertEquals("Effective Java", books[0].getTitle());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get books by category ID - should return books for valid category")
+ void getBooksByCategoryId_WithValidCategory_ShouldReturnBooks() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+ Book expectedBook = bookRepository.findAll().get(0);
+ BookDtoWithoutCategoryIds expectedDto = bookMapper.toDtoWithoutCategories(expectedBook);
+
+ // When
+ MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDtoWithoutCategoryIds[] books = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDtoWithoutCategoryIds[].class
+ );
+
+ assertNotNull(books);
+ assertEquals(1, books.length);
+
+ assertThat(books[0]).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get books by invalid category ID - should return bad request")
+ void getBooksByCategoryId_WithInvalidCategoryId_ShouldReturnBadRequest() throws Exception {
+ // Given
+ String invalidCategoryId = "invalid";
+
+ // When & Then
+ mockMvc.perform(get("/categories/{id}/books", invalidCategoryId))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ @DisplayName("Get books by category ID without authentication - should return unauthorized")
+ void getBooksByCategoryId_WithoutAuthentication_ShouldReturnUnauthorized() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+
+ // When & Then
+ mockMvc.perform(get("/categories/{id}/books", category.getId()))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get books by different categories - should return correct books")
+ void getBooksByCategoryId_WithDifferentCategories_ShouldReturnCorrectBooks() throws Exception {
+ // Given
+ Category programmingCategory = categoryRepository.findById(1L).orElseThrow();
+ Category javaCategory = categoryRepository.findById(2L).orElseThrow();
+
+ // When & Then
+ MvcResult result1 = mockMvc.perform(get("/categories/{id}/books",
+ programmingCategory.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ MvcResult result2 = mockMvc.perform(get("/categories/{id}/books",
+ javaCategory.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ BookDtoWithoutCategoryIds[] books1 = objectMapper.readValue(
+ result1.getResponse().getContentAsString(),
+ BookDtoWithoutCategoryIds[].class
+ );
+
+ BookDtoWithoutCategoryIds[] books2 = objectMapper.readValue(
+ result2.getResponse().getContentAsString(),
+ BookDtoWithoutCategoryIds[].class
+ );
+
+ assertNotNull(books1);
+ assertNotNull(books2);
+ assertEquals(1, books1.length);
+ assertEquals(1, books2.length);
+
+ Book expectedBook = bookRepository.findById(1L).orElseThrow();
+ BookDtoWithoutCategoryIds expectedDto = bookMapper.toDtoWithoutCategories(expectedBook);
+
+ assertThat(books1[0]).usingRecursiveComparison().isEqualTo(expectedDto);
+ assertThat(books2[0]).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get books by category ID with pagination - should return paged results")
+ void getBooksByCategoryId_WithPagination_ShouldReturnPagedResults() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+
+ // When
+ MvcResult result = mockMvc.perform(get("/categories/{id}/books", category.getId())
+ .param("page", "0")
+ .param("size", "5"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDtoWithoutCategoryIds[] books = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDtoWithoutCategoryIds[].class
+ );
+
+ assertNotNull(books);
+ assertEquals(1, books.length);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get book by ID - should return exact book")
+ void getBookById_WithUserRole_ShouldReturnExactBook() throws Exception {
+ // Given
+ BookDto expectedDto = bookRepository.findByIdWithCategories(1L)
+ .map(bookMapper::toDto)
+ .orElseThrow();
+
+ // When
+ MvcResult result = mockMvc.perform(get("/books/{id}", expectedDto.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ BookDto actualDto = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ BookDto.class
+ );
+
+ assertThat(actualDto).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get all books - should return exact books list")
+ void findAll_WithUserRole_ShouldReturnExactBooks() throws Exception {
+ // Given
+ BookDto expectedDto = bookRepository.findByIdWithCategories(1L)
+ .map(bookMapper::toDto)
+ .orElseThrow();
+
+ // When
+ MvcResult result = mockMvc.perform(get("/books")
+ .param("page", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ String responseContent = result.getResponse().getContentAsString();
+ JsonNode root = objectMapper.readTree(responseContent);
+ List books = objectMapper.readValue(
+ root.get("content").toString(),
+ new TypeReference>() {
+ }
+ );
+
+ assertThat(books).hasSize(1);
+
+ assertThat(books.get(0)).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+}
diff --git a/src/test/java/bookrepo/controller/CategoryControllerTest.java b/src/test/java/bookrepo/controller/CategoryControllerTest.java
new file mode 100644
index 0000000..bcb1273
--- /dev/null
+++ b/src/test/java/bookrepo/controller/CategoryControllerTest.java
@@ -0,0 +1,210 @@
+package bookrepo.controller;
+
+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.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 bookrepo.dto.category.CategoryDto;
+import bookrepo.mapper.CategoryMapper;
+import bookrepo.model.Category;
+import bookrepo.repository.category.CategoryRepository;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+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.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Sql(scripts = {
+ "classpath:database/delete-data-from-tables.sql",
+ "classpath:database/category/add-categories-to-category-table.sql"
+}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+@Sql(scripts = "classpath:database/delete-data-from-tables.sql",
+ executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+class CategoryControllerTest {
+
+ protected static MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private CategoryRepository categoryRepository;
+
+ @Autowired
+ private CategoryMapper categoryMapper;
+
+ @BeforeAll
+ static void beforeAll(@Autowired WebApplicationContext applicationContext) {
+ mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @AfterAll
+ static void afterAll(@Autowired CategoryRepository categoryRepository) {
+ categoryRepository.deleteAll();
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get all categories")
+ void getAll_WithUserRole_ShouldReturnCategories() throws Exception {
+ // Given
+ List categories = categoryRepository.findAll();
+ CategoryDto expectedDto = categoryMapper.toDto(categories.get(0));
+
+ // When
+ MvcResult result = mockMvc.perform(get("/categories"))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
+ List actual = objectMapper.readValue(
+ root.get("content").toString(),
+ objectMapper.getTypeFactory().constructCollectionType(List.class, CategoryDto.class)
+ );
+ assertThat(actual).isNotEmpty();
+ assertThat(actual.get(0)).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Get category by ID")
+ void getCategoryById_WithUserRole_ShouldReturnCategory() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+ CategoryDto expectedDto = categoryMapper.toDto(category);
+
+ // When
+ MvcResult result = mockMvc.perform(get("/categories/{id}", category.getId()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ CategoryDto actualDto = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ CategoryDto.class
+ );
+ assertNotNull(actualDto);
+ assertThat(actualDto).usingRecursiveComparison().isEqualTo(expectedDto);
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Create a new category")
+ void createCategory_ValidRequestDto_Success() throws Exception {
+ // Given
+ CategoryDto requestDto = new CategoryDto();
+ requestDto.setName("Science");
+ requestDto.setDescription("Science books");
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // When
+ MvcResult result = mockMvc.perform(
+ post("/categories")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ // Then
+ CategoryDto actual = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ CategoryDto.class
+ );
+ assertNotNull(actual);
+ assertNotNull(actual.getId());
+ assertEquals(requestDto.getName(), actual.getName());
+ assertEquals(requestDto.getDescription(), actual.getDescription());
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Update category")
+ void updateCategory_ValidRequestDto_Success() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+ CategoryDto requestDto = new CategoryDto();
+ requestDto.setName("Updated Fiction");
+ requestDto.setDescription("Updated description");
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ // When
+ MvcResult result = mockMvc.perform(
+ put("/categories/{id}", category.getId())
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isOk())
+ .andReturn();
+
+ // Then
+ CategoryDto actual = objectMapper.readValue(
+ result.getResponse().getContentAsString(),
+ CategoryDto.class
+ );
+ assertNotNull(actual);
+ assertEquals(requestDto.getName(), actual.getName());
+ assertEquals(requestDto.getDescription(), actual.getDescription());
+ }
+
+ @WithMockUser(username = "admin", authorities = {"ADMIN"})
+ @Test
+ @DisplayName("Delete category")
+ void deleteCategory_WithAdminRole_ShouldDelete() throws Exception {
+ // Given
+ Category category = categoryRepository.findAll().get(0);
+ long initialCount = categoryRepository.count();
+
+ // When
+ mockMvc.perform(delete("/categories/{id}", category.getId()))
+ .andExpect(status().isNoContent());
+
+ // Then
+ assertEquals(initialCount - 1, categoryRepository.count());
+ }
+
+ @WithMockUser(username = "user", authorities = {"USER"})
+ @Test
+ @DisplayName("Create category with USER role should return forbidden")
+ void createCategory_WithUserRole_ShouldReturnForbidden() throws Exception {
+ CategoryDto requestDto = new CategoryDto();
+ requestDto.setName("Test");
+ requestDto.setDescription("Test category");
+ String jsonRequest = objectMapper.writeValueAsString(requestDto);
+
+ mockMvc.perform(
+ post("/categories")
+ .content(jsonRequest)
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @DisplayName("Access without authentication should return unauthorized")
+ void accessWithoutAuthentication_ShouldReturnUnauthorized() throws Exception {
+ mockMvc.perform(get("/categories"))
+ .andExpect(status().isUnauthorized());
+ }
+}
diff --git a/src/test/java/bookrepo/repository/BookRepositoryTest.java b/src/test/java/bookrepo/repository/BookRepositoryTest.java
new file mode 100644
index 0000000..3e5bf62
--- /dev/null
+++ b/src/test/java/bookrepo/repository/BookRepositoryTest.java
@@ -0,0 +1,52 @@
+package bookrepo.repository;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import bookrepo.model.Book;
+import bookrepo.model.Category;
+import bookrepo.repository.book.BookRepository;
+import bookrepo.repository.category.CategoryRepository;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("test")
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+public class BookRepositoryTest {
+
+ @Autowired
+ private BookRepository bookRepository;
+
+ @Autowired
+ private CategoryRepository categoryRepository;
+
+ @Test
+ @DisplayName("Find all books by category ID")
+ void findAllByCategories_Id_finds1BookWithId1_ShouldReturnListWith1Book() {
+ Category category = new Category();
+ category.setName("Programming");
+ Category savedCategory = categoryRepository.save(category);
+
+ Book book = new Book();
+ book.setTitle("Book Title 1");
+ book.setAuthor("Author 1");
+ book.setDescription("Description 1");
+ book.setIsbn("ISBN001");
+ book.setPrice(BigDecimal.valueOf(29.99));
+ book.setCategories(Set.of(savedCategory));
+
+ Book savedBook = bookRepository.save(book);
+
+ List actual = bookRepository.findAllByCategories_Id(savedCategory.getId());
+
+ assertEquals(1, actual.size());
+ assertEquals(savedBook.getId(), actual.get(0).getId());
+ }
+}
diff --git a/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java
new file mode 100644
index 0000000..a067ae8
--- /dev/null
+++ b/src/test/java/bookrepo/repository/BookSpecificationBuilderTest.java
@@ -0,0 +1,116 @@
+package bookrepo.repository;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import bookrepo.dto.book.BookSearchParameters;
+import bookrepo.model.Book;
+import bookrepo.repository.book.BookSpecificationBuilder;
+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.jpa.domain.Specification;
+
+@ExtendWith(MockitoExtension.class)
+class BookSpecificationBuilderTest {
+
+ @Mock
+ private SpecificationProviderManager bookSpecificationProviderManager;
+
+ @Mock
+ private SpecificationProvider specificationProvider;
+
+ @InjectMocks
+ private BookSpecificationBuilder bookSpecificationBuilder;
+
+ @Test
+ void build_WithAuthorParameters_ShouldAddAuthorSpecification() {
+ // Given
+ BookSearchParameters searchParameters = new BookSearchParameters(
+ new String[]{"Author1", "Author2"}, null, null, null, null, null
+ );
+
+ when(bookSpecificationProviderManager.getSpecificationProvider("author"))
+ .thenReturn(specificationProvider);
+ when(specificationProvider.getSpecification(any()))
+ .thenReturn((root, query, cb) -> cb.conjunction());
+
+ // When
+ Specification result = bookSpecificationBuilder.build(searchParameters);
+
+ // Then
+ assertNotNull(result);
+ verify(bookSpecificationProviderManager).getSpecificationProvider("author");
+ verify(specificationProvider).getSpecification(new String[]{"Author1", "Author2"});
+ }
+
+ @Test
+ void build_WithTitleParameters_ShouldAddTitleSpecification() {
+ // Given
+ BookSearchParameters searchParameters = new BookSearchParameters(
+ null, new String[]{"Title1", "Title2"}, null, null, null, null
+ );
+
+ when(bookSpecificationProviderManager.getSpecificationProvider("title"))
+ .thenReturn(specificationProvider);
+ when(specificationProvider.getSpecification(any()))
+ .thenReturn((root, query, cb) -> cb.conjunction());
+
+ // When
+ Specification result = bookSpecificationBuilder.build(searchParameters);
+
+ // Then
+ assertNotNull(result);
+ verify(bookSpecificationProviderManager).getSpecificationProvider("title");
+ verify(specificationProvider).getSpecification(new String[]{"Title1", "Title2"});
+ }
+
+ @Test
+ void build_WithMultipleParameters_ShouldCombineSpecifications() {
+ // Given
+ BookSearchParameters searchParameters = new BookSearchParameters(
+ new String[]{"Author1"},
+ new String[]{"Title1"},
+ new String[]{"ISBN123"},
+ null, null, null
+ );
+
+ when(bookSpecificationProviderManager.getSpecificationProvider("author"))
+ .thenReturn(specificationProvider);
+ when(bookSpecificationProviderManager.getSpecificationProvider("title"))
+ .thenReturn(specificationProvider);
+ when(bookSpecificationProviderManager.getSpecificationProvider("isbn"))
+ .thenReturn(specificationProvider);
+ when(specificationProvider.getSpecification(any()))
+ .thenReturn((root, query, cb) -> cb.conjunction());
+
+ // When
+ Specification result = bookSpecificationBuilder.build(searchParameters);
+
+ // Then
+ assertNotNull(result);
+ verify(bookSpecificationProviderManager, times(3)).getSpecificationProvider(anyString());
+ }
+
+ @Test
+ void build_WithNullParameters_ShouldReturnBaseSpecification() {
+ // Given
+ BookSearchParameters searchParameters = new BookSearchParameters(
+ null, null, null, null, null, null
+ );
+
+ // When
+ Specification result = bookSpecificationBuilder.build(searchParameters);
+
+ // Then
+ assertNotNull(result);
+ verifyNoInteractions(bookSpecificationProviderManager);
+ }
+}
diff --git a/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java
new file mode 100644
index 0000000..71c17fd
--- /dev/null
+++ b/src/test/java/bookrepo/repository/BookSpecificationProviderManagerTest.java
@@ -0,0 +1,92 @@
+package bookrepo.repository;
+
+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.verify;
+import static org.mockito.Mockito.when;
+
+import bookrepo.model.Book;
+import bookrepo.repository.book.BookSpecificationProviderManager;
+import java.util.List;
+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;
+
+@ExtendWith(MockitoExtension.class)
+class BookSpecificationProviderManagerTest {
+
+ @Mock
+ private SpecificationProvider authorProvider;
+
+ @Mock
+ private SpecificationProvider titleProvider;
+
+ @InjectMocks
+ private BookSpecificationProviderManager bookSpecificationProviderManager;
+
+ @Test
+ void getSpecificationProvider_WithExistingKey_ShouldReturnProvider() {
+ // Given
+ when(authorProvider.getKey()).thenReturn("author");
+ BookSpecificationProviderManager manager =
+ new BookSpecificationProviderManager(List.of(authorProvider));
+
+ // When
+ SpecificationProvider result = manager.getSpecificationProvider("author");
+
+ // Then
+ assertNotNull(result);
+ assertEquals(authorProvider, result);
+ verify(authorProvider).getKey();
+ }
+
+ @Test
+ void getSpecificationProvider_WithNonExistingKey_ShouldThrowException() {
+ // Given
+ when(authorProvider.getKey()).thenReturn("author");
+ BookSpecificationProviderManager manager =
+ new BookSpecificationProviderManager(List.of(authorProvider));
+
+ // When & Then
+ RuntimeException exception = assertThrows(RuntimeException.class,
+ () -> manager.getSpecificationProvider("nonexistent"));
+
+ assertEquals("No specification provider found for key: nonexistent",
+ exception.getMessage());
+ }
+
+ @Test
+ void getSpecificationProvider_WithMultipleProviders_ShouldReturnCorrectOne() {
+ // Given
+ when(authorProvider.getKey()).thenReturn("author");
+ when(titleProvider.getKey()).thenReturn("title");
+
+ BookSpecificationProviderManager manager =
+ new BookSpecificationProviderManager(List.of(authorProvider, titleProvider));
+
+ // When
+ SpecificationProvider result = manager.getSpecificationProvider("title");
+
+ // Then
+ assertNotNull(result);
+ assertEquals(titleProvider, result);
+ verify(authorProvider).getKey();
+ verify(titleProvider).getKey();
+ }
+
+ @Test
+ void getSpecificationProvider_WithEmptyProviders_ShouldThrowException() {
+ // Given
+ BookSpecificationProviderManager manager =
+ new BookSpecificationProviderManager(List.of());
+
+ // When & Then
+ RuntimeException exception = assertThrows(RuntimeException.class,
+ () -> manager.getSpecificationProvider("author"));
+
+ assertEquals("No specification provider found for key: author", exception.getMessage());
+ }
+}
diff --git a/src/test/java/bookrepo/service/BookServiceTest.java b/src/test/java/bookrepo/service/BookServiceTest.java
new file mode 100644
index 0000000..0174679
--- /dev/null
+++ b/src/test/java/bookrepo/service/BookServiceTest.java
@@ -0,0 +1,255 @@
+package bookrepo.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import bookrepo.dto.book.BookDto;
+import bookrepo.dto.book.BookDtoWithoutCategoryIds;
+import bookrepo.dto.book.BookSearchParameters;
+import bookrepo.dto.book.CreateBookRequestDto;
+import bookrepo.mapper.BookMapper;
+import bookrepo.model.Book;
+import bookrepo.model.Category;
+import bookrepo.repository.book.BookRepository;
+import bookrepo.repository.book.BookSpecificationBuilder;
+import bookrepo.repository.category.CategoryRepository;
+import bookrepo.service.impl.BookServiceImpl;
+import bookrepo.util.TestUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+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.Mockito;
+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)
+public class BookServiceTest {
+
+ @Mock
+ private BookRepository bookRepository;
+
+ @Mock
+ private CategoryRepository categoryRepository;
+
+ @Mock
+ private BookMapper bookMapper;
+
+ @Mock
+ private BookSpecificationBuilder bookSpecificationBuilder;
+
+ @InjectMocks
+ private BookServiceImpl bookService;
+
+ @Test
+ @DisplayName("""
+ Should save a book when save is called with valid request DTO
+ and return the corresponding BookDto with correct category IDs
+ """)
+ void save_withValidRequest_returnsBookDtoWithCategoryIds() {
+ // Given
+ CreateBookRequestDto requestDto = TestUtil.createValidBookRequestDto();
+ Book book = TestUtil.createBookFromRequest(requestDto);
+
+ Category categoryProgramming = TestUtil.createProgrammingCategory();
+ Category categoryJava = TestUtil.createJavaCategory();
+ Set categories = Set.of(categoryProgramming, categoryJava);
+ book.setCategories(categories);
+
+ BookDto expectedDto = TestUtil.createExpectedBookDto(book);
+
+ when(bookMapper.toModel(requestDto)).thenReturn(book);
+ when(categoryRepository.findAllById(requestDto.getCategoryIds()))
+ .thenReturn(new ArrayList<>(categories));
+ when(bookRepository.save(book)).thenReturn(book);
+ when(bookMapper.toDto(book)).thenReturn(expectedDto);
+
+ // When
+ BookDto actualDto = bookService.save(requestDto);
+
+ // Then
+ assertEquals(expectedDto, actualDto);
+ verify(bookMapper).toModel(requestDto);
+ verify(categoryRepository).findAllById(requestDto.getCategoryIds());
+ verify(bookRepository).save(book);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("""
+ Should show all books when findAll is called with valid pageable
+ and return the corresponding Page of BookDto
+ """)
+ void findAll_withValidPageable_returnsPageOfBookDto() {
+ // Given
+ Book book1 = TestUtil.createBookEffectiveJava();
+ Book book2 = TestUtil.createBookCleanCode();
+
+ Pageable pageable = PageRequest.of(0, 10);
+ List books = List.of(book1, book2);
+ Page bookPage = new PageImpl<>(books, pageable, books.size());
+
+ BookDto dto1 = TestUtil.createBookDtoEffectiveJava(book1);
+ BookDto dto2 = TestUtil.createBookDtoCleanCode(book2);
+
+ when(bookRepository.findAll(pageable)).thenReturn(bookPage);
+ when(bookMapper.toDto(book1)).thenReturn(dto1);
+ when(bookMapper.toDto(book2)).thenReturn(dto2);
+
+ // When
+ Page actualPage = bookService.findAll(pageable);
+
+ // Then
+ assertEquals(2, actualPage.getTotalElements());
+ assertEquals(1, actualPage.getTotalPages());
+ assertEquals(2, actualPage.getContent().size());
+ assertEquals("Effective Java", actualPage.getContent().get(0).getTitle());
+ assertEquals("Clean Code", actualPage.getContent().get(1).getTitle());
+
+ verify(bookRepository).findAll(pageable);
+ verify(bookMapper).toDto(book1);
+ verify(bookMapper).toDto(book2);
+ }
+
+ @Test
+ @DisplayName("""
+ Should return BookDto when getById is called with existing ID
+ """)
+ void getById_withValidId_returnsBookDto() {
+ // Given
+ Long id = 1L;
+ Book book = TestUtil.createBookEffectiveJava();
+ BookDto expectedDto = TestUtil.createBookDtoEffectiveJava(book);
+
+ when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(book));
+ when(bookMapper.toDto(book)).thenReturn(expectedDto);
+
+ // When
+ BookDto actualDto = bookService.getById(id);
+
+ // Then
+ assertEquals(expectedDto, actualDto);
+ verify(bookRepository).findById(id);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("""
+ Should update book when update is called with valid ID and request DTO
+ and return updated BookDto
+ """)
+ void update_withValidIdAndRequest_returnsUpdatedBookDto() {
+ // Given
+ CreateBookRequestDto requestDto = new CreateBookRequestDto();
+ requestDto.setTitle("Updated Title");
+ requestDto.setAuthor("Updated Author");
+ requestDto.setCategoryIds(Set.of(1L));
+
+ Long id = 1L;
+ Book existingBook = TestUtil.createBookEffectiveJava();
+ existingBook.setTitle("Old Title");
+
+ Category category = TestUtil.createProgrammingCategory();
+ Set categories = Set.of(category);
+ existingBook.setCategories(categories);
+
+ BookDto expectedDto = new BookDto();
+ expectedDto.setId(id);
+ expectedDto.setTitle(requestDto.getTitle());
+ expectedDto.setAuthor(requestDto.getAuthor());
+ expectedDto.setCategoryIds(Set.of(1L));
+
+ when(bookRepository.findById(id)).thenReturn(java.util.Optional.of(existingBook));
+ when(categoryRepository.findAllById(requestDto.getCategoryIds()))
+ .thenReturn(new ArrayList<>(categories));
+ when(bookRepository.save(existingBook)).thenReturn(existingBook);
+ when(bookMapper.toDto(existingBook)).thenReturn(expectedDto);
+
+ // When
+ BookDto actualDto = bookService.update(id, requestDto);
+
+ // Then
+ assertEquals(expectedDto, actualDto);
+ verify(bookRepository).findById(id);
+ verify(bookMapper).updateModelFromDto(requestDto, existingBook);
+ verify(categoryRepository).findAllById(requestDto.getCategoryIds());
+ verify(bookRepository).save(existingBook);
+ verify(bookMapper).toDto(existingBook);
+ }
+
+ @Test
+ @DisplayName("""
+ Should return list of BookDto when search is called with valid parameters
+ """)
+ void search_withValidParams_returnsListOfBookDto() {
+ // Given
+ Book book = TestUtil.createBookEffectiveJava();
+ BookDto dto = TestUtil.createBookDtoEffectiveJava(book);
+ List books = List.of(book);
+
+ @SuppressWarnings("unchecked")
+ Specification spec = Mockito.mock(Specification.class);
+
+ BookSearchParameters params = new BookSearchParameters(
+ new String[]{"Joshua Bloch"},
+ new String[]{"Effective Java"},
+ new String[]{"Addison-Wesley"},
+ new String[]{"Programming"},
+ new String[]{"cover.jpg"},
+ new String[]{"45.00"}
+ );
+
+ when(bookSpecificationBuilder.build(params)).thenReturn(spec);
+ when(bookRepository.findAll(spec)).thenReturn(books);
+ when(bookMapper.toDto(book)).thenReturn(dto);
+
+ // When
+ List result = bookService.search(params);
+
+ // Then
+ assertEquals(1, result.size());
+ assertEquals("Effective Java", result.get(0).getTitle());
+ verify(bookSpecificationBuilder).build(params);
+ verify(bookRepository).findAll(spec);
+ verify(bookMapper).toDto(book);
+ }
+
+ @Test
+ @DisplayName("""
+ Should return list of BookDtoWithoutCategoryIds\s
+ when findAllByCategoryId is called with valid ID
+ \s""")
+ void findAllByCategoryId_withValidId_returnsListOfBookDtoWithoutCategoryIds() {
+ // Given
+ Long categoryId = 1L;
+ Category category = TestUtil.createProgrammingCategory();
+ Book book = TestUtil.createBookEffectiveJava();
+ BookDtoWithoutCategoryIds dto = TestUtil.createBookDtoWithoutCategories(book);
+
+ when(categoryRepository.findById(categoryId))
+ .thenReturn(java.util.Optional.of(category));
+ when(bookRepository.findAllByCategories_Id(categoryId))
+ .thenReturn(List.of(book));
+ when(bookMapper.toDtoWithoutCategories(book))
+ .thenReturn(dto);
+
+ // When
+ List result = bookService.findAllByCategoryId(categoryId);
+
+ // Then
+ assertEquals(1, result.size());
+ assertEquals("Effective Java", result.get(0).getTitle());
+ verify(categoryRepository).findById(categoryId);
+ verify(bookRepository).findAllByCategories_Id(categoryId);
+ verify(bookMapper).toDtoWithoutCategories(book);
+ }
+}
diff --git a/src/test/java/bookrepo/service/CategoryServiceTest.java b/src/test/java/bookrepo/service/CategoryServiceTest.java
new file mode 100644
index 0000000..d90a0d8
--- /dev/null
+++ b/src/test/java/bookrepo/service/CategoryServiceTest.java
@@ -0,0 +1,178 @@
+package bookrepo.service;
+
+import static bookrepo.util.TestUtil.createCategoryDtoWithoutId;
+import static bookrepo.util.TestUtil.createJavaCategory;
+import static bookrepo.util.TestUtil.createJavaCategoryDto;
+import static bookrepo.util.TestUtil.createProgrammingCategory;
+import static bookrepo.util.TestUtil.createProgrammingCategoryDto;
+import static bookrepo.util.TestUtil.createUpdatedCategoryDto;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import bookrepo.dto.category.CategoryDto;
+import bookrepo.exception.EntityNotFoundException;
+import bookrepo.mapper.CategoryMapper;
+import bookrepo.model.Category;
+import bookrepo.repository.category.CategoryRepository;
+import bookrepo.service.impl.CategoryServiceImpl;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.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)
+public class CategoryServiceTest {
+
+ private static final Long EXISTING_CATEGORY_ID = 1L;
+ private static final Long ANOTHER_CATEGORY_ID = 2L;
+ private static final Long NON_EXISTING_CATEGORY_ID = 999L;
+
+ @Mock
+ private CategoryRepository categoryRepository;
+
+ @Mock
+ private CategoryMapper categoryMapper;
+
+ @InjectMocks
+ private CategoryServiceImpl categoryService;
+
+ @Test
+ @DisplayName("Should return page of CategoryDto when findAll is called with valid pageable")
+ void findAll_withValidPageable_returnsPageOfCategoryDto() {
+ // Given
+ Category category1 = createProgrammingCategory();
+ Category category2 = createJavaCategory();
+
+ CategoryDto dto1 = createProgrammingCategoryDto();
+ CategoryDto dto2 = createJavaCategoryDto();
+
+ Pageable pageable = PageRequest.of(0, 10);
+ List categories = List.of(category1, category2);
+ Page categoryPage = new PageImpl<>(categories, pageable, categories.size());
+
+ when(categoryRepository.findAll(pageable)).thenReturn(categoryPage);
+ when(categoryMapper.toDto(category1)).thenReturn(dto1);
+ when(categoryMapper.toDto(category2)).thenReturn(dto2);
+
+ // When
+ Page result = categoryService.findAll(pageable);
+
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals("Programming", result.getContent().get(0).getName());
+ assertEquals("Java", result.getContent().get(1).getName());
+
+ verify(categoryRepository).findAll(pageable);
+ verify(categoryMapper).toDto(category1);
+ verify(categoryMapper).toDto(category2);
+ }
+
+ @Test
+ @DisplayName("Should return CategoryDto when getById is called with existing ID")
+ void getById_withValidId_returnsCategoryDto() {
+ // Given
+ Category category = createProgrammingCategory();
+ CategoryDto dto = createProgrammingCategoryDto();
+
+ when(categoryRepository.findById(EXISTING_CATEGORY_ID)).thenReturn(Optional.of(category));
+ when(categoryMapper.toDto(category)).thenReturn(dto);
+
+ // When
+ CategoryDto result = categoryService.getById(EXISTING_CATEGORY_ID);
+
+ // Then
+ assertEquals("Programming", result.getName());
+
+ verify(categoryRepository).findById(EXISTING_CATEGORY_ID);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("Should throw EntityNotFoundException when getById is called with non-existing ID")
+ void getById_withInvalidId_throwsException() {
+ // Given
+ when(categoryRepository.findById(NON_EXISTING_CATEGORY_ID)).thenReturn(Optional.empty());
+
+ // When & Then
+ assertThrows(EntityNotFoundException.class, () ->
+ categoryService.getById(NON_EXISTING_CATEGORY_ID));
+
+ verify(categoryRepository).findById(NON_EXISTING_CATEGORY_ID);
+ }
+
+ @Test
+ @DisplayName("Should save category and return CategoryDto when save is called with valid DTO")
+ void save_withValidDto_returnsSavedCategoryDto() {
+ // Given
+ CategoryDto dto = createCategoryDtoWithoutId();
+ Category category = createProgrammingCategory();
+
+ when(categoryMapper.toEntity(dto)).thenReturn(category);
+ when(categoryRepository.save(category)).thenReturn(category);
+ when(categoryMapper.toDto(category)).thenReturn(dto);
+
+ // When
+ CategoryDto result = categoryService.save(dto);
+
+ // Then
+ assertEquals("Programming", result.getName());
+
+ verify(categoryMapper).toEntity(dto);
+ verify(categoryRepository).save(category);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("""
+ Should update category and return updated\s
+ CategoryDto when update is called with valid ID and DTO
+ \s""")
+ void update_withValidIdAndDto_returnsUpdatedCategoryDto() {
+ // Given
+ CategoryDto dto = createUpdatedCategoryDto();
+ Category category = createProgrammingCategory();
+
+ when(categoryRepository.findById(EXISTING_CATEGORY_ID)).thenReturn(Optional.of(category));
+ when(categoryRepository.save(category)).thenReturn(category);
+ when(categoryMapper.toDto(category)).thenReturn(dto);
+
+ // When
+ CategoryDto result = categoryService.update(EXISTING_CATEGORY_ID, dto);
+
+ // Then
+ assertEquals("Updated", result.getName());
+
+ verify(categoryRepository).findById(EXISTING_CATEGORY_ID);
+ verify(categoryMapper).updateEntityFromDto(dto, category);
+ verify(categoryRepository).save(category);
+ verify(categoryMapper).toDto(category);
+ }
+
+ @Test
+ @DisplayName("""
+ Should throw EntityNotFoundException\s
+ when update is called with non-existing ID
+ \s""")
+ void update_withInvalidId_throwsException() {
+ // Given
+ CategoryDto dto = createUpdatedCategoryDto();
+
+ when(categoryRepository.findById(NON_EXISTING_CATEGORY_ID)).thenReturn(Optional.empty());
+
+ // When & Then
+ assertThrows(EntityNotFoundException.class, () ->
+ categoryService.update(NON_EXISTING_CATEGORY_ID, dto));
+
+ verify(categoryRepository).findById(NON_EXISTING_CATEGORY_ID);
+ }
+}
diff --git a/src/test/java/bookrepo/util/TestUtil.java b/src/test/java/bookrepo/util/TestUtil.java
new file mode 100644
index 0000000..4d5cf58
--- /dev/null
+++ b/src/test/java/bookrepo/util/TestUtil.java
@@ -0,0 +1,148 @@
+package bookrepo.util;
+
+import bookrepo.dto.book.BookDto;
+import bookrepo.dto.book.BookDtoWithoutCategoryIds;
+import bookrepo.dto.book.CreateBookRequestDto;
+import bookrepo.dto.category.CategoryDto;
+import bookrepo.model.Book;
+import bookrepo.model.Category;
+import java.math.BigDecimal;
+import java.util.Set;
+
+public class TestUtil {
+
+ public static CreateBookRequestDto createValidBookRequestDto() {
+ CreateBookRequestDto requestDto = new CreateBookRequestDto();
+ requestDto.setTitle("Effective Java");
+ requestDto.setAuthor("Joshua Bloch");
+ requestDto.setDescription("A comprehensive guide to best practices in Java programming.");
+ requestDto.setPrice(BigDecimal.valueOf(45.00));
+ requestDto.setIsbn("978-0134686097");
+ requestDto.setCategoryIds(Set.of(1L, 2L));
+ return requestDto;
+ }
+
+ public static Book createBookFromRequest(CreateBookRequestDto requestDto) {
+ Book book = new Book();
+ book.setTitle(requestDto.getTitle());
+ book.setAuthor(requestDto.getAuthor());
+ book.setDescription(requestDto.getDescription());
+ book.setPrice(requestDto.getPrice());
+ book.setIsbn(requestDto.getIsbn());
+ return book;
+ }
+
+ public static Category createProgrammingCategory() {
+ Category category = new Category();
+ category.setId(1L);
+ category.setName("Programming");
+ return category;
+ }
+
+ public static Category createJavaCategory() {
+ Category category = new Category();
+ category.setId(2L);
+ category.setName("Java");
+ return category;
+ }
+
+ public static BookDto createExpectedBookDto(Book book) {
+ BookDto expectedDto = new BookDto();
+ expectedDto.setTitle(book.getTitle());
+ expectedDto.setAuthor(book.getAuthor());
+ expectedDto.setDescription(book.getDescription());
+ expectedDto.setPrice(book.getPrice());
+ expectedDto.setIsbn(book.getIsbn());
+ expectedDto.setCategoryIds(Set.of(1L, 2L));
+ return expectedDto;
+ }
+
+ public static Book createBookEffectiveJava() {
+ Book book = new Book();
+ book.setId(1L);
+ book.setTitle("Effective Java");
+ book.setAuthor("Joshua Bloch");
+ book.setDescription("A comprehensive guide to best practices in Java programming.");
+ book.setPrice(BigDecimal.valueOf(45.00));
+ book.setIsbn("978-0134686097");
+ return book;
+ }
+
+ public static Book createBookCleanCode() {
+ Book book = new Book();
+ book.setId(2L);
+ book.setTitle("Clean Code");
+ book.setAuthor("Robert C. Martin");
+ book.setDescription("A Handbook of Agile Software Craftsmanship.");
+ book.setPrice(BigDecimal.valueOf(40.00));
+ book.setIsbn("978-0132350884");
+ return book;
+ }
+
+ public static BookDto createBookDtoEffectiveJava(Book book) {
+ BookDto dto = new BookDto();
+ dto.setId(book.getId());
+ dto.setTitle(book.getTitle());
+ dto.setAuthor(book.getAuthor());
+ dto.setDescription(book.getDescription());
+ dto.setPrice(book.getPrice());
+ dto.setIsbn(book.getIsbn());
+ return dto;
+ }
+
+ public static BookDto createBookDtoCleanCode(Book book) {
+ BookDto dto = new BookDto();
+ dto.setId(book.getId());
+ dto.setTitle(book.getTitle());
+ dto.setAuthor(book.getAuthor());
+ dto.setDescription(book.getDescription());
+ dto.setPrice(book.getPrice());
+ dto.setIsbn(book.getIsbn());
+ return dto;
+ }
+
+ public static BookDtoWithoutCategoryIds createBookDtoWithoutCategories(Book book) {
+ BookDtoWithoutCategoryIds dto = new BookDtoWithoutCategoryIds();
+ dto.setId(book.getId());
+ dto.setTitle(book.getTitle());
+ return dto;
+ }
+
+ public static CreateBookRequestDto createBookRequestDto() {
+ CreateBookRequestDto dto = new CreateBookRequestDto();
+ dto.setTitle("Test Book");
+ dto.setAuthor("Test Author");
+ dto.setIsbn("ISBN123");
+ dto.setPrice(BigDecimal.valueOf(29.99));
+ dto.setDescription("Test Description");
+ dto.setCoverImage("cover.jpg");
+ dto.setCategoryIds(Set.of(1L));
+ return dto;
+ }
+
+ public static CategoryDto createProgrammingCategoryDto() {
+ CategoryDto dto = new CategoryDto();
+ dto.setId(1L);
+ dto.setName("Programming");
+ return dto;
+ }
+
+ public static CategoryDto createJavaCategoryDto() {
+ CategoryDto dto = new CategoryDto();
+ dto.setId(2L);
+ dto.setName("Java");
+ return dto;
+ }
+
+ public static CategoryDto createCategoryDtoWithoutId() {
+ CategoryDto dto = new CategoryDto();
+ dto.setName("Programming");
+ return dto;
+ }
+
+ public static CategoryDto createUpdatedCategoryDto() {
+ CategoryDto dto = new CategoryDto();
+ dto.setName("Updated");
+ return dto;
+ }
+}
diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties
deleted file mode 100644
index f33f2ae..0000000
--- a/src/test/resources/application-test.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-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.jpa.hibernate.ddl-auto=create-drop
-
-jwt.expiration=300000
-jwt.secret=MyJwtSecretKeyj98ty4j98hgj95j98hgj98hj
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..40bef53
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:tc:mysql:8.0.37:///mydb
+spring.datasource.username=sa
+spring.datasource.password=password
+jwt.expiration=300000
+jwt.secret=MyJwtSecretKeyj98ty4j98hgj95j98hgj98hj
diff --git a/src/test/resources/database/add-categories-to-books.sql b/src/test/resources/database/add-categories-to-books.sql
new file mode 100644
index 0000000..fba3226
--- /dev/null
+++ b/src/test/resources/database/add-categories-to-books.sql
@@ -0,0 +1,3 @@
+insert into books_categories (book_id, category_id) values
+(1, 1),
+(1, 2);
\ No newline at end of file
diff --git a/src/test/resources/database/book/add-books-to-table.sql b/src/test/resources/database/book/add-books-to-table.sql
new file mode 100644
index 0000000..8953df2
--- /dev/null
+++ b/src/test/resources/database/book/add-books-to-table.sql
@@ -0,0 +1,2 @@
+insert into books (id, title, author, isbn, price, description) values
+(1, 'Effective Java', 'Joshua Bloch', '9780134685991', 49.99, 'A must-have book for every Java developer');
\ No newline at end of file
diff --git a/src/test/resources/database/category/add-categories-to-category-table.sql b/src/test/resources/database/category/add-categories-to-category-table.sql
new file mode 100644
index 0000000..408641c
--- /dev/null
+++ b/src/test/resources/database/category/add-categories-to-category-table.sql
@@ -0,0 +1,3 @@
+insert into categories (id, name, description) values
+(1, 'Programming', 'Books about programming and software development'),
+(2, 'Java', 'Books specifically about Java programming language');
\ No newline at end of file
diff --git a/src/test/resources/database/delete-data-from-tables.sql b/src/test/resources/database/delete-data-from-tables.sql
new file mode 100644
index 0000000..2ff098f
--- /dev/null
+++ b/src/test/resources/database/delete-data-from-tables.sql
@@ -0,0 +1,3 @@
+delete from books_categories;
+delete from books;
+delete from categories;
\ No newline at end of file