Skip to content

Lollo gro/exercise8#211

Open
LolloGro wants to merge 44 commits intomainfrom
LolloGro/exercise8
Open

Lollo gro/exercise8#211
LolloGro wants to merge 44 commits intomainfrom
LolloGro/exercise8

Conversation

@LolloGro
Copy link
Copy Markdown

@LolloGro LolloGro commented Nov 20, 2025

Exercise 8 - Spring Boot application

• Spring Data JPA for MySQL access
• Docker Compose for reproducible database setup
• Spring Security with customized access control
• REST API for books and stores
• Controller with endpoints for API and website
• E2E test of API endpoints
• Integration test for native SQL query in store repository
• Thymeleaf templates for simple view of books in store and page for admin to add new user

Summary by CodeRabbit

  • New Features
    • Browse Books page with dynamic loading; full REST API for listing, filtering, viewing, creating, updating, and deleting books
    • Store lookup by name/author and inventory endpoints
    • User registration, secure authentication, role-based access and a protected Secret page
    • Database migrations and demo data seeded for developer/test profiles
    • Docker Compose setup for a MySQL service
  • Style
    • New site layout and CSS for pages and header
  • Tests
    • Integration and repository tests added using container-backed test environment
  • Chores
    • Project build modernized to Spring Boot conventions; obsolete sample entry/test removed

✏️ Tip: You can customize this high-level summary in your review settings.

kappsegla and others added 30 commits October 8, 2025 15:56
…ady exist if not data is saved. Also checks that book with the samt title not already exists.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 20, 2025

Walkthrough

Adds a Spring Boot 3 web application for a book store: entities, repositories, DTOs, services, REST and MVC controllers, custom security provider and filter chain, Flyway migrations, Thymeleaf + HTMX views, startup data seeding, Docker Compose MySQL, and Testcontainers-based integration tests.

Changes

Cohort / File(s) Summary
Build & Runtime
\pom.xml`, `docker-compose.yml`, `src/main/resources/application.properties`, `src/main/resources/application-developer.properties``
Adopted Spring Boot parent and starters, simplified build/plugins, added docker-compose MySQL service and developer/test application properties.
Application Entry
Added: \src/main/java/org/example/Exercise2025Application.java`<br> **Removed:**`src/main/java/org/example/App.java``
Added Spring Boot main class annotated with @SpringBootApplication; removed old standalone App main.
Domain Entities
\src/main/java/org/example/entities/*``
Added JPA entities: Author, Book (NamedEntityGraph), Language, Store, Role, CustomizedUser, Inventory, InventoryId with mappings, constructors, accessors, proxy-safe equals/hashCode and relationships.
Enums
\src/main/java/org/example/Genre.java`, `src/main/java/org/example/Authority.java``
Added enums for book genres and user authorities.
DTOs / Records
\src/main/java/org/example/dto/*``
Added record-based DTOs: BookDto, Author, Language, StoreDto, UserDto, Inventory with Jakarta Validation annotations.
Repositories
\src/main/java/org/example/repository/*``
Added Spring Data repositories: BookRepository (entityGraph, genre/title finders), AuthorRepository, LanguageRepository, StoreRepository (custom @Query methods), InventoryRepository, RoleRepository, UserRepository.
Services
\src/main/java/org/example/service/*``
Added services: BookService (createBook with author/language resolution), UserService (create user, encode password, assign role), CustomUserDetailService (UserDetailsService), and CustomAuthenticationProvider.
Security & Config
\src/main/java/org/example/config/*``
Added SecurityConfig exposing a SecurityFilterChain with authorization rules and custom provider; added PasswordEncodeConfig providing BCryptPasswordEncoder.
Controllers (REST & MVC)
\src/main/java/org/example/controller/*``
Added REST controllers BookController, StoreController, UserController and MVC controllers HomePageController, BookPageController, SecretPageController (templates and APIs: CRUD, queries, registration).
Exception Handling
\src/main/java/org/example/exception/CustomException.java`, `src/main/java/org/example/exception/DuplicateBooks.java``
Added global @RestControllerAdvice handlers and DuplicateBooks runtime exception.
Dev Data Initialization
\src/main/java/org/example/config/DevDataInitialize.java``
Added ApplicationRunner component (profiles developer,test) that seeds roles, users, stores, authors, languages, books, and inventory on startup.
Flyway Migrations
\src/main/resources/db/migration/V1__author.sql` ... V6__.sql``
Added migrations to create schema: author, language, book, store, inventory, customized_user, role, and user_roles with PKs and FKs.
Templates & Static
\src/main/resources/templates/`, `src/main/resources/static/styles/``
Added Thymeleaf layout and pages (layout.html, index.html, books.html, listOfBooks.html, secret.html) with HTMX integration and CSS files.
SQL Utility
\src/main/java/org/example/sql/store.sql``
Added SQL query selecting store names joined through inventory/book/author for a specific author first_name.
HTTP Examples
\books.http`, `store.http``
Added HTTP request collections demonstrating login and API calls with placeholders for genre/id/author/name.
Tests & Test Infra
\src/test/java/**`, `src/test/resources/application-test.properties``
Removed legacy simple tests; added Testcontainers-based TestSetup, integration tests (BookControllerTest, StoreControllerTest, UserControllerTest, StoreRepositoryTest), and a context-load test; test properties enable SQL logging.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Browser as LoginForm
    participant Security as SecurityFilterChain
    participant Provider as CustomAuthenticationProvider
    participant UserSvc as CustomUserDetailService
    participant UserRepo as UserRepository
    participant Encoder as PasswordEncoder

    Client->>Browser: POST /login (username,password)
    Browser->>Security: Submit credentials
    Security->>Provider: authenticate(token)
    Provider->>UserSvc: loadUserByUsername(username)
    UserSvc->>UserRepo: findByUserName(username)
    UserRepo-->>UserSvc: CustomizedUser
    UserSvc-->>Provider: UserDetails (username, encodedPwd, authorities)
    Provider->>Encoder: matches(raw, encoded)
    Encoder-->>Provider: true/false
    alt password matches
        Provider-->>Security: authenticated token
        Security-->>Client: authentication success (session)
    else mismatch
        Provider-->>Client: authentication failure (401)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas to pay special attention:

  • Entity mappings, cascade rules, lazy loading and composite InventoryId correctness.
  • Flyway migration ordering and foreign key targets (V6__ vs earlier migrations).
  • Security config: authorization rules, CSRF disabled, role-to-authority mapping and password verification.
  • DevDataInitialize seeding order, transactional boundaries, and test-profile scope.
  • Testcontainers @ServiceConnection usage and test database replacement settings.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch LolloGro/exercise8

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8dc41c and 6889174.

📒 Files selected for processing (5)
  • src/main/resources/db/migration/V1__author.sql (1 hunks)
  • src/main/resources/db/migration/V2__language.sql (1 hunks)
  • src/main/resources/db/migration/V3__book.sql (1 hunks)
  • src/main/resources/db/migration/V4__store.sql (1 hunks)
  • src/main/resources/db/migration/V6__.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/resources/db/migration/V2__language.sql
  • src/main/resources/db/migration/V4__store.sql
  • src/main/resources/db/migration/V6__.sql
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: jennymakki
Repo: fungover/exercise2025 PR: 204
File: src/main/java/org/example/config/SecurityConfig.java:19-24
Timestamp: 2025-11-16T13:56:11.263Z
Learning: The repository fungover/exercise2025 is an exercise/learning project where hard-coded credentials and other shortcuts for ease of testing are acceptable and intentional.
🔇 Additional comments (1)
src/main/resources/db/migration/V1__author.sql (1)

1-7: Clean foundational schema.

The author table is well-structured with appropriate column types, constraints, and naming conventions. The design aligns with the relational requirements for the book store domain.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (32)
src/main/resources/static/styles/index.css (1)

1-4: Consider a more descriptive class name than .test.

The styling is fine, but a semantic name (e.g., .hero-text or .intro-message) will age better and be clearer when used in index.html.

src/main/resources/templates/index.html (1)

1-27: Index template wiring with layout fragments and welcome model attribute looks correct.

Header/footer fragments, CSS includes, and ${welcome} binding from HomePageController all line up; the page should render as expected. If you keep this beyond experimentation, you might also wrap the main content in a <main> element and use a more semantic class than test for the paragraph.

src/main/java/org/example/Genre.java (1)

1-5: LGTM with minor naming consideration.

The enum is well-defined. Note that NOVEL and FICTION are conceptually overlapping (a novel is a type of fiction), which might cause classification ambiguity. For an exercise project, this is acceptable.

src/main/resources/db/migration/V4__store.sql (1)

1-6: Consider making store_name NOT NULL.

The store_name column is nullable, which allows stores without names. While this might be intentional for flexibility, stores typically require identifying names for business logic.

If store names should be required, apply this diff:

 CREATE TABLE store
 (
     store_id   INT AUTO_INCREMENT NOT NULL,
-    store_name VARCHAR(255) NULL,
+    store_name VARCHAR(255) NOT NULL,
     CONSTRAINT pk_store PRIMARY KEY (store_id)
 );
src/main/resources/db/migration/V2__language.sql (1)

1-6: Consider making text_language NOT NULL.

The text_language column is nullable, which allows language records without actual language values. This seems counterintuitive as a language record should always have a language identifier.

If language values should be required, apply this diff:

 CREATE TABLE language
 (
     id            INT AUTO_INCREMENT NOT NULL,
-    text_language VARCHAR(255)       NULL,
+    text_language VARCHAR(255)       NOT NULL,
     CONSTRAINT pk_language PRIMARY KEY (id)
 );
docker-compose.yml (1)

1-11: Consider adding a volume mount for data persistence (optional for dev/test).

The MySQL service has no volume mounts, meaning all database data will be lost when the container stops. While this is acceptable for testing and development environments, consider documenting this behavior or adding an optional volume mount if persistence is desired.

Example addition:

   mysql:
     image: 'mysql:9.5.0'
     environment:
       - 'MYSQL_DATABASE=books'
       - 'MYSQL_PASSWORD=secret'
       - 'MYSQL_ROOT_PASSWORD=verysecret'
       - 'MYSQL_USER=user'
     ports:
       - '3306:3306'
+    # Uncomment to persist data across container restarts
+    # volumes:
+    #   - mysql-data:/var/lib/mysql
+# volumes:
+#   mysql-data:
src/test/java/org/example/controller/UserControllerTest.java (1)

20-31: Consider verifying user persistence in the test.

The test currently only validates the HTTP status code (201 CREATED) but doesn't verify that the user was actually persisted to the database or that the response contains the expected user data. While this provides basic coverage, consider adding assertions to verify the user exists in the database.

Example enhancement:

@Autowired
UserRepository userRepository; // Add this field

@Test
void newUserIsSuccessfullyCreated() {
    CustomizedUser newUser = new CustomizedUser("Test", "Testar");
    
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<CustomizedUser> request = new HttpEntity<>(newUser, headers);
    ResponseEntity<String> response = loggedInUser().exchange("/api/users", HttpMethod.POST, request, String.class);
    
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    
    // Verify persistence
    Optional<CustomizedUser> savedUser = userRepository.findByUsername("Test");
    assertThat(savedUser).isPresent();
    assertThat(savedUser.get().getUsername()).isEqualTo("Test");
}
src/main/resources/templates/listOfBooks.html (1)

1-7: Consider simplifying template structure (optional).

The fragment includes a full HTML structure (<html>, <head>, <body>) but only the fragment itself is used. While harmless, you could simplify by keeping only the fragment content since it's loaded dynamically via HTMX.

Example simplified version:

-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Title</title>
-</head>
-<body>
-    <div th:fragment="listOfBooks">
+<div th:fragment="listOfBooks">
     <h2>Books in store</h2>
     <ol>
     <li th:each="bookDto : ${books}"
     ><span th:text="${bookDto.title}"></span> by <span th:text="${bookDto.author.firstName}"></span> <span th:text="${bookDto.author.lastName}"></span></li>
     </ol>
-    </div>
-</body>
-</html>
+</div>
src/main/resources/db/migration/V3__book.sql (3)

7-8: Consider adding indexes on foreign key columns.

Foreign key columns language_id and author_id should have indexes to improve join performance. While MySQL InnoDB automatically creates indexes for foreign key constraints, it's good practice to explicitly define them for clarity.

Add these indexes after the table creation:

CREATE INDEX idx_book_language ON book(language_id);
CREATE INDEX idx_book_author ON book(author_id);

6-6: Consider adding a check constraint for rating validity (optional).

The rating column is defined as INT NOT NULL but has no validation on valid ranges. Consider adding a check constraint if ratings should be within a specific range (e.g., 1-5 or 0-10).

Example for 1-5 rating scale:

     rating      INT          NOT NULL,
+    CONSTRAINT chk_rating CHECK (rating BETWEEN 1 AND 5),

4-4: Consider making title NOT NULL (optional).

The title column is nullable, but books should typically always have titles. Consider whether NULL values are intentional or if this should be NOT NULL.

-    title       VARCHAR(255) NULL,
+    title       VARCHAR(255) NOT NULL,
src/main/java/org/example/repository/RoleRepository.java (1)

10-10: Consider returning Optional for consistency and null safety.

Other repositories in this codebase (e.g., LanguageRepository.findByTextLanguage, AuthorRepository.findByFirstNameAndLastName) return Optional to handle cases where the entity might not be found. Returning Role directly requires callers to perform null checks and creates potential NPE risks.

Apply this diff to improve null safety:

-   Role findByAuthority(Authority authority);
+   Optional<Role> findByAuthority(Authority authority);
src/main/java/org/example/dto/BookDto.java (1)

15-15: Consider adding validation for the rating field.

The rating field lacks validation constraints. Consider adding range validation to ensure ratings are within expected bounds (e.g., 0-5 or 1-10).

For example:

@Min(value = 0, message = "Rating must be at least 0")
@Max(value = 5, message = "Rating must not exceed 5")
int rating,
src/main/java/org/example/repository/StoreRepository.java (1)

14-21: Remove hard-coded schema name for better portability.

The query hard-codes the schema name books in table references (books.inventory, books.book, books.author). This reduces portability across environments where the schema name might differ.

Apply this diff:

     @Query(value = """
         SELECT store_name FROM store
-        JOIN books.inventory i on store.store_id = i.store_id
-        JOIN books.book b on b.book_id = i.book_id
-        JOIN books.author a on a.id = b.author_id
+        JOIN inventory i on store.store_id = i.store_id
+        JOIN book b on b.book_id = i.book_id
+        JOIN author a on a.id = b.author_id
         WHERE a.first_name = :author GROUP BY store_name;
 """, nativeQuery = true)
src/test/java/org/example/controller/StoreControllerTest.java (1)

28-31: Consider using a more flexible assertion.

The test expects exactly 2 stores, which could become brittle if seed data changes. Consider asserting stores.length > 0 or documenting why exactly 2 stores are expected from the test setup.

src/main/java/org/example/dto/UserDto.java (1)

5-9: Consider tightening validation constraints for username/password

The DTO works as-is, but @NotEmpty alone is very permissive, especially for passwords. You might want to use @NotBlank and add a minimum length (and possibly other complexity rules) for password, and maybe a length limit for userName. This remains optional given the exercise nature of the repo, but will make the API more realistic.

Example:

-import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;

-public record UserDto(
-        @NotEmpty(message = "Username must be inserted")
-        String userName,
-        @NotEmpty(message = "Password must be inserted")
-        String password) {
+public record UserDto(
+        @NotBlank(message = "Username must be inserted")
+        @Size(max = 64, message = "Username must be at most 64 characters")
+        String userName,
+        @NotBlank(message = "Password must be inserted")
+        @Size(min = 8, message = "Password must be at least 8 characters")
+        String password) {
 }

Based on learnings

src/main/java/org/example/service/BookService.java (1)

25-55: Make createBook transactional and consider more specific error handling

Functionally this works, but there are a couple of robustness improvements worth considering:

  • Without @Transactional, author and language may be created in separate transactions before the book; if bookRepository.save fails, you can end up with orphaned rows.
  • Throwing a bare RuntimeException("Book already exists") makes it harder to map to an HTTP status; a custom exception or ResponseStatusException would integrate better with the REST layer.

Potential refactor:

-import org.springframework.stereotype.Service;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;

 @Service
 public class BookService {

@@
-    //The controller sends a bookDto that is mapped to a book entity to be stored in repository
-    public Book createBook(BookDto bookDto){
+    //The controller sends a bookDto that is mapped to a book entity to be stored in repository
+    @Transactional
+    public Book createBook(BookDto bookDto){
@@
-        if(bookRepository.findBookByTitle(bookDto.title()).isPresent()){
-            throw new RuntimeException("Book already exists");
-        }
+        if (bookRepository.findBookByTitle(bookDto.title()).isPresent()) {
+            // Consider a custom exception type mapped to 409 CONFLICT at controller level
+            throw new IllegalStateException("Book already exists");
+        }

Please verify that adding @Transactional here aligns with the rest of your transaction configuration and tests.

src/main/java/org/example/entities/Book.java (1)

18-29: Align field access modifiers and normalize rating naming

The entity will work, but there are a couple of consistency issues:

  • title, genre, and Rating are package‑private while language and author are private. Making all fields private improves encapsulation and avoids accidental package-level access.
  • The Rating field and its accessors (getRating/setRating) use a capitalized field name, which is unusual in Java and may be confusing, especially since the DB column is likely rating.

Possible cleanup:

-    String title;
+    private String title;
@@
-    @Enumerated(EnumType.STRING)
-    Genre genre;
+    @Enumerated(EnumType.STRING)
+    private Genre genre;
@@
-    int Rating;
+    private int rating;
@@
-    public Book(String title, Genre genre, int Rating, Author author, Language language) {
-     this.title = title;
-     this.genre = genre;
-     this.Rating = Rating;
+    public Book(String title, Genre genre, int rating, Author author, Language language) {
+     this.title = title;
+     this.genre = genre;
+     this.rating = rating;
@@
-    public int getRating() {
-        return Rating;
+    public int getRating() {
+        return rating;
@@
-    public void setRating(int rating) {
-        Rating = rating;
+    public void setRating(int rating) {
+        this.rating = rating;
@@
-                ", Rating=" + Rating +
+                ", rating=" + rating +

(You’d also need to update any references in DTOs/repositories/tests accordingly.)

Also applies to: 65-71

src/main/java/org/example/repository/BookRepository.java (1)

13-20: Simplify base repository inheritance (optional)

The custom query methods look good, but extending both ListCrudRepository and PagingAndSortingRepository is a bit redundant. In Spring Data JPA, JpaRepository already combines CRUD operations with pagination/sorting (and, in newer versions, list-returning findAll).

If you’re using Spring Data JPA, you could simplify to:

-import org.springframework.data.repository.ListCrudRepository;
-import org.springframework.data.repository.PagingAndSortingRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
@@
-@Repository
-public interface BookRepository extends ListCrudRepository<Book,Integer>, PagingAndSortingRepository<Book,Integer> {
+@Repository
+public interface BookRepository extends JpaRepository<Book, Integer> {

Please verify against the Spring Data JPA version in this project to ensure JpaRepository provides all the methods you currently rely on.

src/main/java/org/example/controller/StoreController.java (1)

25-40: Align method names and DTO mapping with the “store” domain

getBooksByAuthor and getBooksByName are returning StoreDto on /stores/... paths, while getStores also returns StoreDto but uses a slightly different mapping style. For clarity, consider renaming the methods to getStoresByAuthor / getStoresByName (or similar) and consistently using the same mapping style (constructor reference vs lambda) across all three endpoints.

src/main/java/org/example/entities/Inventory.java (1)

69-83: Equals/hashCode can wrongly consider transient Inventory instances equal

Because inventoryId is eagerly initialized and never null, two new Inventory instances (before persistence, with bookId/storeId still null inside InventoryId) will compare equal via Objects.equals(getInventoryId(), inventory.getInventoryId()). This can break Sets/Maps or other logic that relies on equality for new entities. Consider guarding equality on non‑null key parts (e.g., require non‑null bookId and storeId inside InventoryId before returning true) or adopting a pattern similar to other entities (e.g., basing equality on a surrogate key or only when IDs are assigned).

src/main/java/org/example/entities/Language.java (1)

17-35: Tighten encapsulation for textLanguage

textLanguage is package‑private while other entity fields tend to be private with getters/setters. Making it private would align with the rest of the model and avoid accidental direct access from other classes in the package.

src/test/java/org/example/controller/BookControllerTest.java (1)

75-94: Use a 4xx status instead of 500 for “book already exists”

shouldReturnErrorMessageIfBookWithTitleAlreadyExists currently expects HttpStatus.INTERNAL_SERVER_ERROR, which semantically signals an unexpected server failure. For a known domain conflict like “Book already exists”, a 4xx status (typically 409 CONFLICT or 400 BAD_REQUEST) would be more appropriate; if you decide to adjust the controller accordingly, please also update this test to assert on the new status.

src/main/java/org/example/entities/Author.java (1)

11-44: Author mapping is straightforward; consider making fields private

Entity structure is fine for JPA, but you may want to make firstName and lastName private for consistency with other entities and encapsulation, keeping the existing getters/setters.

src/main/java/org/example/entities/Role.java (1)

11-52: Role–user mapping looks consistent; consider pluralizing collection name

The many-to-many mapping to CustomizedUser matches the roles side on CustomizedUser; functionally it’s fine, though renaming customizedUser to customizedUsers would better reflect that it’s a collection.

src/main/java/org/example/exception/CustomException.java (2)

16-20: ResponseStatusException handler is fine; minor typo in variable name

Behavior is correct; if you care about readability, consider renaming formatedMessage to formattedMessage.


36-39: Consider logging RuntimeException server-side and returning a generic message to clients

The current implementation exposes e.getMessage() directly, which can leak internal details (database errors, framework stack-trace hints, etc.). While no logging is currently configured, best practice would be to:

  • Log the full exception server-side for debugging
  • Return a generic message like "An error occurred" to the client

Other handlers in this class similarly expose exception details (e.g., handleNotFund appends e.getMessage()), so applying this pattern consistently across all handlers would be beneficial.

src/main/java/org/example/entities/InventoryId.java (1)

28-30: Rename setBookId parameter for clarity

The parameter name title in setBookId(Integer title) is misleading; it should match the field (e.g., bookId) to avoid confusion when reading or refactoring.

-    public void setBookId(Integer title) {
-        this.bookId = title;
+    public void setBookId(Integer bookId) {
+        this.bookId = bookId;
     }
src/main/java/org/example/entities/CustomizedUser.java (1)

19-33: getAuthorities logic is fine; consider a slightly tighter implementation

The current getAuthorities implementation is correct for Spring Security. If you want to trim it a bit and avoid the mutable intermediate list, you could use a stream and return an unmodifiable list:

-    public List<GrantedAuthority> getAuthorities() {
-        List<GrantedAuthority> authorities = new ArrayList<>();
-        for (Role role : roles) {
-            authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getAuthority()));
-        }
-        return authorities;
-    }
+    public List<GrantedAuthority> getAuthorities() {
+        return roles.stream()
+                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getAuthority()))
+                .toList();
+    }

Purely stylistic; existing code is acceptable.

src/main/java/org/example/controller/BookController.java (1)

64-67: Handle deletion of non‑existent books more explicitly

bookRepository.deleteById(id) will throw if the id doesn’t exist, which will surface as a 500 rather than a clear 404 or a safe 204.

If you want a clean API behavior, consider checking existence first and returning an appropriate status:

     @DeleteMapping("/books/delete/{id}")
-    public void removeBook(@PathVariable Integer id) {
-        bookRepository.deleteById(id);
-    }
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void removeBook(@PathVariable Integer id) {
+        if (!bookRepository.existsById(id)) {
+            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Book not found");
+        }
+        bookRepository.deleteById(id);
+    }
src/main/java/org/example/config/DevDataInitialize.java (1)

80-99: Dev/test seeding looks good; consider reusing loaded roles

Seeding roles and users under developer/test profiles with hard‑coded credentials is fine for this exercise project (and you’re encoding the passwords, which is nice). Based on learnings.

A small robustness/readability tweak: avoid querying roleRepository.findByAuthority(...) twice right after you’ve just seeded the roles. For example:

-        if(roleRepository.count()==0){
-            Role admin = new Role(Authority.ADMIN);
-            Role user = new Role(Authority.USER);
-            roleRepository.saveAll(List.of(admin, user));
-            roleRepository.flush();
-        }
-
-        if(userRepository.count()==0){
-            CustomizedUser admin = new CustomizedUser();
+        Role adminRole = roleRepository.findByAuthority(Authority.ADMIN);
+        Role userRole = roleRepository.findByAuthority(Authority.USER);
+
+        if(userRepository.count()==0){
+            CustomizedUser admin = new CustomizedUser();
             admin.setUserName("admin");
             admin.setPassword(passwordEncoder.encode("admin"));
-            admin.getRoles().add(roleRepository.findByAuthority(Authority.ADMIN));
+            admin.getRoles().add(adminRole);
             userRepository.save(admin);
 
             CustomizedUser visitor = new CustomizedUser();
             visitor.setUserName("visitor");
             visitor.setPassword(passwordEncoder.encode("visitor"));
-            visitor.getRoles().add(roleRepository.findByAuthority(Authority.USER));
+            visitor.getRoles().add(userRole);
             userRepository.save(visitor);

You can still keep the roleRepository.count()==0 guard, but caching the role lookups makes the flow a bit clearer.

src/main/java/org/example/entities/Store.java (1)

15-35: Tighten visibility of storeName

storeName is currently package‑private, while storeId is private. It’s probably intended to be encapsulated the same way:

-    String storeName;
+    private String storeName;

The rest of the class (getters/setters, equals/hashCode, toString) already assumes encapsulation, so this change should be safe.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8618163 and e36f7af.

📒 Files selected for processing (67)
  • books.http (1 hunks)
  • docker-compose.yml (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/org/example/App.java (0 hunks)
  • src/main/java/org/example/Authority.java (1 hunks)
  • src/main/java/org/example/Exercise2025Application.java (1 hunks)
  • src/main/java/org/example/Genre.java (1 hunks)
  • src/main/java/org/example/config/DevDataInitialize.java (1 hunks)
  • src/main/java/org/example/config/PasswordEncodeConfig.java (1 hunks)
  • src/main/java/org/example/config/SecurityConfig.java (1 hunks)
  • src/main/java/org/example/controller/BookController.java (1 hunks)
  • src/main/java/org/example/controller/BookPageController.java (1 hunks)
  • src/main/java/org/example/controller/HomePageController.java (1 hunks)
  • src/main/java/org/example/controller/SecretPageController.java (1 hunks)
  • src/main/java/org/example/controller/StoreController.java (1 hunks)
  • src/main/java/org/example/controller/UserController.java (1 hunks)
  • src/main/java/org/example/dto/Author.java (1 hunks)
  • src/main/java/org/example/dto/BookDto.java (1 hunks)
  • src/main/java/org/example/dto/Inventory.java (1 hunks)
  • src/main/java/org/example/dto/Language.java (1 hunks)
  • src/main/java/org/example/dto/StoreDto.java (1 hunks)
  • src/main/java/org/example/dto/UserDto.java (1 hunks)
  • src/main/java/org/example/entities/Author.java (1 hunks)
  • src/main/java/org/example/entities/Book.java (1 hunks)
  • src/main/java/org/example/entities/CustomizedUser.java (1 hunks)
  • src/main/java/org/example/entities/Inventory.java (1 hunks)
  • src/main/java/org/example/entities/InventoryId.java (1 hunks)
  • src/main/java/org/example/entities/Language.java (1 hunks)
  • src/main/java/org/example/entities/Role.java (1 hunks)
  • src/main/java/org/example/entities/Store.java (1 hunks)
  • src/main/java/org/example/exception/CustomException.java (1 hunks)
  • src/main/java/org/example/repository/AuthorRepository.java (1 hunks)
  • src/main/java/org/example/repository/BookRepository.java (1 hunks)
  • src/main/java/org/example/repository/InventoryRepository.java (1 hunks)
  • src/main/java/org/example/repository/LanguageRepository.java (1 hunks)
  • src/main/java/org/example/repository/RoleRepository.java (1 hunks)
  • src/main/java/org/example/repository/StoreRepository.java (1 hunks)
  • src/main/java/org/example/repository/UserRepository.java (1 hunks)
  • src/main/java/org/example/service/BookService.java (1 hunks)
  • src/main/java/org/example/service/CustomAuthenticationProvider.java (1 hunks)
  • src/main/java/org/example/service/CustomUserDetailService.java (1 hunks)
  • src/main/java/org/example/sql/store.sql (1 hunks)
  • src/main/resources/application-developer.properties (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/main/resources/db/migration/V1__author.sql (1 hunks)
  • src/main/resources/db/migration/V2__language.sql (1 hunks)
  • src/main/resources/db/migration/V3__book.sql (1 hunks)
  • src/main/resources/db/migration/V4__store.sql (1 hunks)
  • src/main/resources/db/migration/V5__inventory.sql (1 hunks)
  • src/main/resources/db/migration/V6__.sql (1 hunks)
  • src/main/resources/static/styles/index.css (1 hunks)
  • src/main/resources/static/styles/layout.css (1 hunks)
  • src/main/resources/templates/books.html (1 hunks)
  • src/main/resources/templates/index.html (1 hunks)
  • src/main/resources/templates/layout.html (1 hunks)
  • src/main/resources/templates/listOfBooks.html (1 hunks)
  • src/main/resources/templates/secret.html (1 hunks)
  • src/test/java/org/example/AppIT.java (0 hunks)
  • src/test/java/org/example/AppTest.java (0 hunks)
  • src/test/java/org/example/Exercise2025ApplicationTest.java (1 hunks)
  • src/test/java/org/example/controller/BookControllerTest.java (1 hunks)
  • src/test/java/org/example/controller/StoreControllerTest.java (1 hunks)
  • src/test/java/org/example/controller/TestSetup.java (1 hunks)
  • src/test/java/org/example/controller/UserControllerTest.java (1 hunks)
  • src/test/java/org/example/repository/StoreRepositoryTest.java (1 hunks)
  • src/test/resources/application-test.properties (1 hunks)
  • store.http (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/org/example/App.java
  • src/test/java/org/example/AppTest.java
  • src/test/java/org/example/AppIT.java
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: jennymakki
Repo: fungover/exercise2025 PR: 204
File: src/main/java/org/example/config/SecurityConfig.java:19-24
Timestamp: 2025-11-16T13:56:11.263Z
Learning: The repository fungover/exercise2025 is an exercise/learning project where hard-coded credentials and other shortcuts for ease of testing are acceptable and intentional.
📚 Learning: 2025-09-19T11:36:59.329Z
Learnt from: LolloGro
Repo: fungover/exercise2025 PR: 106
File: src/main/java/exercise4/repository/InMemoryProductRepository.java:0-0
Timestamp: 2025-09-19T11:36:59.329Z
Learning: In the InMemoryProductRepository class in src/main/java/exercise4/repository/InMemoryProductRepository.java, the updateProduct method implements a versioning system where multiple products with the same ID can exist, but identical product objects are rejected using contains() check. This allows maintaining product history while preventing duplicate identical entries.

Applied to files:

  • src/main/java/org/example/repository/InventoryRepository.java
  • src/test/java/org/example/repository/StoreRepositoryTest.java
📚 Learning: 2025-11-16T13:56:11.263Z
Learnt from: jennymakki
Repo: fungover/exercise2025 PR: 204
File: src/main/java/org/example/config/SecurityConfig.java:19-24
Timestamp: 2025-11-16T13:56:11.263Z
Learning: The repository fungover/exercise2025 is an exercise/learning project where hard-coded credentials and other shortcuts for ease of testing are acceptable and intentional.

Applied to files:

  • src/main/java/org/example/Exercise2025Application.java
  • src/main/resources/application.properties
  • src/test/java/org/example/Exercise2025ApplicationTest.java
  • pom.xml
📚 Learning: 2025-09-10T14:02:54.365Z
Learnt from: alfredbrannare
Repo: fungover/exercise2025 PR: 66
File: src/test/java/org/example/service/WarehouseTest.java:1-21
Timestamp: 2025-09-10T14:02:54.365Z
Learning: In the fungover/exercise2025 project, static mocking of LocalDateTime.now() works successfully with the existing Mockito setup without requiring mockito-inline dependency.

Applied to files:

  • src/test/java/org/example/Exercise2025ApplicationTest.java
🧬 Code graph analysis (13)
src/main/java/org/example/controller/SecretPageController.java (2)
src/main/java/org/example/controller/BookPageController.java (1)
  • Controller (9-30)
src/main/java/org/example/controller/HomePageController.java (1)
  • Controller (7-16)
src/main/java/org/example/config/SecurityConfig.java (1)
src/main/java/org/example/config/PasswordEncodeConfig.java (1)
  • Configuration (8-16)
src/main/java/org/example/entities/Inventory.java (2)
src/main/java/org/example/entities/Book.java (1)
  • Entity (9-116)
src/main/java/org/example/entities/Store.java (1)
  • Entity (8-58)
src/main/java/org/example/entities/Language.java (2)
src/main/java/org/example/entities/Author.java (1)
  • Entity (11-62)
src/main/java/org/example/entities/Book.java (1)
  • Entity (9-116)
src/main/java/org/example/controller/HomePageController.java (2)
src/main/java/org/example/controller/BookPageController.java (1)
  • Controller (9-30)
src/main/java/org/example/controller/SecretPageController.java (1)
  • Controller (8-18)
src/main/java/org/example/controller/BookPageController.java (2)
src/main/java/org/example/controller/HomePageController.java (1)
  • Controller (7-16)
src/main/java/org/example/controller/SecretPageController.java (1)
  • Controller (8-18)
src/main/java/org/example/entities/Role.java (1)
src/main/java/org/example/entities/CustomizedUser.java (1)
  • Entity (10-90)
src/main/java/org/example/config/PasswordEncodeConfig.java (1)
src/main/java/org/example/config/SecurityConfig.java (1)
  • Configuration (12-39)
src/main/java/org/example/entities/Store.java (1)
src/main/java/org/example/entities/Inventory.java (1)
  • Entity (9-85)
src/main/java/org/example/entities/CustomizedUser.java (1)
src/main/java/org/example/entities/Role.java (1)
  • Entity (11-69)
src/test/java/org/example/controller/TestSetup.java (1)
src/test/java/org/example/repository/StoreRepositoryTest.java (1)
  • Testcontainers (19-90)
src/main/java/org/example/entities/Book.java (2)
src/main/java/org/example/entities/Author.java (1)
  • Entity (11-62)
src/main/java/org/example/entities/Language.java (1)
  • Entity (11-53)
src/main/java/org/example/entities/Author.java (1)
src/main/java/org/example/entities/Book.java (1)
  • Entity (9-116)
🔇 Additional comments (38)
src/main/java/org/example/Exercise2025Application.java (1)

1-13: LGTM!

This is a standard Spring Boot application entry point that correctly uses the @SpringBootApplication annotation and delegates to SpringApplication.run() in the main method. The implementation follows Spring Boot conventions and best practices.

src/main/resources/static/styles/layout.css (1)

1-8: Base layout styling looks good.

The centered, max-width body and .header styling align well with layout.html and provide a simple, consistent frame for the pages.

src/main/resources/application-developer.properties (1)

1-3: Dev-only SQL logging and compose settings are appropriate.

Scoping show-sql and formatted SQL to the developer profile plus using start_only for Docker Compose is a reasonable developer experience choice for this exercise.

src/main/resources/templates/layout.html (1)

1-23: Header/footer fragments and navigation look consistent with the rest of the app.

The layout template cleanly defines reusable header/footer fragments and the nav targets (/, /books, /secret) match the controllers, so composition via th:replace should work smoothly.

src/main/java/org/example/controller/HomePageController.java (1)

1-16: Home page controller correctly backs the index view.

The root GET mapping, model attribute name (welcome), and returned view name (index) are all aligned with the template, so this should render the welcome page without issues.

books.http (1)

1-19: HTTP collection matches the book API and is useful for manual testing.

The login flow and the three book endpoints mirror the controller design and give a nice quick way to exercise the API during development. Using admin/admin here is acceptable for this learning repository; just something to revisit if this ever moves beyond an exercise context. Based on learnings

store.http (1)

1-19: Store HTTP examples align with the store API endpoints.

The sample requests for all stores, by author, and by name match the REST contract and provide a handy way to exercise the store API alongside login.

pom.xml (3)

32-118: Well-structured dependency configuration.

The dependency set is appropriate for a Spring Boot book store application with JPA, security, and Thymeleaf views. Good practices observed:

  • Testcontainers for integration testing
  • Docker Compose support for reproducible database setup
  • Proper scoping (runtime, test, provided)

30-30: Invalid Java version specified.

Java 25 does not exist yet. The current latest Java version is 23 (released September 2024), with Java 21 being the latest LTS release. This will cause build failures.

Apply this diff to use a valid Java version:

-        <java.version>25</java.version>
+        <java.version>21</java.version>

Likely an incorrect or invalid review comment.


5-10: Spring Boot 3.5.7 is the latest Spring Boot 3.x release (released October 23, 2025). The pom.xml declaration is valid and uses a current stable version.

src/main/java/org/example/Authority.java (1)

1-5: LGTM!

Clean enum definition for authority levels. The two-level authority model (USER and ADMIN) is appropriate for the application's access control requirements.

src/test/resources/application-test.properties (1)

1-2: LGTM!

Enabling SQL logging and formatting in the test profile is helpful for debugging and verifying database interactions during integration tests.

src/main/resources/application.properties (2)

3-4: LGTM! Development-friendly caching configuration.

Disabling caching for web resources and Thymeleaf templates is appropriate for development environments, enabling immediate feedback during development.


2-2: Virtual threads enabled—requirements verified in build configuration.

Virtual threads require Java 21+, and this project specifies Java 25 in pom.xml, meeting the requirement. Spring Boot 3.5.7 (configured via spring-boot-starter-parent) supports virtual threads and exceeds the 3.2+ requirement. The Java and Spring Boot versions are documented in the build configuration, satisfying the documentation requirement.

src/test/java/org/example/controller/UserControllerTest.java (1)

16-18: LGTM! Clean authentication helper.

The loggedInUser() helper method provides a clean way to add Basic Auth credentials to test requests. The hard-coded credentials are acceptable for this exercise project.

Based on learnings.

src/main/java/org/example/dto/StoreDto.java (1)

5-5: LGTM! Clean and idiomatic DTO record.

The record uses modern Java features effectively with a clear validation message. The @NotEmpty constraint ensures data integrity at the API boundary.

src/main/java/org/example/dto/Language.java (1)

5-5: LGTM! Consistent DTO pattern.

The Language record follows the same clean pattern as other DTOs in the project with clear validation constraints.

src/test/java/org/example/Exercise2025ApplicationTest.java (1)

6-9: LGTM! Standard Spring Boot smoke test.

This context load test is a common pattern in Spring Boot applications. It provides valuable coverage by verifying that the application context loads successfully with all beans wired correctly. The empty test body is intentional—the test passes if the context loads without errors.

docker-compose.yml (1)

3-3: No issues found. MySQL 9.5.0 is a valid, available image on Docker Hub.

The verification confirms that mysql:9.5.0 exists and is actively available on Docker Hub, including variants like 9.5.0-oracle and 9.5.0-oraclelinux9. The image will pull without issues.

src/main/java/org/example/repository/InventoryRepository.java (1)

7-8: LGTM!

Standard JPA repository interface with appropriate generic types for the entity and composite key.

src/main/java/org/example/config/PasswordEncodeConfig.java (1)

8-16: LGTM!

Standard Spring Security configuration using BCryptPasswordEncoder, which is appropriate for password hashing in production and learning scenarios.

src/main/resources/db/migration/V1__author.sql (1)

1-7: LGTM!

The migration correctly creates the author table with appropriate data types and constraints. The nullable name fields provide flexibility for data entry.

src/main/java/org/example/repository/UserRepository.java (1)

9-12: LGTM!

Standard Spring Data JPA repository with a properly named finder method that follows naming conventions. The use of Optional as the return type is a good practice for handling potentially missing users.

src/main/java/org/example/repository/LanguageRepository.java (1)

9-12: LGTM!

The repository interface is well-structured with proper Spring Data conventions and null-safe return type.

src/main/java/org/example/repository/AuthorRepository.java (1)

9-13: LGTM!

Clean repository interface following Spring Data best practices with null-safe return type.

src/main/java/org/example/config/SecurityConfig.java (1)

22-38: LGTM!

The security configuration is well-structured with appropriate authorization rules. The disabled CSRF and simplified authentication setup are acceptable for this learning project.

Based on learnings

src/test/java/org/example/controller/StoreControllerTest.java (1)

17-25: LGTM!

The test setup and HTTP status validation are well-structured. The hard-coded credentials are acceptable for this exercise project.

Based on learnings

src/main/resources/templates/books.html (1)

6-6: No action required — unpkg.dev is valid.

Both unpkg.com and unpkg.dev are valid endpoints for unpkg, and the htmx docs list unpkg as a CDN option. The code is correct as written.

src/main/java/org/example/service/CustomUserDetailService.java (1)

23-35: UserDetails mapping looks solid

The loadUserByUsername implementation is straightforward: it fails fast when the user is missing and correctly maps roles to "ROLE_"-prefixed SimpleGrantedAuthority instances collected into a Set. I don’t see any functional issues here.

src/main/java/org/example/controller/BookPageController.java (1)

9-28: Controller wiring and view mapping look correct

The /books and /books/list mappings are simple and consistent with the described templates (page + fragment), and using bookRepository.findAll() directly here is fine for this read‑only listing use case.

src/test/java/org/example/controller/BookControllerTest.java (1)

23-53: Solid end‑to‑end coverage for read paths

The tests for /api/books, /api/books/id/{id}, and /api/books/genre/{genre} nicely cover status codes, payload shape, and specific field values, which should catch many regressions in the read‑only API behavior. Based on learnings.

src/main/java/org/example/service/CustomAuthenticationProvider.java (2)

15-21: Constructor-based injection and dependencies look good

Using final fields with constructor injection for CustomUserDetailService and PasswordEncoder is clean and testable; no issues here.


37-40: supports() implementation is appropriate

Restricting support to UsernamePasswordAuthenticationToken is standard and matches the expected authentication type.

src/main/java/org/example/entities/Author.java (1)

46-60: equals/hashCode pattern is consistent with Hibernate best practices

Using the proxy-aware, id-based equals/hashCode pattern matches the approach in Book and should behave correctly with Hibernate.

src/main/java/org/example/controller/UserController.java (1)

18-26: Controller wiring is clean

Constructor injection of UserRepository, RoleRepository, and PasswordEncoder is idiomatic and keeps the controller easy to test.

src/main/java/org/example/entities/Role.java (1)

54-68: Equality implementation aligns with id-based entity identity

Proxy-aware equals/hashCode using roleId is consistent with the rest of the model and should behave correctly with Hibernate.

src/main/java/org/example/exception/CustomException.java (1)

27-34: Validation handler produces clear messages

Collecting field errors into "field: message" pairs joined by commas is reasonable and user-friendly for an exercise project.

src/main/java/org/example/controller/BookController.java (1)

28-62: Disregard the Rating() accessor concern—it is correct and will compile

The review comment incorrectly flagged bookDto.Rating() as non-idiomatic or non-compiling. Verification confirms that BookDto is a Java record with int Rating as a component, and records auto-generate accessors matching the component name exactly. Therefore, bookDto.Rating() is the correct accessor and will compile successfully. The suggested diff that changes it to bookDto.rating() would actually cause a compilation error.

The only valid point is the API response type consistency between endpoints returning BookDto vs. Book entities, which is a design consideration rather than a bug.

Likely an incorrect or invalid review comment.

Comment thread src/main/java/org/example/controller/SecretPageController.java
Comment thread src/main/java/org/example/controller/UserController.java
Comment thread src/main/java/org/example/controller/UserController.java Outdated
Comment thread src/main/java/org/example/dto/Author.java Outdated
Comment thread src/main/java/org/example/dto/BookDto.java Outdated
Comment thread src/main/resources/templates/listOfBooks.html
Comment thread src/main/resources/templates/secret.html Outdated
Comment thread src/test/java/org/example/controller/BookControllerTest.java
Comment thread src/test/java/org/example/controller/TestSetup.java
Comment thread src/test/java/org/example/repository/StoreRepositoryTest.java
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/main/java/org/example/controller/SecretPageController.java (1)

13-15: Typo fixed - LGTM!

The typo identified in the previous review ("know" → "now") has been corrected. The controller follows standard Spring MVC patterns and is consistent with other controllers in the codebase.

src/main/java/org/example/service/BookService.java (1)

35-40: New Author instances miss lastName, breaking lookups and causing duplicates

In the orElseGet branch, you only set firstName on the new Author, leaving lastName null. Since findByFirstNameAndLastName uses both names, subsequent lookups for the same person will never match this row, and you will repeatedly insert duplicate authors.

Suggested fix:

-        Author author = authorRepository.findByFirstNameAndLastName(bookDto.author().getFirstName(), bookDto.author().getLastName())
-                .orElseGet(() ->{
-                            Author newAuthor = new Author();
-                            newAuthor.setFirstName(bookDto.author().getFirstName());
-                            return authorRepository.save(newAuthor);
-                        });
+        Author author = authorRepository
+                .findByFirstNameAndLastName(
+                        bookDto.author().getFirstName(),
+                        bookDto.author().getLastName())
+                .orElseGet(() -> {
+                    Author newAuthor = new Author();
+                    newAuthor.setFirstName(bookDto.author().getFirstName());
+                    newAuthor.setLastName(bookDto.author().getLastName());
+                    return authorRepository.save(newAuthor);
+                });
src/test/java/org/example/controller/BookControllerTest.java (1)

31-35: Tests depend on shared DB state and fixed IDs/counts; ensure isolation between tests

Several tests assume a specific initial database state and stable IDs:

  • Line 33–35: shouldReturnAllBooks expects exactly 5 books.
  • Line 39–42: shouldFindBookById assumes book with id 1 and a specific title.
  • Line 53–55: shouldReturnListOfBooksOfGivenGenre expects exactly 4 FICTION books.
  • Line 123–124 & 130–131: shouldUpdateBook and shouldDeleteBook operate on book with id 3.

If the underlying DB is not reset between tests (and the Testcontainers MySQL instance is reused), create/update/delete tests will change the state and make these assertions order-dependent and flaky across runs or when adding new tests.

Please confirm that your TestSetup (or this test class) now enforces isolation, e.g.:

  • Marking the test class with @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD), or
  • Using @Transactional tests with rollback per method, or
  • Manually cleaning relevant tables in @BeforeEach/@AfterEach.

If such a mechanism is already in place after the previous review, this comment can be ignored.

To cross-check in this repo, you can run:

#!/bin/bash
rg -n "@DirtiesContext|@Transactional|@BeforeEach|@AfterEach" src/test/java

Also applies to: 37-55, 114-132

🧹 Nitpick comments (3)
src/main/java/org/example/service/UserService.java (3)

15-17: Consider reordering fields to match constructor parameter order.

The field declaration order (userRepository, roleRepository, passwordEncoder) differs from the constructor parameter order (userRepository, passwordEncoder, roleRepository). While not a functional issue, aligning these improves readability.

Apply this diff to align field order with constructor:

 private final UserRepository  userRepository;
-private final RoleRepository roleRepository;
 private final PasswordEncoder passwordEncoder;
+private final RoleRepository roleRepository;

29-31: Consider consistent exception types across similar services.

This method throws IllegalArgumentException for "already exists" scenarios, while BookService.createBook() throws RuntimeException for the same pattern. While IllegalArgumentException is more semantically appropriate for invalid user input, consistency across similar service methods improves maintainability.

You may want to update BookService.createBook() to also use IllegalArgumentException:

// In BookService.java line 18
if(bookRepository.findBookByTitle(bookDto.title()).isPresent()){
    throw new IllegalArgumentException("Book already exists");
}

36-40: Consider using IllegalStateException for system configuration errors.

The missing USER role indicates a system configuration problem (e.g., missing Flyway migration or data seeding), not invalid user input. IllegalStateException would better communicate that the system is not in an expected state, while IllegalArgumentException typically signals invalid parameters.

Apply this diff to use a more appropriate exception type:

 if(userRole == null){
-    throw new IllegalArgumentException("Role of user was not found");
+    throw new IllegalStateException("USER role not found - system configuration error");
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e36f7af and 494d3b7.

📒 Files selected for processing (9)
  • src/main/java/org/example/controller/SecretPageController.java (1 hunks)
  • src/main/java/org/example/controller/UserController.java (1 hunks)
  • src/main/java/org/example/dto/Author.java (1 hunks)
  • src/main/java/org/example/dto/BookDto.java (1 hunks)
  • src/main/java/org/example/service/BookService.java (1 hunks)
  • src/main/java/org/example/service/UserService.java (1 hunks)
  • src/main/resources/templates/secret.html (1 hunks)
  • src/test/java/org/example/controller/BookControllerTest.java (1 hunks)
  • src/test/java/org/example/controller/TestSetup.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/resources/templates/secret.html
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/org/example/dto/Author.java
  • src/test/java/org/example/controller/TestSetup.java
  • src/main/java/org/example/dto/BookDto.java
  • src/main/java/org/example/controller/UserController.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: jennymakki
Repo: fungover/exercise2025 PR: 204
File: src/main/java/org/example/config/SecurityConfig.java:19-24
Timestamp: 2025-11-16T13:56:11.263Z
Learning: The repository fungover/exercise2025 is an exercise/learning project where hard-coded credentials and other shortcuts for ease of testing are acceptable and intentional.
🧬 Code graph analysis (3)
src/main/java/org/example/service/BookService.java (1)
src/main/java/org/example/service/UserService.java (1)
  • Service (12-46)
src/main/java/org/example/service/UserService.java (1)
src/main/java/org/example/service/BookService.java (1)
  • Service (13-59)
src/main/java/org/example/controller/SecretPageController.java (2)
src/main/java/org/example/controller/HomePageController.java (1)
  • Controller (7-16)
src/main/java/org/example/controller/BookPageController.java (1)
  • Controller (9-30)
🔇 Additional comments (3)
src/main/java/org/example/controller/SecretPageController.java (1)

13-16: Security configuration is properly configured.

The Spring Security configuration in src/main/java/org/example/config/SecurityConfig.java at line 30 correctly restricts the /secret/** path to users with the ADMIN role via .requestMatchers("/secret/**").hasRole("ADMIN"). The controller message accurately reflects this protection, as the endpoint will only be reached by authenticated admin users.

src/main/java/org/example/service/UserService.java (1)

41-41: No issue identified – roles collection is properly initialized.

The roles field in CustomizedUser is initialized at the field declaration level (private Set<Role> roles = new HashSet<>();), so getRoles() will never return null. The call to getRoles().add(userRole) on line 41 is safe and will not throw a NullPointerException.

Likely an incorrect or invalid review comment.

src/main/java/org/example/service/BookService.java (1)

50-52: Double-check the BookDto accessor name for rating

You call bookDto.Rating() with a capital “R”. If BookDto is a record or a typical DTO with a rating field, the accessor is usually rating() (lowercase) or getRating(). As written, this may not compile or may be calling an unintended method.

Please verify the BookDto API and, if needed, adjust this line accordingly.

Comment thread src/main/java/org/example/service/UserService.java
Comment thread src/test/java/org/example/controller/BookControllerTest.java
…d handling of exception added transactional if user isn't created properly
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/org/example/exception/CustomException.java (1)

22-25: Rename handleNotFund to handleNotFound for clarity

Minor typo: the method name handleNotFund is misleading. Renaming it to handleNotFound will better reflect its purpose and improve readability.

🧹 Nitpick comments (7)
src/main/java/org/example/entities/Book.java (2)

25-29: Consider making ManyToOne associations explicitly LAZY

Right now both language and author use default @ManyToOne fetch (EAGER). For larger datasets this can easily cause N+1 issues when listing books. Consider:

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)

for both fields to align with how Inventory uses lazy associations and to keep loading under control.


33-39: Normalize constructor parameter naming

The constructor argument int Rating with a capital “R” is easy to misread as a type. Renaming it to rating keeps it consistent with the field and the rest of the code:

public Book(String title, Genre genre, int rating, Author author, Language language) {
    this.title = title;
    this.genre = genre;
    this.rating = rating;
    this.author = author;
    this.language = language;
}

Purely cosmetic, but improves readability.

src/main/java/org/example/exception/DuplicateBooks.java (1)

3-7: Optional: align exception naming with common conventions

The class name DuplicateBooks works but is a bit unclear (plural noun, no Exception suffix). Consider renaming to something like DuplicateBookException to make its purpose obvious at call sites and in logs.

src/test/java/org/example/controller/BookControllerTest.java (1)

56-122: Tests post Book entities to endpoints that accept BookDto

All the create/update tests build Book entities and send them to controller methods that accept BookDto. This currently works because the JSON shape matches, but it tightly couples tests to the internal entity structure.

For clearer layering and less fragile tests, consider constructing and sending BookDto (or raw JSON payloads) instead. That way changes in the entity (e.g., extra fields, annotations) don’t accidentally break the API tests.

src/main/java/org/example/exception/CustomException.java (1)

41-44: Tighten handler signature to DuplicateBooks instead of Exception

Since this method is specifically annotated with @ExceptionHandler(DuplicateBooks.class), using DuplicateBooks as the parameter type is clearer than Exception and avoids any temptation to reuse it for unrelated exceptions:

@ExceptionHandler(DuplicateBooks.class)
public ResponseEntity<String> handleDuplicateBooks(DuplicateBooks e) {
    return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
}

This keeps the mapping explicit alongside the generic RuntimeException handler.

src/main/java/org/example/controller/BookController.java (2)

28-62: Unify API contract: avoid mixing DTOs and entities in responses

Right now /books and /books/genre/{genre} return List<BookDto>, while /books/id/{id}, POST /books/add, and PUT /books/update/{id} return Book entities. Functionally this works, but it makes the REST API harder to reason about and evolve.

Consider standardizing on a single representation (e.g., always BookDto for responses) and routing all mutations through BookService instead of using BookRepository directly in the controller. That keeps the boundary clean and reduces accidental exposure of JPA internals.


64-67: Consider handling deletes of non‑existent IDs as 404

deleteById(id) will throw if the book doesn’t exist, which will currently be turned into a 500 via the generic RuntimeException handler. For a REST API it’s usually more expressive to return 404 when deleting a non‑existent resource. You could pre-check with existsById, catch the specific exception, or map it via a dedicated @ExceptionHandler.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 494d3b7 and d8dc41c.

📒 Files selected for processing (8)
  • src/main/java/org/example/controller/BookController.java (1 hunks)
  • src/main/java/org/example/dto/BookDto.java (1 hunks)
  • src/main/java/org/example/entities/Book.java (1 hunks)
  • src/main/java/org/example/exception/CustomException.java (1 hunks)
  • src/main/java/org/example/exception/DuplicateBooks.java (1 hunks)
  • src/main/java/org/example/service/BookService.java (1 hunks)
  • src/main/java/org/example/service/UserService.java (1 hunks)
  • src/test/java/org/example/controller/BookControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/example/service/UserService.java
  • src/main/java/org/example/dto/BookDto.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jennymakki
Repo: fungover/exercise2025 PR: 204
File: src/main/java/org/example/config/SecurityConfig.java:19-24
Timestamp: 2025-11-16T13:56:11.263Z
Learning: The repository fungover/exercise2025 is an exercise/learning project where hard-coded credentials and other shortcuts for ease of testing are acceptable and intentional.
📚 Learning: 2025-09-19T11:48:06.449Z
Learnt from: LolloGro
Repo: fungover/exercise2025 PR: 106
File: src/test/java/exercise4/service/ManagingProductTest.java:41-44
Timestamp: 2025-09-19T11:48:06.449Z
Learning: In the ManagingProductTest class in src/test/java/exercise4/service/ManagingProductTest.java, the throwsIllegalArgumentExceptionIfProductWithUniqIdAlreadyExist test is specifically designed to verify that two identical products cannot be created, not just products with the same ID but different properties.

Applied to files:

  • src/test/java/org/example/controller/BookControllerTest.java
🧬 Code graph analysis (2)
src/main/java/org/example/service/BookService.java (1)
src/main/java/org/example/exception/DuplicateBooks.java (1)
  • DuplicateBooks (3-7)
src/main/java/org/example/entities/Book.java (3)
src/main/java/org/example/entities/Author.java (1)
  • Entity (11-62)
src/main/java/org/example/entities/Inventory.java (1)
  • Entity (9-85)
src/main/java/org/example/entities/Language.java (1)
  • Entity (11-53)
🔇 Additional comments (1)
src/main/java/org/example/service/BookService.java (1)

28-59: createBook flow looks coherent and consistent

The transactional flow here is solid: you guard on title uniqueness, reuse or create Author and Language via orElseGet, then persist a properly populated Book. Also good to see both firstName and lastName set when creating a new Author.

@Deansie
Copy link
Copy Markdown

Deansie commented Nov 22, 2025

I think your implementation looks very structured and relatively straight forward to wrap ones head around. On that note, I'd recommend adding some instructions on how to setup your application to run locally.

Maybe a single line command for Linux and WSL or Powershell-equivalent for straight forward hands on testing?

  • cd /your/project/directory && docker compose up & sleep 20 && mvn spring-boot:run -Dspring-boot.run.profiles=developer

  • Run docker compose down afterward to stop and clean up containers if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants