diff --git a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java index 3d4fa45..a1506e0 100644 --- a/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java @@ -11,6 +11,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -20,7 +23,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -120,24 +123,40 @@ void setUp() { // -------------------------------------- // Tests: createBook // -------------------------------------- + @Test void testCreateBook_Success() { + // Arrange: also checks special chars + BookRequest specialRequest = + new BookRequest("Java & Friends!", "Synopsis", "Author"); - // Arrange: tell the mock repository what to do when called - when(testBookRepository.save(any(Book.class))).thenReturn(persistedBook); + // The "Future" Book (What the DB returns) + Book bookFromDb = + Book.builder() + .id(testId) + .title(specialRequest.title()) + .synopsis(specialRequest.synopsis()) + .author(specialRequest.author()) + .build(); - // Act: call the service method we are testing - Book result = testBookService.createBook(validBookRequest); + when(testBookRepository.save(any(Book.class))).thenReturn(bookFromDb); - // Assert: Check the outcome - assertNotNull(result); - assertEquals(testId, result.getId()); - assertEquals(validBookRequest.title(), result.getTitle()); - assertEquals(validBookRequest.synopsis(), result.getSynopsis()); - assertEquals(validBookRequest.author(), result.getAuthor()); + // Act + Book result = testBookService.createBook(specialRequest); + + // Assert: Verify the Service fulfills its return contract + assertEquals(bookFromDb, result, "Service must return the object returned by the repository"); + + // Assert: Capture the "Past" Book (What went INTO the DB) + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository, times(1)).save(bookCaptor.capture()); + Book bookSentToDb = bookCaptor.getValue(); - // Did the service perform the correct action on its dependency? - verify(testBookRepository, times(1)).save(any(Book.class)); + // Assert + assertNull(bookSentToDb.getId(), "ID should be null before DB generates it"); + assertEquals(specialRequest.title(), bookSentToDb.getTitle()); + assertEquals(specialRequest.synopsis(), bookSentToDb.getSynopsis()); + assertEquals(specialRequest.author(), bookSentToDb.getAuthor()); } @Test @@ -151,51 +170,6 @@ void testCreateBook_NullRequest_ThrowsException() { ); } - @Test - void testCreateBook_NullTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest(null, "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - - // Verify repository was never called - verify(testBookRepository, never()).save(any()); - } - - @Test - void testCreateBook_EmptyTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest("", "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - } - - @Test - void testCreateBook_BlankTitle_ThrowsException() { - // Arrange - BookRequest invalidRequest = new BookRequest(" ", "Synopsis", "Author"); - - // Act & Assert - assertThrows( - IllegalArgumentException.class, - () -> { - testBookService.createBook(invalidRequest); - } - ); - } - @Test void testCreateBook_RepositoryFailure_ThrowsException() { // Arrange @@ -211,6 +185,19 @@ void testCreateBook_RepositoryFailure_ThrowsException() { ); } + @ParameterizedTest + @NullAndEmptySource // Covers Null and "" (Empty) + @ValueSource(strings = {" ", "\t", "\n"}) // Covers Blank (Spaces/Tabs) + void testCreateBook_InvalidTitle_ThrowsException(String invalidTitle) { + // Arrange + BookRequest invalidRequest = new BookRequest(invalidTitle, "Synopsis", "Author"); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> { + testBookService.createBook(invalidRequest); + }); + } + // ----- EDGE cases --------- @ParameterizedTest(name = "{0}") // Display the test name @@ -220,51 +207,24 @@ void testCreateBook_VeryLongFields_Success( BookRequest request, Book expectedBook ) { - // Arrange when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); // Act Book result = testBookService.createBook(request); - // Assert - assertNotNull(result); - assertEquals(expectedBook.getId(), result.getId()); - assertEquals(expectedBook.getTitle(), result.getTitle()); - assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); - assertEquals(expectedBook.getAuthor(), result.getAuthor()); - - verify(testBookRepository, times(1)).save(any(Book.class)); - } - - @Test - void testCreateBook_SpecialCharactersInTitle_Success() { - // Arrange - BookRequest specialRequest = - new BookRequest("Test: A Book! @#$%^&*()", "Synopsis", "Author"); - - Book expectedBook = - Book.builder() - .id(testId) - .title(specialRequest.title()) - .synopsis(specialRequest.synopsis()) - .author(specialRequest.author()) - .build(); + // Assert 1: Verify the result flow + assertEquals(expectedBook, result); - when(testBookRepository.save(any(Book.class))).thenReturn(expectedBook); + // --- CAPTOR LOGIC --- + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository).save(bookCaptor.capture()); + Book bookSentToDb = bookCaptor.getValue(); - // Act - Book result = testBookService.createBook(specialRequest); - - // Assert - assertNotNull(result); - assertEquals(testId, result.getId()); - assertEquals(specialRequest.title(), result.getTitle()); - assertEquals(specialRequest.synopsis(), result.getSynopsis()); - assertEquals(specialRequest.author(), result.getAuthor()); - - // Did the service perform the correct action on its dependency? - verify(testBookRepository, times(1)).save(any(Book.class)); + // Assert 2: Verify the Service correctly mapped the LONG strings + // This ensures the service didn't truncate/alter the data + assertEquals(request.title(), bookSentToDb.getTitle()); + assertEquals(request.synopsis(), bookSentToDb.getSynopsis()); } // -------------------------------------------------------------------------------------------- @@ -274,13 +234,11 @@ void testCreateBook_SpecialCharactersInTitle_Success() { @Test void testDelete_Book_ShouldThrowException_WhenIdNotFound() { - // Arrange: As goal is to test what happens when the resource doesn't exist, - // we intentionally simulate DB returning NO result + // Arrange: simulate missing record when(testBookRepository.findById(testId)).thenReturn(Optional.empty()); - // ACT and ASSERT: throw ResourceNotFoundException when calling the delete method. + // ACT and ASSERT: correct exception thrown assertThrows( - // custom exception to reflect business rules vs technical problem ResourceNotFoundException.class, () -> testBookService.deleteBookById(testId) ); @@ -293,24 +251,30 @@ void testDelete_Book_ShouldThrowException_WhenIdNotFound() { @Test void testDeleteBookById_Success() { - // Arrange: - persistedBook.setDeleted(false); // ensure starting state + // Arrange: ensure the test book is active + persistedBook.setDeleted(false); when(testBookRepository.findById(testId)) .thenReturn(Optional.of(persistedBook)); - when(testBookRepository.save(any(Book.class))) - .thenReturn(persistedBook); - // Act: call the service method we are testing testBookService.deleteBookById(testId); // Assert: the entity was marked deleted - assertTrue(persistedBook.isDeleted()); + ArgumentCaptor bookCaptor = ArgumentCaptor.forClass(Book.class); + verify(testBookRepository, times(1)).save(bookCaptor.capture()); + + // extract from the captured object (internal list) + Book savedBook = bookCaptor.getValue(); + + // Assert: the service correctly marked it as deleted + assertTrue( + savedBook.isDeleted(), + "Book passed to save() should be marked deleted" + ); + // Assert: it is the same ID we attempted to delete + assertEquals(testId, savedBook.getId()); - // Assert: repository methods were called correctly - verify(testBookRepository, times(1)).findById(testId); - verify(testBookRepository, times(1)).save(persistedBook); } @Test