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