Skip to content

kai-ericson/Exercise 8#214

Open
kai-ericson wants to merge 5 commits intomainfrom
kai-ericson/exercise8
Open

kai-ericson/Exercise 8#214
kai-ericson wants to merge 5 commits intomainfrom
kai-ericson/exercise8

Conversation

@kai-ericson
Copy link
Copy Markdown

@kai-ericson kai-ericson commented Nov 20, 2025

This pull request contains code for a simple RESTful API like the one in exercise 6, but this time made with Spring Boot.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added REST API endpoints for pet management (adopt, view, delete) and pet interactions (feed, play)
    • Implemented API key-based authentication for secure endpoint access
    • Configured Docker Compose for local database environment setup
  • Chores

    • Migrated project to Spring Boot framework with updated dependencies
    • Reorganized project configuration and build setup

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 20, 2025

Walkthrough

This pull request migrates a template Java project to a Spring Boot-based pet management application. It introduces REST API endpoints for pet CRUD operations, security configuration with API key authentication, JPA persistence with MySQL backend, database migration, development data initialization, and comprehensive test coverage. The project structure shifts from a minimal template to a layered Spring Boot application.

Changes

Cohort / File(s) Summary
Build & Infrastructure
docker-compose.yml, pom.xml
Adds Docker Compose configuration for MySQL 9.5.0 service. Migrates pom.xml to Spring Boot parent 3.5.6, updates project coordinates to exercise2025, replaces legacy test dependencies with Spring Boot starters (web, test, thymeleaf), adds JPA, Flyway, MySQL driver, Docker Compose support, Security, and Validation starters. Removes multi-plugin build configuration in favor of spring-boot-maven-plugin.
Application Configuration
src/main/resources/application.properties, src/main/resources/application-dev.properties
Sets application name to exercise2025, enables virtual threads. Development profile configures Docker Compose lifecycle management and Hibernate DDL auto-update.
Application Entry Point
src/main/java/org/example/App.java (deleted), src/main/java/org/example/Exercise2025Application.java
Removes legacy App.java entry point. Introduces Exercise2025Application with @SpringBootApplication annotation and SpringApplication.run() bootstrap method.
Domain Model & Transfer Objects
src/main/java/org/example/enteties/Pet.java, src/main/java/org/example/PetDTO.java
Adds Pet JPA entity with auto-generated id, name, species, hunger, happiness fields; includes constructors, getters, setters, and Hibernate proxy-aware equals/hashCode. Adds PetDTO record with accessor methods for REST API serialization.
Data Access & Migration
src/main/java/org/example/PetRepository.java, src/main/resources/db/migration/V1__.sql
Introduces PetRepository extending ListCrudRepository with findPetById and deletePetById query methods. Adds Flyway migration V1 creating pet table with id (auto-increment), name, species, hunger, happiness columns.
Business Logic
src/main/java/org/example/PetService.java
Adds @Service component implementing feedPet (decreases hunger by 10, minimum 0) and playWithPet (increases happiness by 10, capped at 100) operations on PetDTO with logging.
REST API & Security
src/main/java/org/example/PetController.java, src/main/java/org/example/config/SecurityConfig.java, src/main/java/org/example/filters/ApiKeyFilter.java
Introduces PetController with CRUD endpoints (GET/POST/PUT/DELETE) at /api/pets/** and domain action endpoints (feed, play). Adds SecurityConfig bean configuring CSRF exemption, endpoint authorization, and ApiKeyFilter integration. Implements ApiKeyFilter validating X-API-KEY header against "secret"; returns 401 if missing or invalid.
Data Initialization
src/main/java/org/example/config/DevDataInitializer.java
Adds @Component ApplicationRunner active under "dev" profile; on startup initializes six Pet entities if repository is empty or force-init flag is present; saves via PetRepository.saveAll().
Tests
src/test/java/org/example/Exercise2025ApplicationTest.java, src/test/java/org/example/PetControllerTest.java, src/test/java/org/example/AppIT.java (deleted), src/test/java/org/example/AppTest.java (deleted)
Removes legacy AppIT.java and AppTest.java test stubs. Adds Exercise2025ApplicationTest verifying Spring context load. Adds PetControllerTest using @WebMvcTest with MockMvc, mocking PetRepository and PetService; tests authorized/unauthorized access, CRUD operations, and domain action endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ApiKeyFilter
    participant SecurityConfig
    participant PetController
    participant PetService
    participant PetRepository
    participant Database

    Client->>ApiKeyFilter: Request + X-API-KEY header
    alt Valid API Key
        ApiKeyFilter->>SecurityConfig: Create SecurityContext with ROLE_USER
        ApiKeyFilter->>PetController: Continue filter chain
        rect rgb(200, 220, 255)
            note over PetController: Route to endpoint
            alt GET /api/pets/{id}
                PetController->>PetRepository: findPetById(id)
                PetRepository->>Database: SELECT * FROM pet WHERE id=?
                Database-->>PetRepository: Pet entity
                PetRepository-->>PetController: Optional<Pet>
                PetController-->>Client: 200 OK + PetDTO
            else PUT /api/pets/{id}/feed
                PetController->>PetService: feedPet(PetDTO)
                PetService-->>PetController: Updated PetDTO (hunger-10)
                PetController->>PetRepository: save(Pet)
                PetRepository->>Database: UPDATE pet SET hunger=?
                PetController-->>Client: 200 OK + Updated PetDTO
            end
        end
    else Invalid/Missing API Key
        ApiKeyFilter-->>Client: 401 Unauthorized
    end
Loading
sequenceDiagram
    participant Spring
    participant DevDataInitializer
    participant PetRepository
    participant Database

    Spring->>DevDataInitializer: Startup (dev profile)
    rect rgb(240, 200, 200)
        note over DevDataInitializer: Initialization Phase
        DevDataInitializer->>PetRepository: Check if empty or force-init flag
        PetRepository->>Database: SELECT COUNT(*) FROM pet
        alt Repository empty or force-init
            DevDataInitializer->>DevDataInitializer: Create 6 Pet entities
            DevDataInitializer->>PetRepository: saveAll(pets)
            PetRepository->>Database: INSERT INTO pet ...
            Database-->>PetRepository: Success
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Security filter logic: Verify ApiKeyFilter correctly creates SecurityContext, handles missing/invalid keys, and integrates properly with Spring Security chain.
  • REST controller design: Review all endpoint implementations for correct HTTP method routing, status code responses, DTO mapping, and error handling.
  • Entity and proxy handling: Examine Pet.java equals/hashCode implementations for correct Hibernate proxy resolution.
  • Dependency injection: Confirm constructor injection in PetController, SecurityConfig, and DevDataInitializer is properly wired.
  • Test coverage: Validate PetControllerTest scenarios cover both success and authorization failure paths; verify mock stubs align with controller logic.
  • Database migration: Ensure V1__.sql schema matches Pet entity field definitions and constraints.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kai-ericson/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 469569d and 30145e5.

📒 Files selected for processing (3)
  • src/main/java/org/example/PetController.java (1 hunks)
  • src/main/java/org/example/PetService.java (1 hunks)
  • src/test/java/org/example/PetControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/org/example/PetService.java
  • src/test/java/org/example/PetControllerTest.java
  • src/main/java/org/example/PetController.java

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.

@kai-ericson kai-ericson linked an issue Nov 20, 2025 that may be closed by this pull request
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: 7

🧹 Nitpick comments (19)
src/main/resources/application-dev.properties (1)

2-2: Consider removing ddl-auto=update to avoid conflicts with Flyway.

Since the project uses Flyway for migrations (V1__.sql), having spring.jpa.hibernate.ddl-auto=update enabled can create conflicts where both Hibernate and Flyway attempt to manage the schema. For consistency and better control, rely solely on Flyway migrations.

Apply this diff:

 spring.docker.compose.lifecycle-management=start_only
-spring.jpa.hibernate.ddl-auto=update
+spring.jpa.hibernate.ddl-auto=validate
src/main/resources/db/migration/V1__.sql (2)

1-9: Flyway migration filename should include a description.

The migration file is named V1__.sql with a double underscore. Flyway convention recommends including a description: V1__Create_pet_table.sql. While the current name works, adding a description improves maintainability.

Consider renaming to:

V1__Create_pet_table.sql

4-5: Consider making name and species NOT NULL.

Having name and species as nullable might lead to incomplete pet data. Unless there's a specific reason for pets to have no name or species, consider adding NOT NULL constraints.

-    name      VARCHAR(255) NULL,
-    species   VARCHAR(255) NULL,
+    name      VARCHAR(255) NOT NULL,
+    species   VARCHAR(255) NOT NULL,
src/main/java/org/example/PetDTO.java (1)

4-18: Remove redundant explicit getter methods.

Java records automatically generate getter methods for all components. The explicit getHunger(), getHappiness(), getName(), getSpecies(), and getId() methods are redundant and can be removed.

Apply this diff:

 public record PetDTO(String name, String species, int hunger, int happiness, Integer id) {
-    public int getHunger() {
-        return hunger;
-    }
-    public int getHappiness() {
-        return happiness;
-    }
-    public String getName() {
-        return name;
-    }
-    public String getSpecies() {
-        return species;
-    }
-    public Integer getId() {
-        return id;
-    }
 }

Records already provide accessor methods: petDTO.name(), petDTO.species(), petDTO.hunger(), petDTO.happiness(), and petDTO.id().

src/main/java/org/example/filters/ApiKeyFilter.java (2)

24-35: Consider skipping API key check for public endpoints.

The ApiKeyFilter currently applies to all requests. If there are any public endpoints (e.g., health checks, public documentation, or unauthenticated endpoints), you should override shouldNotFilter() to exclude them.

Example:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
    String path = request.getServletPath();
    return path.startsWith("/public") || path.equals("/health");
}

32-34: Consider adding a response body to the 401 error.

The 401 response currently has no body or Content-Type. Adding a descriptive error message improves the API user experience.

 } else {
     response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    response.setContentType("application/json");
+    response.getWriter().write("{\"error\":\"Invalid or missing API key\"}");
 }
src/main/java/org/example/PetService.java (2)

8-11: Add boundary validation for hunger values.

The feedPet method can produce negative hunger values if called when hunger is below 10. Consider adding validation to ensure hunger stays within reasonable bounds (e.g., 0-100).

 public static PetDTO feedPet(PetDTO pet) {
-    var hunger = pet.getHunger()-10;
+    var hunger = Math.max(0, pet.getHunger() - 10);
     return new PetDTO(pet.getName(), pet.getSpecies(), hunger, pet.getHappiness(), pet.getId());
 }

12-15: Add boundary validation for happiness values.

The playWithPet method allows happiness to grow unbounded. Consider capping happiness at a maximum value (e.g., 100).

 public static PetDTO playWithPet(PetDTO pet) {
-    var happiness = pet.getHappiness()+10;
+    var happiness = Math.min(100, pet.getHappiness() + 10);
     return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), happiness, pet.getId());
 }
src/main/java/org/example/Exercise2025Application.java (1)

3-8: Remove unused imports.

The imports for Pet, ApplicationRunner, and List are not used in this file and should be removed.

Apply this diff:

 package org.example;
 
-import org.example.enteties.Pet;
-import org.springframework.boot.ApplicationRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-import java.util.List;
src/main/java/org/example/PetRepository.java (3)

5-5: Remove unused import.

The ResponseEntity import is not used in this interface.

Apply this diff:

 import org.example.enteties.Pet;
 import org.springframework.data.repository.ListCrudRepository;
-import org.springframework.http.ResponseEntity;

11-11: Remove commented-out code.

The commented-out savePet method should be removed. ListCrudRepository already provides a save(S entity) method.

Apply this diff:

 public interface PetRepository extends ListCrudRepository<Pet, Integer> {
     Optional<Pet> findPetById(Integer id);
-    //void savePet(Pet pet);
     void deletePetById(Integer id);

9-12: Consider using inherited methods instead of custom query methods.

ListCrudRepository already provides findById(ID id) returning Optional<T> and deleteById(ID id). Your custom methods findPetById and deletePetById are functionally equivalent. Unless there's a specific reason for these custom methods, consider using the inherited ones directly in your controller.

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

12-12: Remove unused import.

The WithUserDetails import is not used in this test class.

Apply this diff:

 import org.springframework.security.test.context.support.WithMockUser;
-import org.springframework.security.test.context.support.WithUserDetails;
 import org.springframework.test.context.bean.override.mockito.MockitoBean;
src/main/java/org/example/config/SecurityConfig.java (2)

36-36: Remove commented-out code.

The commented line in the middle of the method should be removed to improve readability.

Apply this diff:

                 )
                 .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
-        //http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll())
                 .csrf(AbstractHttpConfigurer::disable);

40-46: Remove commented-out alternative implementation.

This large block of commented code should be removed. If you need to preserve it for reference, consider documenting the approach in a commit message or ADR instead.

Apply this diff:

         return http.build();
     }
-   /* public SecurityFilterChain securityFilterChain(HttpSecurity http, ApiKeyFilter apiKeyFilter) throws Exception {
-        http
-                .securityMatcher("/api/**")
-                .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
-                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
-        return http.build();
-    }*/
     @Bean
src/main/java/org/example/config/DevDataInitializer.java (1)

27-33: Standardize species capitalization.

The species values have inconsistent capitalization: "Rat" (lines 27-29), "rat" (lines 30-31), and "dog" (line 32). Consider standardizing to a consistent format.

Apply this diff:

             var pet1 = new Pet("T-rex", "Rat", 50, 80);
             var pet2 = new Pet("Chetney", "Rat", 40, 90);
             var pet3 = new Pet("Håkan Bråkan", "Rat", 80, 70);
-            var pet4 = new Pet("Madstab", "rat", 70,70);
-            var pet5 = new Pet("Pimpek", "rat", 70,60);
-            var pet6 = new Pet("Rolf", "dog", 80, 80);
+            var pet4 = new Pet("Madstab", "Rat", 70,70);
+            var pet5 = new Pet("Pimpek", "Rat", 70,60);
+            var pet6 = new Pet("Rolf", "Dog", 80, 80);
src/main/java/org/example/PetController.java (3)

28-34: Check security coverage on read vs mutate endpoints

Only getAllPets, adoptPet, and deletePet are protected with @PreAuthorize("hasRole('API')"). getPet, feedPet, and playWithPet are currently callable without that role.

If the intent is that all API operations (especially state-changing ones like feed/play) require the API role, you may want to add @PreAuthorize("hasRole('API')") to those methods as well for consistency.

If they are intentionally public, consider documenting that difference for clarity.

Also applies to: 49-57, 59-70


60-67: Replace System.out.println with proper logging or remove debug output

The playWithPet method uses multiple System.out.println calls for debugging. In a Spring Boot REST API, these should either be removed or replaced with a logger (Logger / LoggerFactory) at an appropriate level to avoid noisy stdout in production.

Example sketch:

-        System.out.println("In controller.playWithPet()");
+        log.debug("In controller.playWithPet()");
...
-        System.out.println("petToPlayWith:");
-        System.out.println(petToPlayWith.getName());
+        log.debug("petToPlayWith name={}", petToPlayWith.getName());
...
-        System.out.println(petDto);
+        log.debug("Updated pet DTO={}", petDto);

(Assuming you add a private static final Logger log = LoggerFactory.getLogger(PetController.class);)


23-25: Factor out repeated Pet ↔ PetDTO mapping logic

The Pet-to-PetDTO and PetDTO-to-Pet construction logic is duplicated in several places (getAllPets, getPet, feedPet, playWithPet). This makes changes to the DTO shape or entity fields harder and more error-prone.

Consider extracting small helpers inside the controller (or centralizing in a mapper/service):

 public class PetController {

     private final PetRepository repository;
 
+    private PetDTO toDto(Pet pet) {
+        return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId());
+    }
+
+    private Pet toEntity(PetDTO dto) {
+        return new Pet(dto.getName(), dto.getSpecies(), dto.getHunger(), dto.getHappiness(), dto.getId());
+    }

Then use them:

-        return repository.findAll().stream()
-                .map(pet -> new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId()))
-                .toList();
+        return repository.findAll().stream()
+                .map(this::toDto)
+                .toList();
...
-        PetDTO petDto = PetService.feedPet(repository.findPetById(id)
-                .map(pet -> new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId()))
-                .orElseThrow());
-        Pet fedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
+        PetDTO petDto = PetService.feedPet(repository.findPetById(id)
+                .map(this::toDto)
+                .orElseThrow());
+        Pet fedPet = toEntity(petDto);
...
-        PetDTO petToPlayWith = repository.findPetById(id)
-                .map(pet -> new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId()))
-                .orElseThrow();
+        PetDTO petToPlayWith = repository.findPetById(id)
+                .map(this::toDto)
+                .orElseThrow();
...
-        Pet playedWithPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
+        Pet playedWithPet = toEntity(petDto);

This keeps the mapping consistent and easier to evolve.

Also applies to: 31-32, 51-55, 61-63, 68-69

📜 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 779bab3.

📒 Files selected for processing (20)
  • docker-compose.yml (1 hunks)
  • pets.http (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/org/example/App.java (0 hunks)
  • src/main/java/org/example/Exercise2025Application.java (1 hunks)
  • src/main/java/org/example/PetController.java (1 hunks)
  • src/main/java/org/example/PetDTO.java (1 hunks)
  • src/main/java/org/example/PetRepository.java (1 hunks)
  • src/main/java/org/example/PetService.java (1 hunks)
  • src/main/java/org/example/config/DevDataInitializer.java (1 hunks)
  • src/main/java/org/example/config/SecurityConfig.java (1 hunks)
  • src/main/java/org/example/enteties/Pet.java (1 hunks)
  • src/main/java/org/example/filters/ApiKeyFilter.java (1 hunks)
  • src/main/resources/application-dev.properties (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/main/resources/db/migration/V1__.sql (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/PetControllerTest.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/test/java/org/example/AppTest.java
  • src/test/java/org/example/AppIT.java
  • src/main/java/org/example/App.java
🧰 Additional context used
🧠 Learnings (3)
📚 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/resources/application.properties
  • pom.xml
  • src/test/java/org/example/Exercise2025ApplicationTest.java
📚 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
📚 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/PetRepository.java
🔇 Additional comments (13)
src/main/resources/application.properties (1)

1-2: LGTM! Configuration is appropriate.

The application name and virtual threads configuration are correctly set. Virtual threads are a good choice for I/O-bound applications.

pom.xml (3)

17-28: Empty metadata sections are acceptable for exercises.

The empty license, developers, and SCM sections are fine for a learning project. These would typically be populated in production projects.


33-84: Dependencies are well-structured for a Spring Boot REST API.

The dependency set appropriately includes web, JPA, MySQL, Flyway, validation, security, and testing support. The use of Spring Boot starters follows best practices.


30-30: Java 25 has been released and is available as of November 2025.

Java 25 was released with general availability announced on September 16, 2025. The pom.xml configuration specifying <java.version>25</java.version> is valid and points to a publicly available Java version. No changes are required.

Likely an incorrect or invalid review comment.

docker-compose.yml (2)

4-8: Hard-coded credentials are acceptable for this exercise project.

The database credentials are hard-coded, which is appropriate for a learning/exercise environment.

Based on learnings.


1-10: MySQL image version 9.5.0 is valid and available.

As of November 2025, MySQL Docker image version 9.5.0 is among the recently published tags on Docker Hub, confirming it exists and is properly maintained. The original concern that this version might not be available is unfounded. No changes needed.

Likely an incorrect or invalid review comment.

src/main/java/org/example/filters/ApiKeyFilter.java (2)

21-21: Hard-coded API key is acceptable for this exercise project.

The VALID_API_KEY = "secret" is hard-coded, which is appropriate for a learning/exercise environment.

Based on learnings.


26-31: Authentication logic is correctly implemented.

The filter properly creates a SecurityContext with appropriate authentication when the API key is valid and continues the filter chain.

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

6-11: LGTM!

Standard Spring Boot context load test. This verifies the application context can start successfully.

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

32-91: Well-structured test coverage.

The test suite effectively covers authorization scenarios, CRUD operations, and the feed/play actions with appropriate security contexts and mocking.

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

27-28: Verify that unauthenticated pet creation is intentional.

Line 28 allows POST /api/pets without authentication (permitAll()), while other operations require authentication. Ensure this is the intended behavior for your API.

src/main/java/org/example/enteties/Pet.java (1)

42-56: Well-implemented equals and hashCode.

The equals and hashCode methods correctly handle Hibernate proxy instances, which is essential for proper JPA entity behavior.

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

11-26: Controller wiring and list endpoint look solid

Constructor injection, base @RequestMapping("api"), and the getAllPets stream-to-DTO mapping are clear and idiomatic. No functional issues spotted here.

Comment thread pets.http Outdated
Comment thread pom.xml
Comment thread src/main/java/org/example/enteties/Pet.java Outdated
Comment thread src/main/java/org/example/PetController.java
Comment thread src/main/java/org/example/PetController.java
Comment thread src/main/java/org/example/PetService.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: 1

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

3-16: Missing @service annotation and static methods prevent Spring management.

The class lacks the @Service annotation and all methods are static. This prevents Spring from managing the bean lifecycle, dependency injection, and makes testing difficult. This issue was previously raised in past reviews.

Apply this diff to fix:

+import org.springframework.stereotype.Service;
+
+@Service
 public class PetService {
-    public static PetDTO feedPet(PetDTO pet) {
+    public PetDTO feedPet(PetDTO pet) {
         System.out.println("In service.feedPet()");
         var hunger = pet.getHunger()-10;
         System.out.println(pet.getName());
         return new PetDTO(pet.getName(), pet.getSpecies(), hunger, pet.getHappiness(), pet.getId());
     }
-    public static PetDTO playWithPet(PetDTO pet) {
+    public PetDTO playWithPet(PetDTO pet) {
         System.out.println("in service.playWithPet()");
         var happiness = pet.getHappiness()+10;
         System.out.println(pet.getName());
         return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), happiness, pet.getId());
     }
 }

Then update PetController to inject PetService via constructor.

🧹 Nitpick comments (8)
src/main/java/org/example/PetService.java (3)

5-7: Replace debug print statements with proper logging.

Console print statements are not suitable for production. Use SLF4J or remove them entirely.

Example with SLF4J:

+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public class PetService {
+    private static final Logger log = LoggerFactory.getLogger(PetService.class);
+
     public static PetDTO feedPet(PetDTO pet) {
-        System.out.println("In service.feedPet()");
+        log.debug("Feeding pet: {}", pet.getName());
         var hunger = pet.getHunger()-10;
-        System.out.println(pet.getName());
         return new PetDTO(pet.getName(), pet.getSpecies(), hunger, pet.getHappiness(), pet.getId());
     }

Apply similarly to playWithPet at lines 11-13.


6-6: Add boundary validation for hunger.

Decrementing hunger without validation could produce negative values. Consider clamping to a valid range (e.g., 0-100).

     public static PetDTO feedPet(PetDTO pet) {
         System.out.println("In service.feedPet()");
-        var hunger = pet.getHunger()-10;
+        var hunger = Math.max(0, pet.getHunger() - 10);
         System.out.println(pet.getName());
         return new PetDTO(pet.getName(), pet.getSpecies(), hunger, pet.getHappiness(), pet.getId());
     }

12-12: Add boundary validation for happiness.

Incrementing happiness without validation could produce unrealistic values. Consider clamping to a valid range (e.g., 0-100).

     public static PetDTO playWithPet(PetDTO pet) {
         System.out.println("in service.playWithPet()");
-        var happiness = pet.getHappiness()+10;
+        var happiness = Math.min(100, pet.getHappiness() + 10);
         System.out.println(pet.getName());
         return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), happiness, pet.getId());
     }
src/main/java/org/example/config/SecurityConfig.java (2)

36-46: Remove commented-out code.

Dead code should be deleted to maintain clarity. If the alternative security configuration is needed for reference, document it elsewhere or use version control history.

                 .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class);
-        //http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll())
-                //.csrf(AbstractHttpConfigurer::disable);
         return http.build();
     }
-   /* public SecurityFilterChain securityFilterChain(HttpSecurity http, ApiKeyFilter apiKeyFilter) throws Exception {
-        http
-                .securityMatcher("/api/**")
-                .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
-                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
-        return http.build();
-    }*/
     @Bean

48-58: In-memory user store is not suitable for production.

The hardcoded users and passwords are acceptable for development or testing but should be replaced with a proper user store (database, LDAP, OAuth) before production deployment.

src/main/java/org/example/PetController.java (3)

64-64: Remove debug print statement.

Console output is not appropriate for production code.

     @PutMapping("pets/{id}/play")
     public ResponseEntity<PetDTO> playWithPet(@PathVariable Integer id){
-        System.out.println("In controller.playWithPet()");
         PetDTO petDto = PetService.playWithPet(repository.findPetById(id)

68-68: Fix misleading variable name.

The variable fedPet is used in the playWithPet method, which is confusing. Use a more accurate name like playedPet or a generic name like updatedPet.

-        Pet fedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
-        repository.save(fedPet);
+        Pet updatedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
+        repository.save(updatedPet);

52-71: Consider extracting the common pattern in feedPet and playWithPet.

Both methods share the same structure: fetch entity, map to DTO, call service method, create new entity, save, and return. You could extract this into a helper method to reduce duplication.

Example:

private ResponseEntity<PetDTO> updatePet(Integer id, Function<PetDTO, PetDTO> updateFunction) {
    PetDTO petDto = updateFunction.apply(repository.findPetById(id)
            .map(pet -> new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId()))
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)));
    Pet updatedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
    repository.save(updatedPet);
    return new ResponseEntity<>(petDto, HttpStatus.OK);
}

@PutMapping("pets/{id}/feed")
public ResponseEntity<PetDTO> feedPet(@PathVariable Integer id) {
    return updatePet(id, petService::feedPet);
}

@PutMapping("pets/{id}/play")
public ResponseEntity<PetDTO> playWithPet(@PathVariable Integer id) {
    return updatePet(id, petService::playWithPet);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 779bab3 and 5414b20.

📒 Files selected for processing (5)
  • src/main/java/org/example/PetController.java (1 hunks)
  • src/main/java/org/example/PetService.java (1 hunks)
  • src/main/java/org/example/config/SecurityConfig.java (1 hunks)
  • src/main/java/org/example/enteties/Pet.java (1 hunks)
  • src/test/java/org/example/PetControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/org/example/PetControllerTest.java
  • src/main/java/org/example/enteties/Pet.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/example/PetController.java (1)
src/main/java/org/example/PetService.java (1)
  • PetService (3-16)
🔇 Additional comments (3)
src/main/java/org/example/config/SecurityConfig.java (1)

25-35: LGTM! Security configuration is appropriate for a stateless API.

The security chain correctly:

  • Disables CSRF for API endpoints (appropriate for stateless APIs)
  • Requires authentication for all pet operations
  • Applies the custom ApiKeyFilter before standard authentication
src/main/java/org/example/PetController.java (2)

30-36: LGTM! 404 handling is now correctly implemented.

The method properly returns 404 Not Found when a pet doesn't exist, addressing the previous review feedback.


38-43: LGTM! Persistence issue is fixed.

The adoptPet method now correctly persists the new pet via repository.save(), addressing the previous critical issue.

Comment thread src/main/java/org/example/PetController.java Outdated
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: 4

🧹 Nitpick comments (6)
src/test/java/org/example/PetControllerTest.java (4)

31-39: Add assertions to verify response content, not just status.

The test only checks the HTTP status but doesn't verify the returned pets match the mock data. This could pass even if the endpoint returns incorrect data.

Consider adding content assertions:

+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+
     @Test
     @WithMockUser
     void getAllPetsShouldReturnPetsWhenAuthorized() throws Exception {
         List<Pet> pets = List.of(new Pet("Bergman", "Dog", 40, 70));
         Mockito.when(petRepository.findAll()).thenReturn(pets);
 
         mockMvc.perform(get("/api/pets").header("X-API-KEY", "secret"))
-                .andExpect(status().isOk());
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].name").value("Bergman"))
+                .andExpect(jsonPath("$[0].species").value("Dog"));
     }

40-48: Remove unnecessary mock setup in unauthorized test.

Lines 42-43 set up mock data, but since the request is unauthorized, it never reaches the controller logic that would use this mock. The setup adds noise without value.

Apply this diff to simplify:

     @Test
     void getAllPetsShouldReturn401WhenUnauthorized() throws Exception{
-        List<Pet> pets = List.of(new Pet("Bergman", "Dog", 40, 70));
-        Mockito.when(petRepository.findAll()).thenReturn(pets);
-
         mockMvc.perform(get("/api/pets"))
                 .andExpect(status().isUnauthorized());
-
     }

50-58: Add assertions to verify the returned pet data.

Similar to the previous test, this only checks status but doesn't verify the response body contains the correct pet information.

Add content assertions:

     @Test
     @WithMockUser
     void getPetShouldReturnPetWhenAuthorized() throws Exception {
         Pet pet = new Pet("Bergman", "Dog", 40, 70);
         Mockito.when(petRepository.findPetById(1)).thenReturn(Optional.of(pet));
 
         mockMvc.perform(get("/api/pets/1").header("X-API-KEY", "secret"))
-                .andExpect(status().isOk());
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.name").value("Bergman"))
+                .andExpect(jsonPath("$.species").value("Dog"))
+                .andExpect(jsonPath("$.hunger").value(40))
+                .andExpect(jsonPath("$.happiness").value(70));
     }

75-96: Add assertions to verify the service transformations are applied.

Both feedPet and playWithPet tests only check status codes but don't verify that the returned DTOs reflect the expected changes (hunger decreased by 10, happiness increased by 10).

Add content assertions to both tests:

     @Test
     @WithMockUser(username="admin", roles={"ADMIN"})
     void feedPetShouldReturnOk() throws Exception {
         Pet pet = new Pet("Bergman", "Dog", 40, 60, 2);
         Mockito.when(petRepository.findPetById(2)).thenReturn(Optional.of(pet));
         PetDTO petDTO = new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId());
         PetDTO petDTO2 = new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger()-10, pet.getHappiness(), pet.getId());
         Mockito.when(petService.feedPet(petDTO)).thenReturn(petDTO2);
         mockMvc.perform(put("/api/pets/2/feed").with(csrf()).header("X-API-KEY", "secret"))
-                .andExpect(status().isOk());
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.hunger").value(30));
     }
     @Test
     @WithMockUser
     void playWithPetShouldReturnOk() throws Exception {
         Pet pet = new Pet("Bergman", "Dog", 40, 40, 1);
         Mockito.when(petRepository.findPetById(1)).thenReturn(Optional.of(pet));
         PetDTO petDTO = new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId());
         PetDTO petDTO2 = new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness()+10, pet.getId());
         Mockito.when(petService.playWithPet(petDTO)).thenReturn(petDTO2);
         mockMvc.perform(put("/api/pets/1/play").with(csrf()).header("X-API-KEY", "secret"))
-                .andExpect(status().isOk());
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.happiness").value(50));
     }
src/main/java/org/example/PetController.java (2)

54-73: Extract common logic to reduce duplication between feedPet and playWithPet.

These two methods share nearly identical structure:

  1. Fetch Pet by ID and map to DTO (or throw 404)
  2. Call service method
  3. Map DTO back to Pet entity
  4. Save and return

Additionally, the Pet-to-PetDTO mapping pattern appears throughout the controller (lines 28, 35, 57, 68).

Consider extracting helper methods:

private PetDTO mapToDTO(Pet pet) {
    return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId());
}

private Pet mapToEntity(PetDTO dto) {
    return new Pet(dto.getName(), dto.getSpecies(), dto.getHunger(), dto.getHappiness(), dto.getId());
}

private PetDTO findAndMapOrThrow(Integer id) {
    return repository.findPetById(id)
            .map(this::mapToDTO)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}

Then simplify both methods:

@PutMapping("pets/{id}/feed")
public ResponseEntity<PetDTO> feedPet(@PathVariable Integer id){
    PetDTO updatedDto = service.feedPet(findAndMapOrThrow(id));
    repository.save(mapToEntity(updatedDto));
    return ResponseEntity.ok(updatedDto);
}

@PutMapping("pets/{id}/play")
public ResponseEntity<PetDTO> playWithPet(@PathVariable Integer id){
    PetDTO updatedDto = service.playWithPet(findAndMapOrThrow(id));
    repository.save(mapToEntity(updatedDto));
    return ResponseEntity.ok(updatedDto);
}

59-70: Rename variable for clarity in playWithPet.

Lines 59 and 70 both use the variable name fedPet, but in playWithPet the pet isn't being fed—it's being played with. Consider renaming to updatedPet for consistency.

Apply this diff:

     @PutMapping("pets/{id}/play")
     public ResponseEntity<PetDTO> playWithPet(@PathVariable Integer id){
         System.out.println("In controller.playWithPet()");
         PetDTO petDto = service.playWithPet(repository.findPetById(id)
                 .map(pet -> new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), pet.getHappiness(), pet.getId()))
                 .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)));
-        Pet fedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
-        repository.save(fedPet);
+        Pet updatedPet = new Pet(petDto.getName(), petDto.getSpecies(), petDto.getHunger(), petDto.getHappiness(), petDto.getId());
+        repository.save(updatedPet);
         return new ResponseEntity<>(petDto, HttpStatus.OK);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5414b20 and 469569d.

📒 Files selected for processing (3)
  • src/main/java/org/example/PetController.java (1 hunks)
  • src/main/java/org/example/PetService.java (1 hunks)
  • src/test/java/org/example/PetControllerTest.java (1 hunks)

Comment thread src/main/java/org/example/PetController.java Outdated
Comment thread src/main/java/org/example/PetService.java
Comment on lines +13 to +18
public PetDTO playWithPet(PetDTO pet) {
System.out.println("in service.playWithPet()");
var happiness = pet.getHappiness()+10;
System.out.println(pet.getName());
return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), happiness, pet.getId());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace console logging with a proper logger and add input validation.

Similar to feedPet, this method has the same issues:

  • Uses System.out.println instead of a logging framework
  • No validation prevents happiness from growing unbounded when increased by 10
  • No null check on the pet parameter

Apply this diff:

     public PetDTO playWithPet(PetDTO pet) {
-        System.out.println("in service.playWithPet()");
+        if (pet == null) {
+            throw new IllegalArgumentException("Pet cannot be null");
+        }
+        log.debug("Playing with pet: {}", pet.getName());
         var happiness = pet.getHappiness()+10;
-        System.out.println(pet.getName());
+        if (happiness > 100) {
+            happiness = 100;
+        }
         return new PetDTO(pet.getName(), pet.getSpecies(), pet.getHunger(), happiness, pet.getId());
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/org/example/PetService.java around lines 13 to 18, replace the
System.out.println calls with the class logger (e.g., private static final
Logger logger = LoggerFactory.getLogger(PetService.class)) and log at an
appropriate level (info/debug) the method entry and pet name; add a null check
that throws IllegalArgumentException if pet is null; clamp the happiness
increase so it cannot grow unbounded (e.g., newHappiness =
Math.min(pet.getHappiness() + 10, 100)); then construct and return the new
PetDTO with the bounded happiness value.

Comment on lines +59 to +68
@Test
@WithMockUser(username="api", roles = {"API"})
void createPetShouldReturnPet() throws Exception {
Pet pet = new Pet("Bergman", "Dog", 40, 70);
Mockito.when(petRepository.save(pet)).thenReturn(pet);

mockMvc.perform(post("/api/pets").with(csrf()).header("X-API-KEY", "secret").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"Bergman\", \"species\": \"dog\",\"hunger\": 50,\"happiness\": 50}"))
.andExpect(status().isCreated());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix data mismatch between mock and request body.

Line 62 creates a Pet with hunger: 40, happiness: 70, but line 66 sends JSON with hunger: 50, happiness: 50. The mock at line 63 expects the exact Pet object from line 62, so it won't match the actual saved entity, which will be created from the JSON payload.

Apply this diff to align the test data:

     @Test
     @WithMockUser(username="api", roles = {"API"})
     void createPetShouldReturnPet() throws Exception {
-        Pet pet = new Pet("Bergman", "Dog", 40, 70);
-        Mockito.when(petRepository.save(pet)).thenReturn(pet);
+        Pet pet = new Pet("Bergman", "dog", 50, 50);
+        Mockito.when(petRepository.save(Mockito.any(Pet.class))).thenReturn(pet);
 
         mockMvc.perform(post("/api/pets").with(csrf()).header("X-API-KEY", "secret").contentType(MediaType.APPLICATION_JSON)
                         .content("{\"name\": \"Bergman\", \"species\": \"dog\",\"hunger\": 50,\"happiness\": 50}"))
                 .andExpect(status().isCreated());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
@WithMockUser(username="api", roles = {"API"})
void createPetShouldReturnPet() throws Exception {
Pet pet = new Pet("Bergman", "Dog", 40, 70);
Mockito.when(petRepository.save(pet)).thenReturn(pet);
mockMvc.perform(post("/api/pets").with(csrf()).header("X-API-KEY", "secret").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"Bergman\", \"species\": \"dog\",\"hunger\": 50,\"happiness\": 50}"))
.andExpect(status().isCreated());
}
@Test
@WithMockUser(username="api", roles = {"API"})
void createPetShouldReturnPet() throws Exception {
Pet pet = new Pet("Bergman", "dog", 50, 50);
Mockito.when(petRepository.save(Mockito.any(Pet.class))).thenReturn(pet);
mockMvc.perform(post("/api/pets").with(csrf()).header("X-API-KEY", "secret").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"Bergman\", \"species\": \"dog\",\"hunger\": 50,\"happiness\": 50}"))
.andExpect(status().isCreated());
}
🤖 Prompt for AI Agents
In src/test/java/org/example/PetControllerTest.java around lines 59 to 68, the
test creates a Pet with hunger=40 and happiness=70 but sends JSON with hunger=50
and happiness=50, so the Mockito.when(petRepository.save(pet)) mock won't match;
update the Pet instantiation on line 62 to use hunger=50 and happiness=50 (or
alternatively change the JSON to match the Pet) so the mocked saved entity
matches the request body.

@kai-ericson kai-ericson changed the title Exercise 8 kai-ericson/Exercise 8 Nov 24, 2025
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.

Exercise 8 - Spring Boot

2 participants