Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 72 additions & 108 deletions src/test/java/com/codesungrape/hmcts/bookapi/BookServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Book> 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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<Book> 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());
}

// --------------------------------------------------------------------------------------------
Expand All @@ -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)
);
Expand All @@ -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<Book> 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
Expand Down