From c36920b8f0d1d6af4e6334522eba9fbf13ca8461 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 6 Nov 2025 16:57:37 +0000 Subject: [PATCH 01/22] Add package declaration to BookApplicationTests --- .../hmcts/BookAPI/BookApiApplicationTests.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/test/java/com/codesungrape/hmcts/BookAPI/BookApiApplicationTests.java diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookApiApplicationTests.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookApiApplicationTests.java new file mode 100644 index 0000000..eea05f7 --- /dev/null +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.codesungrape.hmcts.BookAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BookApiApplicationTests { + + @Test + void contextLoads() { + } + +} From ce18bad50312c425e05905c44b48680375de9309 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Thu, 6 Nov 2025 17:03:03 +0000 Subject: [PATCH 02/22] Add test setup for BookService with mocked repository --- .../hmcts/BookAPI/BookServiceTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java new file mode 100644 index 0000000..2b18945 --- /dev/null +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -0,0 +1,55 @@ +package com.codesungrape.hmcts.BookAPI; + +import com.codesungrape.hmcts.BookAPI.dto.BookRequest; +import com.codesungrape.hmcts.BookAPI.entity.Book; +import com.codesungrape.hmcts.BookAPI.repository.BookRepository; +import com.codesungrape.hmcts.BookAPI.service.BookService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; +import java.util.UUID; + +// Annotation tells JUnit to use Mockito +@ExtendWith(MockitoExtension.class) +class BookServiceTest { + + // Arrange: Mock a fake BookRepository + @Mock + private BookRepository testBookRepository; + + // Service to Test: Real service with fake repo injected + @InjectMocks + private BookService testBookService; + + // Test data setup (HMCTS naming consistency enforced) + private BookRequest validBookRequest; + private Book persistedBook; + private UUID testId; + + @BeforeEach + void setUp() { + testId = UUID.randomUUID(); + + validBookRequest = new BookRequest( + "The Great Java Gatsby", + "A story about unit testing and wealth.", + "F. Scott Spring" + ); + + // This simulates a Book object as it would look coming back from the DB + persistedBook = Book.builder() + .id(testId) + .title(validBookRequest.getTitle()) + .synopsis(validBookRequest.getSynopsis()) + .author(validBookRequest.getAuthor()) + .deleted(false) + .createdAt(java.time.Instant.now()) + .build(); + } + +} \ No newline at end of file From 98ef1853373d5421b952f424f5b77bf84720148b Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 09:39:19 +0000 Subject: [PATCH 03/22] Add test for createBook success --- .../hmcts/BookAPI/BookServiceTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 2b18945..16a3e54 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -14,6 +14,11 @@ import java.util.Optional; import java.util.UUID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + // Annotation tells JUnit to use Mockito @ExtendWith(MockitoExtension.class) class BookServiceTest { @@ -52,4 +57,27 @@ void setUp() { .build(); } + // --------- TESTS ------------ + + @Test + void testCreateBook_Success() { + + // Arrange: tell the mock repository what to do when called + when(testBookRepository.save(any(Book.class))).thenReturn(persistedBook); + + // Act: call the service method we are testing + Book result = testBookService.createBook(validBookRequest); + + // Assert: Check the outcome + assertNotNull(result); + assertEquals(testId, result.getId()); + assertEquals(validBookRequest.getTitle(), result.getTitle()); + assertEquals(validBookRequest.getSynopsis(), result.getSynopsis()); + assertEquals(validBookRequest.getAuthor(), result.getAuthor()); + + // Did the service perform the correct action on its dependency? + verify(testBookRepository, times(1)).save(any(Book.class)); + + } + } \ No newline at end of file From c9c366bc8e7a9f30df6ab396a689424029ea4224 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 09:55:35 +0000 Subject: [PATCH 04/22] Add test for blank title edge case --- .../codesungrape/hmcts/BookAPI/BookServiceTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 16a3e54..5771597 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -80,4 +80,14 @@ void testCreateBook_Success() { } + @Test + void testCreateBook_BlankTitle_ThrowsException() { + // Arrange + BookRequest invalidRequest = new BookRequest(" ", "Synopsis", "Author"); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> { + testBookService.createBook(invalidRequest); + }); + } } \ No newline at end of file From bbee6901d2fc1c0a9f32ba2933b036906d4d8e19 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 09:57:55 +0000 Subject: [PATCH 05/22] Add test for null/invalid input tests --- .../hmcts/BookAPI/BookServiceTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 5771597..4ba0072 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -80,6 +80,31 @@ void testCreateBook_Success() { } + @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 From 1a78f63a816dc70e36bc12f76129b0c79e227d8b Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 10:13:39 +0000 Subject: [PATCH 06/22] Add NullPointerException when entire request is null --- .../com/codesungrape/hmcts/BookAPI/BookServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 4ba0072..7eac60a 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -80,6 +80,14 @@ void testCreateBook_Success() { } + @Test + void testCreateBook_NullRequest_ThrowsException() { + // Act & Assert + assertThrows(NullPointerException.class, () -> { + testBookService.createBook(null); + }); + } + @Test void testCreateBook_NullTitle_ThrowsException() { // Arrange From dc7b207f3b3da5e57c9a077c8553bb5f3d25c357 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 10:50:42 +0000 Subject: [PATCH 07/22] Add BookService.java with createBook() and validation; TDD tests passing --- .../hmcts/BookAPI/service/BookService.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java new file mode 100644 index 0000000..a005237 --- /dev/null +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java @@ -0,0 +1,44 @@ +package com.codesungrape.hmcts.BookAPI.service; + +import com.codesungrape.hmcts.BookAPI.dto.BookRequest; +import com.codesungrape.hmcts.BookAPI.repository.BookRepository; +import org.springframework.stereotype.Service; // Marks a class as a Service Layer component. +import lombok.RequiredArgsConstructor; + +import com.codesungrape.hmcts.BookAPI.entity.Book; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service layer responsible for all business logic related to the Book resource. + */ +@Service +@RequiredArgsConstructor // Lombok creates constructor for dependency injection +public class BookService { + + // Create a field to store the repo + private final BookRepository bookRepository; + + // 1. CREATE Operation (POST /books) + public Book createBook(BookRequest request) { + // Validation check for business rules (e.g., uniqueness, if required) would go here + if (request == null) { + throw new NullPointerException("BookRequest cannot be null"); + } + + if (request.getTitle() == null || request.getTitle().isBlank()) { + throw new IllegalArgumentException("Book title cannot be null or blank"); + } + + // Map DTO to Entity + Book newBook = Book.builder() //Cannot resolve method 'builder' in 'Book' + .title(request.getTitle()) + .author(request.getAuthor()) + .synopsis(request.getSynopsis()) + // ID and created_at are auto-generated by JPA/DB + .build(); + + return bookRepository.save(newBook); + } +} \ No newline at end of file From 36e31f0f41043941cb9046abb474d841fac2a4d9 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 10:52:28 +0000 Subject: [PATCH 08/22] Add basic scaffolding for book request, book entity and book repository --- .../hmcts/BookAPI/BookApiApplication.java | 13 + .../hmcts/BookAPI/dto/BookRequest.java | 27 ++ .../hmcts/BookAPI/entity/Book.java | 63 +++++ .../BookAPI/repository/BookRepository.java | 24 ++ src/main/resources/application.yaml | 3 + src/main/resources/openapi.yml | 258 ++++++++++++++++++ 6 files changed, 388 insertions(+) create mode 100644 src/main/java/com/codesungrape/hmcts/BookAPI/BookApiApplication.java create mode 100644 src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java create mode 100644 src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java create mode 100644 src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/openapi.yml diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/BookApiApplication.java b/src/main/java/com/codesungrape/hmcts/BookAPI/BookApiApplication.java new file mode 100644 index 0000000..cb2617c --- /dev/null +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/BookApiApplication.java @@ -0,0 +1,13 @@ +package com.codesungrape.hmcts.BookAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BookApiApplication { + + public static void main(String[] args) { + SpringApplication.run(BookApiApplication.class, args); + } + +} diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java b/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java new file mode 100644 index 0000000..302ea2c --- /dev/null +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java @@ -0,0 +1,27 @@ +package com.codesungrape.hmcts.BookAPI.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import lombok.Value; + +/** + * DTO representing the required input for creating or replacing a Book resource. + * This class mirrors the OpenAPI 'BookInput' schema. + * @Value: Makes all fields 'final' (immutable), generates constructor, getters, and equals/hashCode/toString. + * @NotBlank: Enforces the required status from your OpenAPI schema. If the field is missing or an empty string, Spring will return a 400 Bad Request. + * @JsonProperty: Jackson library - maps snake_case JSON (HMCTS rules) to camelCase Java + */ +@Value +public class BookRequest { + @NotBlank(message= "Title is required") // enforces + @JsonProperty("title") + String title; + + @NotBlank(message = "Synopsis is required") + @JsonProperty("synopsis") + String synopsis; + + @NotBlank(message = "Author is required") + @JsonProperty("author") + String author; +} \ No newline at end of file diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java b/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java new file mode 100644 index 0000000..32f13bb --- /dev/null +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java @@ -0,0 +1,63 @@ +package com.codesungrape.hmcts.BookAPI.entity; + +import jakarta.persistence.*; +import java.util.UUID; +import lombok.*; + +/** + * JPA Entity representing the Book table in PostgreSQL. + * This holds the persisted state of the resource. + * HMCTS Rule Check: IDs must be opaque strings. Using UUID for distributed ID generation. + * @Entity: Marks the class as a JPA entity - tells hibernate to map Java classes to database tables. + * @Table: Defines which database table this entity maps to. HMCTS Naming: Lowercase, singular table name is common practice. + * Lombok annotations: + * @Getter: Automatically generates getters for all fields. + * @Setter: Automatically generates setters. + * @AllArgsConstructor: Generates a no-argument constructor (required by JPA). + * JPA needs to instantiate the enity using reflection. 'PROTECTED' prevents misuse. + * @Builder: Adds a builder pattern for clean object creation. +* You can do Book.builder().title("A").author("B").build(); + */ +@Entity +@Table(name = "book") +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // For JPA/Hibernate requirements +@AllArgsConstructor // For easy construction in tests +@Builder // For convenience in creating instances +public class Book { + + @Id // Primary key of the table + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", nullable = false) // maps the field to a database column names 'id' + 'nullable =false' database column cannot be NULL. + private UUID id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "synopsis", nullable = false, columnDefinition = "TEXT") + private String synopsis; + + @Column(name = "author", nullable = false) + private String author; + + // Soft delete - makes DELETE operations idempotent (safe to repeat) + @Column(name = "deleted", nullable = false) + private boolean deleted = false; + + @Column(name = "created_at", nullable = false) + private java.time.Instant createdAt = java.time.Instant.now(); + + @Column(name = "modified_at") + private java.time.Instant modifiedAt; + + // --- Business Logic Helper --- + // HMCTS mandates business logic in services, but a setter hook is acceptable. + // Lifecycle callback - special method runs automatically before Hibernate updates a record in the database. + @PreUpdate + protected void onUpdate() { + this.modifiedAt = java.time.Instant.now(); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java b/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java new file mode 100644 index 0000000..4614bb8 --- /dev/null +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java @@ -0,0 +1,24 @@ +package com.codesungrape.hmcts.BookAPI.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import com.codesungrape.hmcts.BookAPI.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository interface for Book Entity. + * Spring Data JPA automatically provides CRUD operations based on the Entity and ID type. + */ +@Repository +public interface BookRepository extends JpaRepository { // same error msg here: Missing package statement: 'com.codesungrape.hmcts.BookAPI.repository' + + // Custom query to find books that have NOT been soft-deleted + List findAllByDeletedFalse(); + + // Custom query to find a specific, non-deleted book by ID. + Optional findByIdAndDeleteFalse(UUID id); + +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..11e45a7 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: BookAPI diff --git a/src/main/resources/openapi.yml b/src/main/resources/openapi.yml new file mode 100644 index 0000000..6029081 --- /dev/null +++ b/src/main/resources/openapi.yml @@ -0,0 +1,258 @@ +spring: + application: + name: BookAPI +# --- +openapi: 3.0.3 +# -------------------------------------------- +# Info +info: + title: Book Collection API + version: v1.0.0 + description: A simple API to manage a collection of books. + termsOfService: 'https://github.com/methods/S_BookAPIV.2' + contact: + email: booksAPI@example.com + license: + name: MIT License + url: 'https://github.com/methods/S_BookAPIV.2/blob/main/LICENSE.md' + +# -------------------------------------------- +# Server +servers: + - url: http://localhost:5000 + description: Development server + +# -------------------------------------------- +# Tags +tags: + - name: Books + description: Operations related to books + - name: Reservations + description: Operations related to book reservations + - name: Authentication + description: Operations related to user registration and login + +# -------------------------------------------- +# Components +components: + # ... securitySchemes remain the same ... + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-KEY + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: "Enter JWT Bearer token" + + parameters: + BookId: + name: book_id + in: path + required: true + description: The unique identifier of the book (standard UUID format). + schema: + type: string + format: uuid # UPDATED: Using UUID format + example: "a1b2c3d4-e5f6-7890-1234-567890abcdef" # UPDATED: Example UUID + ReservationId: + name: res_id + in: path + required: true + description: The unique identifier of the reservation (standard UUID format). + schema: + type: string + format: uuid # UPDATED: Using UUID format + example: "b1c2d3e4-f5a6-7890-1234-567890fedcba" # UPDATED: Example UUID + Offset: + # ... remains the same ... + name: offset + in: query + description: The number of items to skip before starting to collect the result set. Must be a positive number and not exceed the system maximum. + required: false + schema: + type: integer + default: 0 + minimum: 0 + Limit: + # ... remains the same ... + name: limit + in: query + description: The maximum number of items to return. Must be a positive number and not exceed the system maximum. + required: false + schema: + type: integer + default: 20 + minimum: 1 + maximum: 1000 + + schemas: + # --- Book Schemas --- + BookInput: + # ... remains the same ... + type: object + required: + - title + - synopsis + - author + properties: + title: + type: string + example: "The Hitchhiker's Guide to the Galaxy" + synopsis: + type: string + example: "Seconds before the Earth is demolished to make way for a galactic freeway..." + author: + type: string + example: "Douglas Adams" + + # Schema for the HATEOAS links object (No change to format, but target IDs will be UUIDs) + Links: + # ... remains the same ... + type: object + required: + - self + - reservations + - reviews + properties: + self: + type: string + description: Link to the book resource itself. + format: uri + reservations: + type: string + description: Link to reservations for this book. + format: uri + reviews: + type: string + description: Link to reviews for this book. + format: uri + + # Schema for the full Book object as returned by the server + BookOutput: + allOf: + - $ref: '#/components/schemas/BookInput' + properties: + id: + type: string + format: uuid # UPDATED: Explicit UUID format + description: The unique identifier for the book, generated by the server (UUID). + readOnly: true + example: "a1b2c3d4-e5f6-7890-1234-567890abcdef" # UPDATED: Example UUID + links: + $ref: '#/components/schemas/Links' + readOnly: true + + # Schema for the full Books Database Object as returned by the server + BookListResponse: + # ... remains the same ... + type: object + properties: + total_count: + type: integer + description: Total number of books + example: 3 + items: + type: array + items: + $ref: '#/components/schemas/BookOutput' + + # ----- AUTH schemas ------------- + + UserOutput: + type: object + properties: + id: + type: string + format: uuid # UPDATED: User ID is now UUID + readOnly: true + example: "c1d2e3f4-g5h6-7890-1234-567890abcdef" # UPDATED: Example UUID + email: + type: string + format: email + readOnly: true + example: "newuser@example.com" + + + # ------- Reservation schemas ---------- + + # ... ReservationUserOutput remains the same ... + # Schema for the PUT /reservations/{res_id} request body + ReservationUserOutput: + type: object + properties: + forenames: + type: string + example: "John" + middlenames: + type: string + example: "Fitzgerald" + surname: + type: string + example: "Doe" + + # ... ReservationLinks remains the same ... + ReservationLinks: + type: object + properties: + self: + type: string + format: uri + description: A link to the reservation resource itself. + book: + type: string + format: uri + description: A link to the parent book resource. + readOnly: true + + # Schema for the full Reservation object returned by the API + ReservationOutput: + type: object + properties: + id: + type: string + format: uuid # UPDATED: Reservation ID is UUID + description: The unique identifier for the reservation (UUID). + example: "b1c2d3e4-f5a6-7890-1234-567890fedcba" # UPDATED: Example UUID + state: + type: string + description: The current state of the reservation. + example: "reserved" + readOnly: true + user_id: + type: string + format: uuid # UPDATED: User ID is UUID + description: The ID of the user who made the reservation (UUID). + example: "c1d2e3f4-g5h6-7890-1234-567890abcdef" # UPDATED: Example UUID + book_id: + type: string + format: uuid # UPDATED: Book ID is UUID + description: The ID of the book being reserved (UUID). + example: "a1b2c3d4-e5f6-7890-1234-567890abcdef" # UPDATED: Example UUID + links: + $ref: '#/components/schemas/ReservationLinks' + reservationDate: + type: string + format: date-time + description: The timestamp when the reservation was made. + example: "2023-10-27T10:00:00Z" + + # Schema for a paginated list of reservations + ReservationListResponse: + type: object + properties: + total_count: + type: integer + description: Total number of reservations for this book + example: 5 + items: + type: array + items: + $ref: '#/components/schemas/ReservationOutput' + + + # ------ ERROR schemas ---------- + # Error examples that mention invalid ID formats would also need adjustment + # ... (omitted for brevity, but this is the final check) \ No newline at end of file From 314cfeb1b4aedeff0bec11cd70d5d731c4f88927 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 11:28:23 +0000 Subject: [PATCH 09/22] add failing tests for repository failures --- .../hmcts/BookAPI/BookServiceTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 7eac60a..3bb2269 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -19,6 +19,16 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +/** + * @ExtendWith(MockitoExtension.class): tells JUnit 5 to use Mockito's extension and automatically initializes all @Mock and @InjectMocks fields when running this test class. + * @Mock: Creates a fake version (mock) of the dependency. + * @InjectMocks: creates an instance of the real class under test. + * @BeforeEach: Runs before each test method in the class. + * @Test: Marks the method as a test case that JUnit should execute. + * + */ + + // Annotation tells JUnit to use Mockito @ExtendWith(MockitoExtension.class) class BookServiceTest { @@ -123,4 +133,29 @@ void testCreateBook_BlankTitle_ThrowsException() { testBookService.createBook(invalidRequest); }); } + + // --------- Repository failures + @Test + void testCreateBook_RepositoryFailure_ThrowsException() { + // Arrange + when(testBookRepository.save(any(Book.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + // Act & assert + assertThrows(RuntimeException.class, () -> { + testBookService.createBook(validBookRequest); + }); + } + + @Test + void testCreateBook_RepositoryReturnsNull_HandlesGracefully() { + // Arrange + when(testBookRepository.save(any(Book.class))) + .thenReturn(null); + + // Act & assert + assertThrows(IllegalStateException.class, () -> { + testBookService.createBook(validBookRequest); + }); + } } \ No newline at end of file From f5aec79a644a10ee04db2e08a869e8800e63845a Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 11:35:25 +0000 Subject: [PATCH 10/22] Add defensive check for learning purposes --- .../codesungrape/hmcts/BookAPI/service/BookService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java index a005237..bb2961d 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java @@ -39,6 +39,13 @@ public Book createBook(BookRequest request) { // ID and created_at are auto-generated by JPA/DB .build(); - return bookRepository.save(newBook); + Book savedBook = bookRepository.save(newBook); + + // Defensive check (even though it "shouldn't" happen) + if (savedBook == null) { + throw new IllegalStateException("Failed to save book - repository returned null"); + } + + return savedBook; } } \ No newline at end of file From 5e298ee1cf11c830332fca9213702300cdd66ba6 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 11:35:58 +0000 Subject: [PATCH 11/22] Add clearer comments --- .../com/codesungrape/hmcts/BookAPI/service/BookService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java index bb2961d..852f216 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java @@ -41,7 +41,7 @@ public Book createBook(BookRequest request) { Book savedBook = bookRepository.save(newBook); - // Defensive check (even though it "shouldn't" happen) + // Defensive check (even though it "shouldn't" happen aka follows JPA contract) if (savedBook == null) { throw new IllegalStateException("Failed to save book - repository returned null"); } From 624be2db6abbfb237f9feb6d68d0093c1a7f54b2 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 12:14:32 +0000 Subject: [PATCH 12/22] Add edgecase tests: longtitle, longSynopsis, specialChars in title --- .../hmcts/BookAPI/BookServiceTest.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 3bb2269..0209bde 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -11,6 +11,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.util.Assert; + import java.util.Optional; import java.util.UUID; @@ -158,4 +160,94 @@ void testCreateBook_RepositoryReturnsNull_HandlesGracefully() { testBookService.createBook(validBookRequest); }); } + + // ----- EDGE cases --------- + @Test + void testCreateBook_VeryLongTitle_Success() { + // Arrange + String longTitle = "A".repeat(500); + BookRequest longTitleRequest = new BookRequest(longTitle, "Synopsis", "Author"); + + Book expectedBook = Book.builder() + .id(testId) + .title(longTitle) + .synopsis("Synopsis") + .author("Author") + .build(); + + when(testBookRepository.save(any(Book.class))) + .thenReturn(expectedBook); + + // Act + Book result = testBookService.createBook(longTitleRequest); + + // Assert + assertNotNull(result); + assertEquals(testId, result.getId()); + assertEquals(longTitle, result.getTitle()); + assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); + assertEquals(expectedBook.getAuthor(), result.getAuthor()); + + // Did the service perform the correct action on its dependency? + verify(testBookRepository, times(1)).save(any(Book.class)); + + } + + @Test + void testCreateBook_VeryLongSynopsis_Success() { + + // Arrange + String longSynopsis = "A".repeat(1000); + BookRequest longSynopsisRequest = new BookRequest("Title", longSynopsis, "Author"); + + Book expectedBook = Book.builder() + .id(testId) + .title("Title") + .synopsis(longSynopsis) + .author("Author") + .build(); + + when(testBookRepository.save(any(Book.class))) + .thenReturn(expectedBook); + + // Act + Book result = testBookService.createBook(longSynopsisRequest); + + // Assert + assertEquals(longSynopsis, result.getSynopsis()); + + } + + @Test + void testCreateBook_SpecialCharactersInTitle_Success() { + // Arrange + BookRequest specialRequest = new BookRequest( + "Test: A Book! @#$%^&*()", + "Synopsis", + "Author" + ); + + Book expectedBook = Book.builder() + .id(testId) + .title(specialRequest.getTitle()) + .synopsis(specialRequest.getSynopsis()) + .author(specialRequest.getAuthor()) + .build(); + + when(testBookRepository.save(any(Book.class))) + .thenReturn(expectedBook); + + // Act + Book result = testBookService.createBook(specialRequest); + + // Assert + assertNotNull(result); + assertEquals(testId, result.getId()); + assertEquals(specialRequest.getTitle(), result.getTitle()); + assertEquals(specialRequest.getSynopsis(), result.getSynopsis()); + assertEquals(specialRequest.getAuthor(), result.getAuthor()); + + // Did the service perform the correct action on its dependency? + verify(testBookRepository, times(1)).save(any(Book.class)); + } } \ No newline at end of file From 1b5fa563163498e7eb250031ea6be7273dcaef88 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 12:32:52 +0000 Subject: [PATCH 13/22] Refactor test to use @ParameterizedTest/CsvSource + update build.gradle --- build.gradle | 1 + .../hmcts/BookAPI/BookServiceTest.java | 135 ++++++++++++------ 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/build.gradle b/build.gradle index 9b4d7bf..3d04508 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.junit.jupiter:junit-jupiter-params' } tasks.named('test') { diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 0209bde..4cdb43e 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -12,6 +12,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.util.Assert; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.util.Optional; import java.util.UUID; @@ -162,61 +164,112 @@ void testCreateBook_RepositoryReturnsNull_HandlesGracefully() { } // ----- EDGE cases --------- - @Test - void testCreateBook_VeryLongTitle_Success() { - // Arrange - String longTitle = "A".repeat(500); - BookRequest longTitleRequest = new BookRequest(longTitle, "Synopsis", "Author"); - Book expectedBook = Book.builder() - .id(testId) - .title(longTitle) - .synopsis("Synopsis") - .author("Author") - .build(); + @ParameterizedTest + @CsvSource({ + "500, title, 'A very long title test'", + "1000, synopsis, 'A very long synopsis test'" + }) + void testCreateBook_VeryLongFields_Success(int repeatCount, String fieldType, String description) { + + // Arrange + String longText = "A".repeat(repeatCount); + + BookRequest request; + Book expectedBook; + + if (fieldType.equals("title")) { + request = new BookRequest(longText, "Synopsis", "Author"); + expectedBook = Book.builder() + .id(testId) + .title(longText) + .synopsis("Synopsis") + .author("Author") + .build(); + } else { + request = new BookRequest("Title", longText, "Author"); + expectedBook = Book.builder() + .id(testId) + .title("Title") + .synopsis(longText) + .author("Author") + .build(); + } when(testBookRepository.save(any(Book.class))) .thenReturn(expectedBook); // Act - Book result = testBookService.createBook(longTitleRequest); + Book result = testBookService.createBook(request); // Assert assertNotNull(result); assertEquals(testId, result.getId()); - assertEquals(longTitle, result.getTitle()); - assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); - assertEquals(expectedBook.getAuthor(), result.getAuthor()); - // Did the service perform the correct action on its dependency? - verify(testBookRepository, times(1)).save(any(Book.class)); + if (fieldType.equals("title")) { + assertEquals(longText, result.getTitle()); + } else { + assertEquals(longText, result.getSynopsis()); + } + verify(testBookRepository, times(1)).save(any(Book.class)); } - @Test - void testCreateBook_VeryLongSynopsis_Success() { - - // Arrange - String longSynopsis = "A".repeat(1000); - BookRequest longSynopsisRequest = new BookRequest("Title", longSynopsis, "Author"); - - Book expectedBook = Book.builder() - .id(testId) - .title("Title") - .synopsis(longSynopsis) - .author("Author") - .build(); - - when(testBookRepository.save(any(Book.class))) - .thenReturn(expectedBook); - - // Act - Book result = testBookService.createBook(longSynopsisRequest); - - // Assert - assertEquals(longSynopsis, result.getSynopsis()); - - } +// @Test +// void testCreateBook_VeryLongTitle_Success() { +// // Arrange +// String longTitle = "A".repeat(500); +// BookRequest longTitleRequest = new BookRequest(longTitle, "Synopsis", "Author"); +// +// Book expectedBook = Book.builder() +// .id(testId) +// .title(longTitle) +// .synopsis("Synopsis") +// .author("Author") +// .build(); +// +// when(testBookRepository.save(any(Book.class))) +// .thenReturn(expectedBook); +// +// // Act +// Book result = testBookService.createBook(longTitleRequest); +// +// // Assert +// assertNotNull(result); +// assertEquals(testId, result.getId()); +// assertEquals(longTitle, result.getTitle()); +// assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); +// assertEquals(expectedBook.getAuthor(), result.getAuthor()); +// +// // Did the service perform the correct action on its dependency? +// verify(testBookRepository, times(1)).save(any(Book.class)); +// +// } +// +// @Test +// void testCreateBook_VeryLongSynopsis_Success() { +// +// // Arrange +// String longSynopsis = "A".repeat(1000); +// BookRequest longSynopsisRequest = new BookRequest("Title", longSynopsis, "Author"); +// +// Book expectedBook = Book.builder() +// .id(testId) +// .title("Title") +// .synopsis(longSynopsis) +// .author("Author") +// .build(); +// +// when(testBookRepository.save(any(Book.class))) +// .thenReturn(expectedBook); +// +// // Act +// Book result = testBookService.createBook(longSynopsisRequest); +// +// // Assert +// assertEquals(longSynopsis, result.getSynopsis()); +// +// } @Test void testCreateBook_SpecialCharactersInTitle_Success() { From 0176bece2ae21c625d26d817af1f05f334f013a1 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Fri, 7 Nov 2025 13:55:41 +0000 Subject: [PATCH 14/22] test: use @MethodSource to pass objects instead of primitives --- .../hmcts/BookAPI/BookServiceTest.java | 128 +++++++++++++----- 1 file changed, 92 insertions(+), 36 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 4cdb43e..e9cbed5 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.provider.Arguments; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -14,6 +15,9 @@ import org.springframework.util.Assert; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.Arguments; +import java.util.stream.Stream; import java.util.Optional; import java.util.UUID; @@ -165,37 +169,11 @@ void testCreateBook_RepositoryReturnsNull_HandlesGracefully() { // ----- EDGE cases --------- - @ParameterizedTest - @CsvSource({ - "500, title, 'A very long title test'", - "1000, synopsis, 'A very long synopsis test'" - }) - void testCreateBook_VeryLongFields_Success(int repeatCount, String fieldType, String description) { + @ParameterizedTest(name= "{0}") // Display the test name + @MethodSource("provideLongFieldTestCases") + void testCreateBook_VeryLongFields_Success(String testName, BookRequest request, Book expectedBook) { // Arrange - String longText = "A".repeat(repeatCount); - - BookRequest request; - Book expectedBook; - - if (fieldType.equals("title")) { - request = new BookRequest(longText, "Synopsis", "Author"); - expectedBook = Book.builder() - .id(testId) - .title(longText) - .synopsis("Synopsis") - .author("Author") - .build(); - } else { - request = new BookRequest("Title", longText, "Author"); - expectedBook = Book.builder() - .id(testId) - .title("Title") - .synopsis(longText) - .author("Author") - .build(); - } - when(testBookRepository.save(any(Book.class))) .thenReturn(expectedBook); @@ -204,17 +182,95 @@ void testCreateBook_VeryLongFields_Success(int repeatCount, String fieldType, St // Assert assertNotNull(result); - assertEquals(testId, result.getId()); - - if (fieldType.equals("title")) { - assertEquals(longText, result.getTitle()); - } else { - assertEquals(longText, result.getSynopsis()); - } + 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)); } + // Provide test data, static method: can be called without creating an object. + private static Stream provideLongFieldTestCases() { + UUID testId = UUID.randomUUID(); + + String longTitle = "A".repeat(500); + String longSynopsis = "A".repeat(1000); + + return Stream.of( + Arguments.of( + "Very long title (500 chars)", + new BookRequest(longTitle, "Synopsis", "Author"), + Book.builder() + .id(testId) + .title(longTitle) + .synopsis("Synopsis") + .author("Author") + .build() + ), + Arguments.of( + "Very long synopsis (1000 chars)", + new BookRequest("Title", longSynopsis, "Author"), + Book.builder() + .id(testId) + .title("Title") + .synopsis(longSynopsis) + .author("Author") + .build() + ) + ); + } + +// @ParameterizedTest +// @CsvSource({ +// "500, title, 'A very long title test'", +// "1000, synopsis, 'A very long synopsis test'" +// }) +// void testCreateBook_VeryLongFields_Success(int repeatCount, String fieldType, String description) { +// +// // Arrange +// String longText = "A".repeat(repeatCount); +// +// BookRequest request; +// Book expectedBook; +// +// if (fieldType.equals("title")) { +// request = new BookRequest(longText, "Synopsis", "Author"); +// expectedBook = Book.builder() +// .id(testId) +// .title(longText) +// .synopsis("Synopsis") +// .author("Author") +// .build(); +// } else { +// request = new BookRequest("Title", longText, "Author"); +// expectedBook = Book.builder() +// .id(testId) +// .title("Title") +// .synopsis(longText) +// .author("Author") +// .build(); +// } +// +// when(testBookRepository.save(any(Book.class))) +// .thenReturn(expectedBook); +// +// // Act +// Book result = testBookService.createBook(request); +// +// // Assert +// assertNotNull(result); +// assertEquals(testId, result.getId()); +// +// if (fieldType.equals("title")) { +// assertEquals(longText, result.getTitle()); +// } else { +// assertEquals(longText, result.getSynopsis()); +// } +// +// verify(testBookRepository, times(1)).save(any(Book.class)); +// } + // @Test // void testCreateBook_VeryLongTitle_Success() { // // Arrange From 1e35f74a7f346d992f550e69e159a2cb8d65c4a7 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 17:15:10 +0000 Subject: [PATCH 15/22] Clean up unused imports and comments --- .../hmcts/BookAPI/service/BookService.java | 5 +---- .../codesungrape/hmcts/BookAPI/BookServiceTest.java | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java index 852f216..4ad3119 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java @@ -6,9 +6,6 @@ import lombok.RequiredArgsConstructor; import com.codesungrape.hmcts.BookAPI.entity.Book; -import java.util.List; -import java.util.Optional; -import java.util.UUID; /** * Service layer responsible for all business logic related to the Book resource. @@ -22,7 +19,7 @@ public class BookService { // 1. CREATE Operation (POST /books) public Book createBook(BookRequest request) { - // Validation check for business rules (e.g., uniqueness, if required) would go here + // Validation check for business rules (e.g., uniqueness, if required) if (request == null) { throw new NullPointerException("BookRequest cannot be null"); } diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index e9cbed5..66c1cf7 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -10,16 +10,11 @@ import org.junit.jupiter.params.provider.Arguments; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.util.Assert; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.Arguments; import java.util.stream.Stream; -import java.util.Optional; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -171,7 +166,11 @@ void testCreateBook_RepositoryReturnsNull_HandlesGracefully() { @ParameterizedTest(name= "{0}") // Display the test name @MethodSource("provideLongFieldTestCases") - void testCreateBook_VeryLongFields_Success(String testName, BookRequest request, Book expectedBook) { + void testCreateBook_VeryLongFields_Success( + String testName, + BookRequest request, + Book expectedBook + ) { // Arrange when(testBookRepository.save(any(Book.class))) From 8f669dcde918a0ab8c744249c21075e7254b1d8c Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 19:44:30 +0000 Subject: [PATCH 16/22] fix(entity): Correct timestamp and builder defaults in Book - Use @PrePersist for to set timestamp at persistence time. - Use @Builder.Default for to ensure builder honors the default. --- .../hmcts/BookAPI/entity/Book.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java b/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java index 32f13bb..43cd2cd 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import java.util.UUID; import lombok.*; +import java.time.Instant; /** * JPA Entity representing the Book table in PostgreSQL. @@ -14,7 +15,7 @@ * @Getter: Automatically generates getters for all fields. * @Setter: Automatically generates setters. * @AllArgsConstructor: Generates a no-argument constructor (required by JPA). - * JPA needs to instantiate the enity using reflection. 'PROTECTED' prevents misuse. + * JPA needs to instantiate the entity using reflection. 'PROTECTED' prevents misuse. * @Builder: Adds a builder pattern for clean object creation. * You can do Book.builder().title("A").author("B").build(); */ @@ -29,7 +30,7 @@ public class Book { @Id // Primary key of the table @GeneratedValue(strategy = GenerationType.UUID) - @Column(name = "id", nullable = false) // maps the field to a database column names 'id' + 'nullable =false' database column cannot be NULL. + @Column(name = "id", nullable = false) // maps the field to a database column named 'id' + 'nullable =false' database column cannot be NULL. private UUID id; @Column(name = "title", nullable = false) @@ -42,20 +43,32 @@ public class Book { private String author; // Soft delete - makes DELETE operations idempotent (safe to repeat) + // Soft delete - using @Builder.Default to ensure the builder + // respects this initialization if the field is not set explicitly. @Column(name = "deleted", nullable = false) + @Builder.Default private boolean deleted = false; - @Column(name = "created_at", nullable = false) - private java.time.Instant createdAt = java.time.Instant.now(); + // `createdAt` is null upon object creation. + // It will be set by the `onCreate()` method right before persistence. + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; @Column(name = "modified_at") private java.time.Instant modifiedAt; + // --- JPA lifecycle callbacks --- + @PrePersist + protected void onCreate() { + this.createdAt = java.time.Instant.now(); + } + // --- Business Logic Helper --- // HMCTS mandates business logic in services, but a setter hook is acceptable. // Lifecycle callback - special method runs automatically before Hibernate updates a record in the database. @PreUpdate protected void onUpdate() { + this.modifiedAt = java.time.Instant.now(); } From 9a294c6d2daebd9c39e970dcc5333c8b78f90c3f Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 19:51:43 +0000 Subject: [PATCH 17/22] Leave comment about redundant validation check --- .../hmcts/BookAPI/service/BookService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java index 4ad3119..5022908 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/service/BookService.java @@ -24,12 +24,20 @@ public Book createBook(BookRequest request) { throw new NullPointerException("BookRequest cannot be null"); } + + // REVISIT: Leaving this here for now as i haven't implemented the Controller Layer yet + // The service layer is duplicating validation that already exists in the + // BookRequest DTO with @notblank annotations. Since the DTO has validation + // constraints, this manual check is redundant when Spring's validation + // framework is properly configured in the controller layer. + // Consider removing this duplication or adding a comment explaining + // why service-level validation is necessary in addition to DTO validation. if (request.getTitle() == null || request.getTitle().isBlank()) { throw new IllegalArgumentException("Book title cannot be null or blank"); } // Map DTO to Entity - Book newBook = Book.builder() //Cannot resolve method 'builder' in 'Book' + Book newBook = Book.builder() .title(request.getTitle()) .author(request.getAuthor()) .synopsis(request.getSynopsis()) From 0ee37edacfa548e35d0833e6c6c3a7b3666746ef Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 19:54:44 +0000 Subject: [PATCH 18/22] Clean up comments --- .../codesungrape/hmcts/BookAPI/repository/BookRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java b/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java index 4614bb8..5bc5413 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/repository/BookRepository.java @@ -13,7 +13,7 @@ * Spring Data JPA automatically provides CRUD operations based on the Entity and ID type. */ @Repository -public interface BookRepository extends JpaRepository { // same error msg here: Missing package statement: 'com.codesungrape.hmcts.BookAPI.repository' +public interface BookRepository extends JpaRepository { // Custom query to find books that have NOT been soft-deleted List findAllByDeletedFalse(); From e4da445a627320781d4a1fb5ca8833a7692a80e9 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 19:56:01 +0000 Subject: [PATCH 19/22] Clean up redundant comment --- .../java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java b/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java index 302ea2c..76667b4 100644 --- a/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java +++ b/src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java @@ -13,7 +13,7 @@ */ @Value public class BookRequest { - @NotBlank(message= "Title is required") // enforces + @NotBlank(message= "Title is required") @JsonProperty("title") String title; From 621eaefc56c210c40392d33bba22a149351a0dfc Mon Sep 17 00:00:00 2001 From: codesungrape Date: Tue, 11 Nov 2025 20:07:40 +0000 Subject: [PATCH 20/22] Remove redundant SpringBoot config from openapi.yml --- src/main/resources/openapi.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/resources/openapi.yml b/src/main/resources/openapi.yml index 6029081..4684e21 100644 --- a/src/main/resources/openapi.yml +++ b/src/main/resources/openapi.yml @@ -1,7 +1,3 @@ -spring: - application: - name: BookAPI -# --- openapi: 3.0.3 # -------------------------------------------- # Info From 3ecf354e0717ad47b7cbc4f8bc197b851ae8e77c Mon Sep 17 00:00:00 2001 From: codesungrape Date: Wed, 12 Nov 2025 07:41:46 +0000 Subject: [PATCH 21/22] Remove commented out code for redability and maintainability --- .../hmcts/BookAPI/BookServiceTest.java | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index 66c1cf7..c507263 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -220,112 +220,6 @@ private static Stream provideLongFieldTestCases() { ); } -// @ParameterizedTest -// @CsvSource({ -// "500, title, 'A very long title test'", -// "1000, synopsis, 'A very long synopsis test'" -// }) -// void testCreateBook_VeryLongFields_Success(int repeatCount, String fieldType, String description) { -// -// // Arrange -// String longText = "A".repeat(repeatCount); -// -// BookRequest request; -// Book expectedBook; -// -// if (fieldType.equals("title")) { -// request = new BookRequest(longText, "Synopsis", "Author"); -// expectedBook = Book.builder() -// .id(testId) -// .title(longText) -// .synopsis("Synopsis") -// .author("Author") -// .build(); -// } else { -// request = new BookRequest("Title", longText, "Author"); -// expectedBook = Book.builder() -// .id(testId) -// .title("Title") -// .synopsis(longText) -// .author("Author") -// .build(); -// } -// -// when(testBookRepository.save(any(Book.class))) -// .thenReturn(expectedBook); -// -// // Act -// Book result = testBookService.createBook(request); -// -// // Assert -// assertNotNull(result); -// assertEquals(testId, result.getId()); -// -// if (fieldType.equals("title")) { -// assertEquals(longText, result.getTitle()); -// } else { -// assertEquals(longText, result.getSynopsis()); -// } -// -// verify(testBookRepository, times(1)).save(any(Book.class)); -// } - -// @Test -// void testCreateBook_VeryLongTitle_Success() { -// // Arrange -// String longTitle = "A".repeat(500); -// BookRequest longTitleRequest = new BookRequest(longTitle, "Synopsis", "Author"); -// -// Book expectedBook = Book.builder() -// .id(testId) -// .title(longTitle) -// .synopsis("Synopsis") -// .author("Author") -// .build(); -// -// when(testBookRepository.save(any(Book.class))) -// .thenReturn(expectedBook); -// -// // Act -// Book result = testBookService.createBook(longTitleRequest); -// -// // Assert -// assertNotNull(result); -// assertEquals(testId, result.getId()); -// assertEquals(longTitle, result.getTitle()); -// assertEquals(expectedBook.getSynopsis(), result.getSynopsis()); -// assertEquals(expectedBook.getAuthor(), result.getAuthor()); -// -// // Did the service perform the correct action on its dependency? -// verify(testBookRepository, times(1)).save(any(Book.class)); -// -// } -// -// @Test -// void testCreateBook_VeryLongSynopsis_Success() { -// -// // Arrange -// String longSynopsis = "A".repeat(1000); -// BookRequest longSynopsisRequest = new BookRequest("Title", longSynopsis, "Author"); -// -// Book expectedBook = Book.builder() -// .id(testId) -// .title("Title") -// .synopsis(longSynopsis) -// .author("Author") -// .build(); -// -// when(testBookRepository.save(any(Book.class))) -// .thenReturn(expectedBook); -// -// // Act -// Book result = testBookService.createBook(longSynopsisRequest); -// -// // Assert -// assertEquals(longSynopsis, result.getSynopsis()); -// -// } - @Test void testCreateBook_SpecialCharactersInTitle_Success() { // Arrange From db2e5fe2ab7153e327ae3cbb033cfc06d6255076 Mon Sep 17 00:00:00 2001 From: codesungrape Date: Wed, 12 Nov 2025 08:31:48 +0000 Subject: [PATCH 22/22] Add comments for Test error to fix in next PR --- .../com/codesungrape/hmcts/BookAPI/BookServiceTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java index c507263..8d160c4 100644 --- a/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java +++ b/src/test/java/com/codesungrape/hmcts/BookAPI/BookServiceTest.java @@ -101,6 +101,13 @@ void testCreateBook_NullRequest_ThrowsException() { }); } + // CoPilot feedback: + //This test will fail because BookRequest uses @value from Lombok with @notblank validation. + //The @notblank constraint on the title field means that creating a BookRequest with a null + // title should trigger validation failure at the DTO level, not allow the object to be + // created. Either the test expectations are incorrect, or the DTO validation is not being + // applied. The same issue affects tests on lines 105-116, 119-127, and 130-138. + @Test void testCreateBook_NullTitle_ThrowsException() { // Arrange