Repository: storkforge/PetConnect
Pull request #34: Feature profile picture #10
Author: login: Biggienation
Created: 2025-03-30T07:45:18Z | Merged: 2025-03-31T12:34:08Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Enabled pet and user profile picture management with new options to upload and retrieve images.
- **Chores**
	- Configured file upload settings with file size and allowed types restrictions for reliable image handling.
- **Tests**
	- Introduced automated tests to ensure robust performance of the file storage and profile picture features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/pom.xml b/pom.xml
index 16d45a8..bf39cc1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,11 +56,11 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
-<!--        <dependency>-->
-<!--            <groupId>org.springframework.security</groupId>-->
-<!--            <artifactId>spring-security-test</artifactId>-->
-<!--            <scope>test</scope>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>com.h2database</groupId>
             <artifactId>h2</artifactId>
diff --git a/src/main/java/se/storkforge/petconnect/controller/PetController.java b/src/main/java/se/storkforge/petconnect/controller/PetController.java
index 697d777..314e1f4 100644
--- a/src/main/java/se/storkforge/petconnect/controller/PetController.java
+++ b/src/main/java/se/storkforge/petconnect/controller/PetController.java
@@ -1,7 +1,10 @@
 package se.storkforge.petconnect.controller;
 
+import org.springframework.core.io.Resource;
+import org.springframework.web.multipart.MultipartFile;
 import se.storkforge.petconnect.entity.Pet;
 import se.storkforge.petconnect.exception.PetNotFoundException;
+import se.storkforge.petconnect.repository.PetRepository;
 import se.storkforge.petconnect.service.PetService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
@@ -18,10 +21,13 @@
 public class PetController {
 
     private final PetService petService;
+    private final PetRepository petRepository;
 
     @Autowired
-    public PetController(PetService petService) {
+    public PetController(PetService petService,
+                         PetRepository petRepository) {
         this.petService = petService;
+        this.petRepository = petRepository;
     }
 
     @GetMapping
@@ -69,4 +75,18 @@ public ResponseEntity<Void> deletePet(@PathVariable Long id) {
             return ResponseEntity.notFound().build();
         }
     }
+
+    @PostMapping("/{id}/PFP")
+    public ResponseEntity<String> uploadPetProfilePicture(
+            @PathVariable Long id, @RequestParam("file") MultipartFile file) {
+        petService.uploadProfilePicture(id, file);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/{id}/PFP")
+    public ResponseEntity<Resource> getPetProfilePicture(
+            @PathVariable Long id) {
+        Resource resource = petService.getProfilePicture(id);
+        return new ResponseEntity<>(resource, HttpStatus.OK);
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/se/storkforge/petconnect/controller/UserController.java b/src/main/java/se/storkforge/petconnect/controller/UserController.java
index ed2d11e..69fa5d9 100644
--- a/src/main/java/se/storkforge/petconnect/controller/UserController.java
+++ b/src/main/java/se/storkforge/petconnect/controller/UserController.java
@@ -2,9 +2,11 @@
 
 import jakarta.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 import se.storkforge.petconnect.entity.User;
 import se.storkforge.petconnect.exception.UserNotFoundException;
 import se.storkforge.petconnect.service.UserService;
@@ -66,4 +68,18 @@ public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
             return ResponseEntity.notFound().build();
         }
     }
+
+    @PostMapping("/{id}/PFP")
+    public ResponseEntity<String> uploadPetProfilePicture(
+            @PathVariable Long id, @RequestParam("file") MultipartFile file) {
+        userService.uploadProfilePicture(id, file);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/{id}/PFP")
+    public ResponseEntity<Resource> getPetProfilePicture(
+            @PathVariable Long id) {
+        Resource resource = userService.getProfilePicture(id);
+        return new ResponseEntity<>(resource, HttpStatus.OK);
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/se/storkforge/petconnect/entity/Pet.java b/src/main/java/se/storkforge/petconnect/entity/Pet.java
index 352f250..0c8738c 100644
--- a/src/main/java/se/storkforge/petconnect/entity/Pet.java
+++ b/src/main/java/se/storkforge/petconnect/entity/Pet.java
@@ -28,6 +28,8 @@ public class Pet {
     private String owner;
     private String location;
 
+    private String profilePicturePath;
+
     public Pet() {}
 
     public Pet(String name, String species, boolean available, int age, String owner, String location) {
@@ -95,6 +97,10 @@ public void setLocation(String location) {
         this.location = location;
     }
 
+    public String getProfilePicturePath() { return profilePicturePath; }
+
+    public void setProfilePicturePath(String profilePicturePath) { this.profilePicturePath = profilePicturePath; }
+
     @Override
     public String toString() {
         return "Pet{" +
diff --git a/src/main/java/se/storkforge/petconnect/entity/User.java b/src/main/java/se/storkforge/petconnect/entity/User.java
index 4e0a4f4..95d1e9e 100644
--- a/src/main/java/se/storkforge/petconnect/entity/User.java
+++ b/src/main/java/se/storkforge/petconnect/entity/User.java
@@ -27,6 +27,8 @@ public class User {
     @Size(min = 8, message = "Password must be at least 8 characters")
     private String password;
 
+    private String profilePicturePath;
+
     public User() {
     }
 
@@ -68,6 +70,10 @@ public void setPassword(String password) {
         this.password = password;
     }
 
+    public String getProfilePicturePath() { return profilePicturePath; }
+
+    public void setProfilePicturePath(String profilePicturePath) { this.profilePicturePath = profilePicturePath; }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/src/main/java/se/storkforge/petconnect/service/FileStorageService.java b/src/main/java/se/storkforge/petconnect/service/FileStorageService.java
new file mode 100644
index 0000000..0d93781
--- /dev/null
+++ b/src/main/java/se/storkforge/petconnect/service/FileStorageService.java
@@ -0,0 +1,123 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            if (!destination.startsWith(this.root.toAbsolutePath())) {
+                 throw new RuntimeException("Invalid path outside upload directory.");
+            }
+            try (var inputStream = file.getInputStream()) {
+                Files.copy(inputStream, destination);
+            }
+            LOG.info("Stored file: {}", destination);
+            return filename;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to store file.", e);
+        }
+    }
+
+    private String generateFileName(String contentType) {
+        String filename = UUID.randomUUID() + "." + contentType.split("/")[1];
+
+        //if name is already taken
+        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+        if (Files.exists(destination)) {
+            filename = generateFileName(contentType);
+        }
+
+        return filename;
+    }
+
+
+    public void delete(String filename) {
+        if (filename == null) {
+            LOG.warn("Attempted to delete a null filename");
+            return;
+        }
+        try {
+            Path file = root.resolve(filename);
+            Files.deleteIfExists(file);
+            LOG.info("Deleted file: {}", filename);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not delete file: " + filename, e);
+        }
+    }
+
+    //Returns a Resource representing the file
+    public Resource loadFile(String filename) {
+        try {
+            Path file = root.resolve(filename);
+            Resource resource = new UrlResource(file.toUri());
+
+            if (resource.exists() && resource.isReadable()) {
+                return resource;
+            } else {
+                throw new RuntimeException("Could not read file: " + filename);
+            }
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("Could not read file: " + filename, e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/se/storkforge/petconnect/service/PetService.java b/src/main/java/se/storkforge/petconnect/service/PetService.java
index 9e11592..e73f015 100644
--- a/src/main/java/se/storkforge/petconnect/service/PetService.java
+++ b/src/main/java/se/storkforge/petconnect/service/PetService.java
@@ -2,11 +2,15 @@
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
 import se.storkforge.petconnect.entity.Pet;
 import se.storkforge.petconnect.exception.PetNotFoundException;
 import se.storkforge.petconnect.repository.PetRepository;
@@ -19,10 +23,12 @@ public class PetService {
     private static final Logger logger = LoggerFactory.getLogger(PetService.class);
 
     private final PetRepository petRepository;
+    private final FileStorageService fileStorageService;
 
     @Autowired
-    public PetService(PetRepository petRepository) {
+    public PetService(PetRepository petRepository, FileStorageService storageService) {
         this.petRepository = petRepository;
+        this.fileStorageService = storageService;
     }
 
     @Transactional(readOnly = true)
@@ -68,4 +74,40 @@ public void deletePet(Long id) {
         petRepository.deleteById(id);
         logger.info("Pet deleted: {}", id);
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        if (file == null) {
+            throw new IllegalArgumentException("File cannot be null");
+        }
+        logger.info("Uploading profile picture to pet with ID: {}", id);
+        Optional<Pet> pet = petRepository.findById(id);
+        if (pet.isEmpty()) {
+            logger.error("Pet not found with ID: {}", id);
+            throw new PetNotFoundException("Pet with id " + id + " not found");
+        }
+
+        if (pet.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(pet.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        pet.get().setProfilePicturePath(filename);
+        petRepository.save(pet.get());
+    }
+
+    @Transactional(readOnly = true)
+    public Resource getProfilePicture(Long id) {
+        Optional<Pet> pet = petRepository.findById(id);
+        if (pet.isEmpty()) {
+            logger.error("Pet not found with ID: {}", id);
+            throw new PetNotFoundException("Pet with id " + id + " not found");
+        }
+        String filename = pet.get().getProfilePicturePath();
+        if (filename == null) {
+            logger.error("Profile picture path is null for pet ID: {}", id);
+            throw new RuntimeException("Pet does not have a profile picture");
+        }
+        return fileStorageService.loadFile(filename);
+
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/se/storkforge/petconnect/service/UserService.java b/src/main/java/se/storkforge/petconnect/service/UserService.java
index 7759d88..71d4910 100644
--- a/src/main/java/se/storkforge/petconnect/service/UserService.java
+++ b/src/main/java/se/storkforge/petconnect/service/UserService.java
@@ -1,10 +1,12 @@
 package se.storkforge.petconnect.service;
 
+import org.springframework.core.io.Resource;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
 import se.storkforge.petconnect.entity.User;
 import se.storkforge.petconnect.exception.UserNotFoundException;
 import se.storkforge.petconnect.repository.UserRepository;
@@ -17,15 +19,16 @@
 @Service
 @Transactional
 public class UserService {
-
+    private final FileStorageService fileStorageService;
     private final UserRepository userRepository;
     private final PasswordEncoder passwordEncoder;
     private static final String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
     private final Pattern emailPattern = Pattern.compile(EMAIL_REGEX);
 
-    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
+    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, FileStorageService fileStorageService) {
         this.userRepository = userRepository;
         this.passwordEncoder = passwordEncoder;
+        this.fileStorageService = fileStorageService;
     }
 
     public User createUser(User user) {
@@ -124,4 +127,35 @@ private boolean isInvalidEmail(String email) {
         Matcher matcher = emailPattern.matcher(email);
         return !matcher.matches();
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        if (file == null) {
+            throw new IllegalArgumentException("File cannot be null");
+        }
+        Optional<User> user = userRepository.findById(id);
+        if (user.isEmpty()) {
+            throw new UserNotFoundException("User with id " + id + " not found");
+        }
+
+        if (user.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(user.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        user.get().setProfilePicturePath(filename);
+        userRepository.save(user.get());
+    }
+
+    @Transactional(readOnly = true)
+    public Resource getProfilePicture(Long id) {
+        Optional<User> user = userRepository.findById(id);
+        if (user.isEmpty()) {
+            throw new UserNotFoundException("User with id " + id + " not found");
+        }
+        String filename = user.get().getProfilePicturePath();
+        if (filename == null) {
+            throw new RuntimeException("User does not have a profile picture");
+        }
+        return fileStorageService.loadFile(filename);
+    }
 }
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b787193..1c3792b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -13,3 +13,8 @@ spring.jpa.properties.hibernate.format_sql=true
 
 # Enable GraphQL endpoint
 spring.graphql.graphiql.enabled=true
+
+# === File Storage Configuration ===
+file.upload-dir=uploads
+file.max-size=1048576
+file.allowed-types=image/jpeg,image/png,image/gif
diff --git a/src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java b/src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java
new file mode 100644
index 0000000..c0f291a
--- /dev/null
+++ b/src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java
@@ -0,0 +1,91 @@
+package se.storkforge.petconnect.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.util.ReflectionTestUtils;
+import se.storkforge.petconnect.entity.Pet;
+import se.storkforge.petconnect.repository.PetRepository;
+import se.storkforge.petconnect.service.FileStorageService;
+import se.storkforge.petconnect.service.PetService;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FileStorageServiceTest {
+
+    private Pet pet =  new Pet("Buddy",
+            "Dog",
+            true,
+            3,
+            "John",
+            "New York");
+
+    private Optional<Pet> optionalPet;
+    private MockMultipartFile file;
+
+    private FileStorageService fileStorageService;
+    private PetService petService;
+
+    private Path testDir;
+
+    @Mock
+    private PetRepository petRepository;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        this.testDir = Files.createTempDirectory("test-uploads");
+        this.optionalPet = Optional.of(pet);
+        this.file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test".getBytes());
+        this.fileStorageService = new FileStorageService();
+        this.petService = new PetService(petRepository, fileStorageService);
+        // Ensure directory exists
+        try {
+            Files.createDirectories(testDir);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not create test directory", e);
+        }
+        ReflectionTestUtils.setField(fileStorageService, "maxFileSize", 100L);
+        ReflectionTestUtils.setField(fileStorageService, "allowedTypes", List.of("image/jpeg"));
+        ReflectionTestUtils.setField(fileStorageService, "root", testDir);
+    }
+
+    @Test
+    void petProfilePicturePathNotNull() throws Exception {
+        when(petRepository.findById(1L)).thenReturn(optionalPet);
+        petService.uploadProfilePicture(1L, file);
+        assertThat(pet.getProfilePicturePath()).isNotNull();
+        fileStorageService.delete(pet.getProfilePicturePath());
+    }
+
+    @Test
+    void uploadFileEvenIfOldFileIsGone() throws Exception {
+        when(petRepository.findById(1L)).thenReturn(optionalPet);
+        petService.uploadProfilePicture(1L, file);
+        fileStorageService.delete(pet.getProfilePicturePath());
+        petService.uploadProfilePicture(1L, file);
+        assertThat(pet.getProfilePicturePath()).isNotNull();
+        fileStorageService.delete(pet.getProfilePicturePath());
+
+    }
+
+    @Test
+    void returnFile() throws Exception {
+        when(petRepository.findById(1L)).thenReturn(optionalPet);
+        petService.uploadProfilePicture(1L, file);
+        Resource pfp = petService.getProfilePicture(1L);
+        assertThat(pfp.getURL()).isNotNull();
+        fileStorageService.delete(pet.getProfilePicturePath());
+    }
+}
\ No newline at end of file
diff --git a/uploads/55213a1e-f531-4460-add0-8da98feda7ce.jpeg b/uploads/55213a1e-f531-4460-add0-8da98feda7ce.jpeg
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/uploads/55213a1e-f531-4460-add0-8da98feda7ce.jpeg
@@ -0,0 +1 @@
+test
\ No newline at end of file


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 12**

<details>
<summary>🧹 Nitpick comments (17)</summary><blockquote>

<details>
<summary>src/main/resources/application.properties (1)</summary><blockquote>

`17-20`: **File storage configuration has some potential improvements.**

The file storage configuration looks good overall, but there are a couple of suggestions:

1. The `file.upload-dir=uploads` uses a relative path which could be problematic in production environments if the working directory changes. Consider using an absolute path or making it configurable for different environments.

2. The maximum file size of 1MB (1048576 bytes) is reasonable for basic profile pictures, but modern high-resolution images might exceed this limit. Consider increasing it to 2-5MB for better user experience.

3. The restriction of allowed file types to common image formats is a good security practice.

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/entity/Pet.java (3)</summary><blockquote>

`31-31`: **Consider adding documentation for the profilePicturePath field.**

Adding a comment explaining what this field represents, its format, and any constraints would help other developers understand its purpose.

---

`100-102`: **Maintain consistent coding style.**

The getter and setter methods for `profilePicturePath` use a compact one-line style, while other methods in the class use a multi-line style. Consider following the existing pattern for consistency:

```diff
-    public String getProfilePicturePath() { return profilePicturePath; }
+    public String getProfilePicturePath() {
+        return profilePicturePath;
+    }

-    public void setProfilePicturePath(String profilePicturePath) { this.profilePicturePath = profilePicturePath; }
+    public void setProfilePicturePath(String profilePicturePath) {
+        this.profilePicturePath = profilePicturePath;
+    }
```

---

`35-42`: **Update constructor and toString to include profilePicturePath.**

Consider updating the constructor to include the new field, and add the field to the toString method for better debugging:

```diff
    public Pet(String name, String species, boolean available, int age, String owner, String location) {
        this.name = name;
        this.species = species;
        this.available = available; // Corrected field name
        this.age = age;
        this.owner = owner;
        this.location = location;
+       this.profilePicturePath = null; // Or provide a default value
    }

    @Override
    public String toString() {
        return "Pet{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", species='" + species + '\'' +
                ", available=" + (available ? "Yes" : "No") + // Corrected field name
                ", age=" + age +
                ", owner='" + owner + '\'' +
                ", location='" + location + '\'' +
+               ", profilePicturePath='" + profilePicturePath + '\'' +
                '}';
    }
```



Also applies to: 104-115

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/entity/User.java (3)</summary><blockquote>

`30-30`: **Consider adding documentation for the profilePicturePath field.**

Adding a comment explaining what this field represents, its format, and any constraints would help other developers understand its purpose.

---

`73-75`: **Maintain consistent coding style.**

The getter and setter methods for `profilePicturePath` use a compact one-line style, while other methods in the class use a multi-line style. Consider following the existing pattern for consistency:

```diff
-    public String getProfilePicturePath() { return profilePicturePath; }
+    public String getProfilePicturePath() {
+        return profilePicturePath;
+    }

-    public void setProfilePicturePath(String profilePicturePath) { this.profilePicturePath = profilePicturePath; }
+    public void setProfilePicturePath(String profilePicturePath) {
+        this.profilePicturePath = profilePicturePath;
+    }
```

---

`35-39`: **Update constructor and toString to include profilePicturePath.**

Consider updating the constructor to include the new field, and add the field to the toString method for better debugging:

```diff
    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
+       this.profilePicturePath = null; // Or provide a default value
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", email='" + email + '\'' +
+               ", profilePicturePath='" + profilePicturePath + '\'' +
                '}';
    }
```



Also applies to: 90-97

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/controller/UserController.java (2)</summary><blockquote>

`72-77`: **Inconsistent naming convention in endpoint URL**

The endpoint URL uses "PFP" abbreviation while the method name uses "ProfilePicture". Consider using consistent terminology throughout the codebase for better maintainability.

Also, the method name indicates it's for a pet profile picture (`uploadPetProfilePicture`), but this is in the `UserController` and calls `userService.uploadProfilePicture()`.

```diff
-    @PostMapping("/{id}/PFP")
-    public ResponseEntity<String> uploadPetProfilePicture(
+    @PostMapping("/{id}/profile-picture")
+    public ResponseEntity<String> uploadUserProfilePicture(
```

---

`79-84`: **Inconsistent response style and naming convention**

1. The endpoint uses "PFP" abbreviation while the method name uses "ProfilePicture"
2. This method uses `new ResponseEntity<>()` while other methods in this controller use the builder pattern with `ResponseEntity.ok()`
3. The method name indicates it's for a pet profile picture, but this is in the `UserController`


```diff
-    @GetMapping("/{id}/PFP")
-    public ResponseEntity<Resource> getPetProfilePicture(
+    @GetMapping("/{id}/profile-picture")
+    public ResponseEntity<Resource> getUserProfilePicture(
         @PathVariable Long id) {
         Resource resource = userService.getProfilePicture(id);
-        return new ResponseEntity<>(resource, HttpStatus.OK);
+        return ResponseEntity.ok(resource);
     }
```

</blockquote></details>
<details>
<summary>src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java (1)</summary><blockquote>

`74-80`: **Missing cleanup in test method**

This test method is not cleaning up the files it creates, which could leave files hanging around in test environments. Also, the assertion only checks if the URL is not null, not if the content is correct.

Add proper cleanup and enhance the assertion:

```diff
     @Test
     void returnFile() throws Exception {
         when(petRepository.findById(1L)).thenReturn(optionalPet);
         petService.uploadProfilePicture(1L, file);
         Resource pfp = petService.getProfilePicture(1L);
         assertThat(pfp.getURL()).isNotNull();
+        assertThat(pfp.contentLength()).isEqualTo(file.getSize());
+        // Clean up
+        fileStorageService.delete(pet.getProfilePicturePath());
     }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: Java CI with Maven</summary>

[error] 77-77: Failed to store file. Caused by: java.nio.file.NoSuchFileException: /home/runner/work/PetConnect/PetConnect/uploads/d21531c0-0a38-4dc6-afdb-a58c3851b616.jpeg

</details>

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (2)</summary><blockquote>

`70-75`: **Inconsistent naming convention in endpoint URL**

The endpoint URL uses the "PFP" abbreviation which is inconsistent with the method name that uses "ProfilePicture". Also, no error handling is implemented for potential file upload failures.

```diff
-    @PostMapping("/{id}/PFP")
+    @PostMapping("/{id}/profile-picture")
     public ResponseEntity<String> uploadPetProfilePicture(
             @PathVariable Long id, @RequestParam("file") MultipartFile file) {
-        petService.uploadProfilePicture(id, file);
-        return ResponseEntity.ok().build();
+        try {
+            petService.uploadProfilePicture(id, file);
+            return ResponseEntity.ok("Profile picture uploaded successfully");
+        } catch (PetNotFoundException e) {
+            return ResponseEntity.notFound().build();
+        } catch (RuntimeException e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        }
     }
```

---

`77-82`: **Inconsistent naming convention and response style**

The endpoint uses "PFP" abbreviation while the method name uses "ProfilePicture". This method creates a new ResponseEntity directly while other methods in the controller use the builder pattern. Also, no error handling is implemented for potential file retrieval failures.

```diff
-    @GetMapping("/{id}/PFP")
+    @GetMapping("/{id}/profile-picture")
     public ResponseEntity<Resource> getPetProfilePicture(
             @PathVariable Long id) {
-        Resource resource = petService.getProfilePicture(id);
-        return new ResponseEntity<>(resource, HttpStatus.OK);
+        try {
+            Resource resource = petService.getProfilePicture(id);
+            return ResponseEntity.ok()
+                   .contentType(MediaType.IMAGE_JPEG) // Set appropriate content type
+                   .body(resource);
+        } catch (PetNotFoundException e) {
+            return ResponseEntity.notFound().build();
+        } catch (RuntimeException e) {
+            return ResponseEntity.badRequest().build();
+        }
     }
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (2)</summary><blockquote>

`131-144`: **Add logging statements to profile picture operations.**

Unlike the PetService class (which includes logging for similar operations), the uploadProfilePicture method lacks logging statements. Consider adding log entries for important events like upload attempts, successful uploads, and file deletions.

```diff
public void uploadProfilePicture(Long id, MultipartFile file) {
+    logger.info("Uploading profile picture for user with ID: {}", id);
    Optional<User> user = userRepository.findById(id);
    if (user.isEmpty()) {
+        logger.error("User not found with ID: {}", id);
        throw new UserNotFoundException("User with id " + id + " not found");
    }

    if (user.get().getProfilePicturePath() != null) {
+        logger.debug("Deleting existing profile picture: {}", user.get().getProfilePicturePath());
        fileStorageService.delete(user.get().getProfilePicturePath());
    }

    String filename = fileStorageService.store(file);
+    logger.info("Profile picture uploaded successfully for user ID: {}, filename: {}", id, filename);
    user.get().setProfilePicturePath(filename);
    userRepository.save(user.get());
}
```

---

`146-153`: **Add logging to getProfilePicture method.**

Similar to the previous comment, add logging to track profile picture retrievals.

```diff
public Resource getProfilePicture(Long id) {
+    logger.info("Retrieving profile picture for user with ID: {}", id);
    Optional<User> user = userRepository.findById(id);
    if (user.isEmpty()) {
+        logger.error("User not found with ID: {}", id);
        throw new UserNotFoundException("User with id " + id + " not found");
    }
    String filename = user.get().getProfilePicturePath();
+    logger.debug("Loading profile picture: {}", filename);
    return fileStorageService.loadFile(filename);
}
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (3)</summary><blockquote>

`58-61`: **Improve error messages for file validation failures.**

Provide more descriptive error messages that include details about what was rejected and what is acceptable.

```diff
String contentType = file.getContentType();
if (contentType == null || !allowedTypes.contains(contentType)) {
-    throw new RuntimeException("File type not allowed.");
+    throw new RuntimeException("File type not allowed. Received: " + contentType + 
+        ". Allowed types: " + String.join(", ", allowedTypes));
}
```

---

`77-87`: **Add maximum recursion limit to prevent stack overflow.**

The recursive call to generateFileName could potentially cause a stack overflow if too many filenames are taken. Add a limit to the recursion depth.

```diff
- private String generateFileName(String contentType) {
+ private String generateFileName(String contentType) {
+     return generateFileName(contentType, 0);
+ }
+ 
+ private String generateFileName(String contentType, int recursionDepth) {
+     final int MAX_RECURSION_DEPTH = 10;
+     if (recursionDepth >= MAX_RECURSION_DEPTH) {
+         throw new RuntimeException("Failed to generate unique filename after " + 
+             MAX_RECURSION_DEPTH + " attempts.");
+     }
      
      String filename = UUID.randomUUID() + "." + contentType.split("/")[1];

      //if name is already taken
      Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
      if (Files.exists(destination)) {
-         filename = generateFileName(contentType);
+         filename = generateFileName(contentType, recursionDepth + 1);
      }

      return filename;
}
```

---

`20-114`: **Consider creating specific service exceptions.**

Instead of using generic RuntimeExceptions, create specific service exceptions that provide more context and allow for better error handling by the calling code.


Create a new exception class like `FileStorageException`:

```java
public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }
    
    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
```

Then replace the RuntimeExceptions with this specific exception throughout the service.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c04d7e90ddeea4399ede126eb7c9ee6e72489a99 and fd7d6b984c4c951f476d255084f5d918d3bdc685.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `pom.xml` (1 hunks)
* `src/main/java/se/storkforge/petconnect/controller/PetController.java` (4 hunks)
* `src/main/java/se/storkforge/petconnect/controller/UserController.java` (2 hunks)
* `src/main/java/se/storkforge/petconnect/entity/Pet.java` (2 hunks)
* `src/main/java/se/storkforge/petconnect/entity/User.java` (2 hunks)
* `src/main/java/se/storkforge/petconnect/service/FileStorageService.java` (1 hunks)
* `src/main/java/se/storkforge/petconnect/service/PetService.java` (3 hunks)
* `src/main/java/se/storkforge/petconnect/service/UserService.java` (3 hunks)
* `src/main/resources/application.properties` (1 hunks)
* `src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (4)</summary>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (1)</summary>

* `Service` (17-102)

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (2)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary>

* `Service` (19-154)

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary>

* `Service` (20-114)

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (1)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/exception/PetNotFoundException.java (1)</summary>

* `PetNotFoundException` (3-12)

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (1)</summary>

* `Service` (17-102)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: Java CI with Maven</summary>

<details>
<summary>src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java</summary>

[error] 58-58: Failed to store file. Caused by: java.nio.file.NoSuchFileException: /home/runner/work/PetConnect/PetConnect/uploads/d04a9942-1b4b-4472-a45a-b40a345f24ff.jpeg

---

[error] 66-66: Failed to store file. Caused by: java.nio.file.NoSuchFileException: /home/runner/work/PetConnect/PetConnect/uploads/698bc6c1-a171-4149-926d-b56bb0185ebd.jpeg

---

[error] 77-77: Failed to store file. Caused by: java.nio.file.NoSuchFileException: /home/runner/work/PetConnect/PetConnect/uploads/d21531c0-0a38-4dc6-afdb-a58c3851b616.jpeg

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>pom.xml (1)</summary>

`59-63`: **Good addition of Spring Security Test dependency.**

This dependency is appropriate for testing security aspects of the new profile picture functionality. The scope is correctly set to "test".

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary>

`22-22`: **Good implementation of dependency injection for FileStorageService.**

The FileStorageService is properly injected via constructor injection, which is a best practice that improves testability.



Also applies to: 28-31

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary>

`22-44`: **Good implementation of PostConstruct for directory initialization.**

The service properly creates the necessary directory structure at startup, making it robust against missing directories.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-30T07:48:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (2)</summary><blockquote>

`95-109`: _🛠️ Refactor suggestion_

**Missing @Transactional annotation on getProfilePicture method.**

The `getProfilePicture` method is a read operation, but it's missing the `@Transactional(readOnly = true)` annotation that's used on other read methods in this class.

```diff
+    @Transactional(readOnly = true)
     public Resource getProfilePicture(Long id) {
         Optional<Pet> pet = petRepository.findById(id);
         if (pet.isEmpty()) {
             logger.error("Pet not found with ID: {}", id);
             throw new PetNotFoundException("Pet with id " + id + " not found");
         }
         String filename = pet.get().getProfilePicturePath();
         if (filename == null) {
             logger.error("Profile picture path is null for pet ID: {}", id);
             throw new RuntimeException("Pet does not have a profile picture");
         }
         return fileStorageService.loadFile(filename);
     }
```

---

`75-93`: _🛠️ Refactor suggestion_

**Missing @Transactional annotation for file upload operation.**

The `uploadProfilePicture` method involves both file system operations and database updates, but lacks the `@Transactional` annotation found on other modifying methods in this class. This could lead to data inconsistency if the file operation succeeds but the database update fails.

```diff
+    @Transactional
     public void uploadProfilePicture(Long id, MultipartFile file) {
         if (file == null) {
+            logger.error("File is null for pet ID: {}", id);
             throw new IllegalArgumentException("File cannot be null");
         }
+        if (file.isEmpty()) {
+            logger.error("File is empty for pet ID: {}", id);
+            throw new IllegalArgumentException("File cannot be empty");
+        }
         logger.info("Uploading profile picture to pet with ID: {}", id);
         Optional<Pet> pet = petRepository.findById(id);
         if (pet.isEmpty()) {
             logger.error("Pet not found with ID: {}", id);
             throw new PetNotFoundException("Pet with id " + id + " not found");
         }

         if (pet.get().getProfilePicturePath() != null) {
-            fileStorageService.delete(pet.get().getProfilePicturePath());
+            try {
+                fileStorageService.delete(pet.get().getProfilePicturePath());
+            } catch (RuntimeException e) {
+                logger.warn("Failed to delete old profile picture: {}", e.getMessage());
+                // Continue with upload even if delete fails
+            }
         }

         String filename = fileStorageService.store(file);
         pet.get().setProfilePicturePath(filename);
         petRepository.save(pet.get());
     }
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary><blockquote>

`131-147`: _🛠️ Refactor suggestion_

**Missing @Transactional annotation for file upload operation.**

The `uploadProfilePicture` method involves both file system operations and database updates, but lacks the `@Transactional` annotation. This could lead to data inconsistency if the file operation succeeds but the database update fails.


```diff
+    @Transactional
     public void uploadProfilePicture(Long id, MultipartFile file) {
         if (file == null) {
             throw new IllegalArgumentException("File cannot be null");
         }
+        if (file.isEmpty()) {
+            throw new IllegalArgumentException("File cannot be empty");
+        }
         Optional<User> user = userRepository.findById(id);
         if (user.isEmpty()) {
             throw new UserNotFoundException("User with id " + id + " not found");
         }

         if (user.get().getProfilePicturePath() != null) {
-            fileStorageService.delete(user.get().getProfilePicturePath());
+            try {
+                fileStorageService.delete(user.get().getProfilePicturePath());
+            } catch (RuntimeException e) {
+                // Log warning but continue with upload
+                // Continue with upload even if delete fails
+            }
         }

         String filename = fileStorageService.store(file);
         user.get().setProfilePicturePath(filename);
         userRepository.save(user.get());
     }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java (4)</summary><blockquote>

`48-58`: **Redundant directory creation logic.**

You're already creating a temporary directory in line 48 with `Files.createTempDirectory("test-uploads")`, but then attempting to create it again in lines 54-58. This is unnecessary duplication that could be removed.

```diff
    @BeforeEach
    void setUp() throws IOException {
        this.testDir = Files.createTempDirectory("test-uploads");
        this.optionalPet = Optional.of(pet);
        this.file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test".getBytes());
        this.fileStorageService = new FileStorageService();
        this.petService = new PetService(petRepository, fileStorageService);
-        // Ensure directory exists
-        try {
-            Files.createDirectories(testDir);
-        } catch (IOException e) {
-            throw new RuntimeException("Could not create test directory", e);
-        }
        ReflectionTestUtils.setField(fileStorageService, "maxFileSize", 100L);
        ReflectionTestUtils.setField(fileStorageService, "allowedTypes", List.of("image/jpeg"));
        ReflectionTestUtils.setField(fileStorageService, "root", testDir);
    }
```

---

`64-70`: **Strong test assertions needed.**

The test only verifies that the profile picture path is not null, but doesn't check if the content was actually saved correctly or if the path refers to a valid file.

```diff
    @Test
    void petProfilePicturePathNotNull() throws Exception {
        when(petRepository.findById(1L)).thenReturn(optionalPet);
        petService.uploadProfilePicture(1L, file);
        assertThat(pet.getProfilePicturePath()).isNotNull();
+        // Verify the file exists at the path
+        Path filePath = testDir.resolve(pet.getProfilePicturePath());
+        assertThat(Files.exists(filePath)).isTrue();
+        // Verify content if needed
+        assertThat(Files.readAllBytes(filePath)).isEqualTo("test".getBytes());
        fileStorageService.delete(pet.getProfilePicturePath());
    }
```

---

`72-81`: **Verify the new profile picture path for reoploaded file.**

When testing that a new upload works after deleting an old file, you should also verify that the new path is different from the old one, which would confirm that a new file was created rather than overwriting the old one.

```diff
    @Test
    void uploadFileEvenIfOldFileIsGone() throws Exception {
        when(petRepository.findById(1L)).thenReturn(optionalPet);
        petService.uploadProfilePicture(1L, file);
+        String oldPath = pet.getProfilePicturePath();
        fileStorageService.delete(pet.getProfilePicturePath());
        petService.uploadProfilePicture(1L, file);
        assertThat(pet.getProfilePicturePath()).isNotNull();
+        assertThat(pet.getProfilePicturePath()).isNotEqualTo(oldPath);
+        // Verify the file exists at the new path
+        Path filePath = testDir.resolve(pet.getProfilePicturePath());
+        assertThat(Files.exists(filePath)).isTrue();
        fileStorageService.delete(pet.getProfilePicturePath());
    }
```

---

`83-90`: **Add more comprehensive file content verification.**

When testing the file retrieval functionality, it's important to verify not just that the URL is not null, but also that the retrieved resource contains the expected content.

```diff
    @Test
    void returnFile() throws Exception {
        when(petRepository.findById(1L)).thenReturn(optionalPet);
        petService.uploadProfilePicture(1L, file);
        Resource pfp = petService.getProfilePicture(1L);
        assertThat(pfp.getURL()).isNotNull();
+        // Verify the content of the retrieved resource
+        byte[] content = Files.readAllBytes(Paths.get(pfp.getURI()));
+        assertThat(content).isEqualTo("test".getBytes());
        fileStorageService.delete(pet.getProfilePicturePath());
    }
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (2)</summary><blockquote>

`47-79`: **Improve exception handling with specific exception types.**

The `store` method throws generic `RuntimeException` instances with different messages. For better error handling by calling code, consider creating and using custom exception types for different failure scenarios.

```diff
+    public class FileStorageException extends RuntimeException {
+        public FileStorageException(String message) {
+            super(message);
+        }
+
+        public FileStorageException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    public class InvalidFileTypeException extends FileStorageException {
+        public InvalidFileTypeException(String message) {
+            super(message);
+        }
+    }
+
+    public class FileSizeLimitExceededException extends FileStorageException {
+        public FileSizeLimitExceededException(String message) {
+            super(message);
+        }
+    }

     public String store(MultipartFile file) {
         try {
             //Validation
             if (file.isEmpty()) {
-                throw new RuntimeException("Unable to store empty file.");
+                throw new FileStorageException("Unable to store empty file.");
             }

             if (file.getSize() > maxFileSize) {
-                throw new RuntimeException("File size exceeds the allowed limit.");
+                throw new FileSizeLimitExceededException("File size exceeds the allowed limit.");
             }

             String contentType = file.getContentType();
             if (contentType == null || !allowedTypes.contains(contentType)) {
-                throw new RuntimeException("File type not allowed.");
+                throw new InvalidFileTypeException("File type not allowed.");
             }

             //Generate unique filename
             String filename = generateFileName(contentType);

             //Save file
             Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
             if (!destination.startsWith(this.root.toAbsolutePath())) {
-                 throw new RuntimeException("Invalid path outside upload directory.");
+                 throw new FileStorageException("Invalid path outside upload directory.");
             }
             try (var inputStream = file.getInputStream()) {
                 Files.copy(inputStream, destination);
             }
             LOG.info("Stored file: {}", destination);
             return filename;
         } catch (IOException e) {
-            throw new RuntimeException("Failed to store file.", e);
+            throw new FileStorageException("Failed to store file.", e);
         }
     }
```

---

`109-121`: **Add more detailed logging for file loading errors.**

When a file can't be loaded, the error message doesn't provide much context. Adding more detailed logging and error information would help in troubleshooting issues.

```diff
    public Resource loadFile(String filename) {
        try {
            Path file = root.resolve(filename);
            Resource resource = new UrlResource(file.toUri());

            if (resource.exists() && resource.isReadable()) {
+                LOG.info("Loaded file: {}", filename);
                return resource;
            } else {
-                throw new RuntimeException("Could not read file: " + filename);
+                LOG.error("File not found or not readable: {}", file.toAbsolutePath());
+                throw new FileStorageException("Could not read file: " + filename + " (File not found or not readable)");
            }
        } catch (MalformedURLException e) {
-            throw new RuntimeException("Could not read file: " + filename, e);
+            LOG.error("Malformed URL for file: {}", filename, e);
+            throw new FileStorageException("Could not read file: " + filename + " (Malformed URL)", e);
        }
    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd7d6b984c4c951f476d255084f5d918d3bdc685 and 9462128d7471f3d73e2551139fa3131b792313f2.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `uploads/55213a1e-f531-4460-add0-8da98feda7ce.jpeg` is excluded by `!**/*.jpeg`

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/main/java/se/storkforge/petconnect/service/FileStorageService.java` (1 hunks)
* `src/main/java/se/storkforge/petconnect/service/PetService.java` (3 hunks)
* `src/main/java/se/storkforge/petconnect/service/UserService.java` (3 hunks)
* `src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary>

* `Service` (20-123)

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (2)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary>

* `Service` (19-161)

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary>

* `Service` (20-123)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-30T08:29:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (2)</summary><blockquote>

`24-24`: _⚠️ Potential issue_

**Unused repository dependency**

The `petRepository` field is injected but never used in this controller. Controllers should only depend on service layer components, not directly on repositories.

Remove the unused dependency:

```diff
     private final PetService petService;
-    private final PetRepository petRepository;
```




Also applies to: 30-30

---

`27-28`: _⚠️ Potential issue_

**Remove unused constructor parameter**

The `petRepository` parameter is injected but never used in this controller. This violates the dependency injection principle of only injecting what you need.

Simplify the constructor:

```diff
     @Autowired
-    public PetController(PetService petService,
-                         PetRepository petRepository) {
+    public PetController(PetService petService) {
         this.petService = petService;
-        this.petRepository = petRepository;
     }
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (1)</summary><blockquote>

`78-96`: _🛠️ Refactor suggestion_

**Missing @Transactional annotation and error handling**

This method should be annotated with @Transactional to ensure consistency between file operations and database updates. Also, it needs better exception handling when deleting old profile pictures.

```diff
+    @Transactional
     public void uploadProfilePicture(Long id, MultipartFile file) {
         if (file == null) {
             throw new IllegalArgumentException("File cannot be null");
         }
+        if (file.isEmpty()) {
+            throw new IllegalArgumentException("File cannot be empty");
+        }
         logger.info("Uploading profile picture to pet with ID: {}", id);
         Optional<Pet> pet = petRepository.findById(id);
         if (pet.isEmpty()) {
             logger.error("Pet not found with ID: {}", id);
             throw new PetNotFoundException("Pet with id " + id + " not found");
         }

         if (pet.get().getProfilePicturePath() != null) {
-            fileStorageService.delete(pet.get().getProfilePicturePath());
+            try {
+                fileStorageService.delete(pet.get().getProfilePicturePath());
+            } catch (RuntimeException e) {
+                logger.warn("Failed to delete old profile picture: {}", e.getMessage());
+                // Continue with upload even if delete fails
+            }
         }

         String filename = fileStorageService.store(file);
         pet.get().setProfilePicturePath(filename);
         petRepository.save(pet.get());
     }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (1)</summary><blockquote>

`86-91`: **Inconsistent API path naming and missing media type header**

The endpoint should follow REST naming conventions, and the response should include an appropriate Content-Type header based on the image type.

```diff
-    @GetMapping("/{id}/PFP")
+    @GetMapping("/{id}/profile-picture")
     public ResponseEntity<Resource> getPetProfilePicture(
             @PathVariable Long id) {
         Resource resource = petService.getProfilePicture(id);
-        return new ResponseEntity<>(resource, HttpStatus.OK);
+        return ResponseEntity
+            .ok()
+            .contentType(MediaType.IMAGE_JPEG) // Consider detecting actual content type
+            .body(resource);
     }
```

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (1)</summary><blockquote>

`98-112`: **Consider using a more specific exception type**

Instead of using a generic `RuntimeException` when a profile picture is not found, consider creating a more specific exception type like `ProfilePictureNotFoundException` to provide better error handling and documentation.

```diff
         String filename = pet.get().getProfilePicturePath();
         if (filename == null) {
             logger.error("Profile picture path is null for pet ID: {}", id);
-            throw new RuntimeException("Pet does not have a profile picture");
+            throw new ProfilePictureNotFoundException("Pet with id " + id + " does not have a profile picture");
         }
         return fileStorageService.loadFile(filename);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9462128d7471f3d73e2551139fa3131b792313f2 and f1ae5dca80b39896c870b544d14441a6a240b532.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/main/java/se/storkforge/petconnect/controller/PetController.java` (3 hunks)
* `src/main/java/se/storkforge/petconnect/service/PetService.java` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (2)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/service/UserService.java (1)</summary>

* `Service` (19-161)

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/FileStorageService.java (1)</summary>

* `Service` (20-123)

</details>

</blockquote></details>
<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (1)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/exception/PetNotFoundException.java (1)</summary>

* `PetNotFoundException` (3-12)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/main/java/se/storkforge/petconnect/controller/PetController.java (1)</summary>

`3-4`: **LGTM: Appropriate imports added for new functionality**

The imports for `Resource` and `MultipartFile` are correctly added to support the new profile picture functionality.



Also applies to: 7-7

</details>
<details>
<summary>src/main/java/se/storkforge/petconnect/service/PetService.java (2)</summary>

`26-26`: **LGTM: FileStorageService dependency properly injected**

The `FileStorageService` dependency is correctly added and initialized in the constructor.



Also applies to: 31-31

---

`98-112`: **LGTM: Profile picture retrieval implementation**

The implementation correctly checks for the existence of the pet and its profile picture before attempting to load the file. The @Transactional annotation is properly applied with readOnly=true since this is a read operation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T12:33:40Z

Review comments:
diff_hunk: @@ -0,0 +1,81 @@
+package se.storkforge.petconnect.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.util.ReflectionTestUtils;
+import se.storkforge.petconnect.entity.Pet;
+import se.storkforge.petconnect.repository.PetRepository;
+import se.storkforge.petconnect.service.FileStorageService;
+import se.storkforge.petconnect.service.PetService;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FileStorageServiceTest {
+
+    private Pet pet =  new Pet("Buddy",
+            "Dog",
+            true,
+            3,
+            "John",
+            "New York");
+
+    private Optional<Pet> optionalPet;
+    private MockMultipartFile file;
+
+    private FileStorageService fileStorageService;
+    private PetService petService;
+
+    private Path testDir = Path.of("uploads");
+
+    @Mock
+    private PetRepository petRepository;
+
+    @BeforeEach
+    void setUp() {
+        this.optionalPet = Optional.of(pet);
+        this.file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test".getBytes());
+        this.fileStorageService = new FileStorageService();
+        this.petService = new PetService(petRepository, fileStorageService);
+        ReflectionTestUtils.setField(fileStorageService, "maxFileSize", 100L);
+        ReflectionTestUtils.setField(fileStorageService, "allowedTypes", List.of("image/jpeg"));
+        ReflectionTestUtils.setField(fileStorageService, "root", testDir);
path: src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Test configuration should create required directories**

The `ReflectionTestUtils` is setting configuration values, but there's no guarantee that the directory at `testDir` exists. This is causing the tests to fail in the CI environment.

Consider adding directory creation logic in the test setup:

```diff
     @BeforeEach
     void setUp() {
         this.optionalPet = Optional.of(pet);
         this.file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test".getBytes());
         this.fileStorageService = new FileStorageService();
         this.petService = new PetService(petRepository, fileStorageService);
+        // Ensure directory exists
+        try {
+            Files.createDirectories(testDir);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not create test directory", e);
+        }
         ReflectionTestUtils.setField(fileStorageService, "maxFileSize", 100L);
         ReflectionTestUtils.setField(fileStorageService, "allowedTypes", List.of("image/jpeg"));
         ReflectionTestUtils.setField(fileStorageService, "root", testDir);
     }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
     @BeforeEach
     void setUp() {
         this.optionalPet = Optional.of(pet);
         this.file = new MockMultipartFile("file", "test.jpg", "image/jpeg", "test".getBytes());
         this.fileStorageService = new FileStorageService();
         this.petService = new PetService(petRepository, fileStorageService);
         // Ensure directory exists
         try {
             Files.createDirectories(testDir);
         } catch (IOException e) {
             throw new RuntimeException("Could not create test directory", e);
         }
         ReflectionTestUtils.setField(fileStorageService, "maxFileSize", 100L);
         ReflectionTestUtils.setField(fileStorageService, "allowedTypes", List.of("image/jpeg"));
         ReflectionTestUtils.setField(fileStorageService, "root", testDir);
     }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,81 @@
+package se.storkforge.petconnect.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.util.ReflectionTestUtils;
+import se.storkforge.petconnect.entity.Pet;
+import se.storkforge.petconnect.repository.PetRepository;
+import se.storkforge.petconnect.service.FileStorageService;
+import se.storkforge.petconnect.service.PetService;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class FileStorageServiceTest {
+
+    private Pet pet =  new Pet("Buddy",
+            "Dog",
+            true,
+            3,
+            "John",
+            "New York");
+
+    private Optional<Pet> optionalPet;
+    private MockMultipartFile file;
+
+    private FileStorageService fileStorageService;
+    private PetService petService;
+
+    private Path testDir = Path.of("uploads");
path: src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Test directory not being created for CI environment**

The pipeline failures show that the tests are failing because the upload directory doesn't exist in the CI environment.

The tests need to ensure the directory exists or create it before running:

```diff
-    private Path testDir = Path.of("uploads");
+    private Path testDir;

     @BeforeEach
     void setUp() {
+        this.testDir = Files.createTempDirectory("test-uploads");
+        // Rest of setup
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileStorageServiceTest {

    private Path testDir;

    @BeforeEach
    void setUp() throws IOException {
        this.testDir = Files.createTempDirectory("test-uploads");
        // Rest of setup
    }

    @Test
    void someTest() {
        // Test implementation using testDir
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:33Z
author_association: NONE
diff_hunk: @@ -16,10 +19,13 @@
 public class PetController {
 
     private final PetService petService;
+    private final PetRepository petRepository;
path: src/main/java/se/storkforge/petconnect/controller/PetController.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Unused repository dependency**

The `petRepository` field is injected but never used in this controller. Controllers should only depend on service layer components, not directly on repositories.

Remove the unused dependency:

```diff
     private final PetService petService;
-    private final PetRepository petRepository;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    private final PetService petService;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:33Z
author_association: NONE
diff_hunk: @@ -16,10 +19,13 @@
 public class PetController {
 
     private final PetService petService;
+    private final PetRepository petRepository;
 
     @Autowired
-    public PetController(PetService petService) {
+    public PetController(PetService petService,
+                         PetRepository petRepository) {
         this.petService = petService;
+        this.petRepository = petRepository;
     }
path: src/main/java/se/storkforge/petconnect/controller/PetController.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove unused constructor parameter**

The `petRepository` parameter is injected but never used in this controller. This violates the dependency injection principle of only injecting what you need.

Simplify the constructor:

```diff
     @Autowired
-    public PetController(PetService petService,
-                         PetRepository petRepository) {
+    public PetController(PetService petService) {
         this.petService = petService;
-        this.petRepository = petRepository;
     }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    @Autowired
    public PetController(PetService petService) {
        this.petService = petService;
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -67,4 +71,32 @@ public void deletePet(Long id) {
         petRepository.deleteById(id);
         logger.info("Pet deleted: {}", id);
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        logger.info("Uploading profile picture to pet with ID: {}", id);
+        Optional<Pet> pet = petRepository.findById(id);
+        if (pet.isEmpty()) {
+            logger.error("Pet not found with ID: {}", id);
+            throw new PetNotFoundException("Pet with id " + id + " not found");
+        }
+
+        if (pet.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(pet.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        pet.get().setProfilePicturePath(filename);
+        petRepository.save(pet.get());
+    }
+
+    public Resource getProfilePicture(Long id) {
+        Optional<Pet> pet = petRepository.findById(id);
+        if (pet.isEmpty()) {
+            logger.error("Pet not found with ID: {}", id);
+            throw new PetNotFoundException("Pet with id " + id + " not found");
+        }
+        String filename = pet.get().getProfilePicturePath();
+        return fileStorageService.loadFile(filename);
+
+    }
path: src/main/java/se/storkforge/petconnect/service/PetService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing @Transactional annotation and null check for profile picture path**

This method should be annotated with @Transactional(readOnly = true) like other read methods. Also, there's no check if the profile picture path is null before trying to load the file.

```diff
+    @Transactional(readOnly = true)
     public Resource getProfilePicture(Long id) {
         Optional<Pet> pet = petRepository.findById(id);
         if (pet.isEmpty()) {
             logger.error("Pet not found with ID: {}", id);
             throw new PetNotFoundException("Pet with id " + id + " not found");
         }
         String filename = pet.get().getProfilePicturePath();
+        if (filename == null) {
+            logger.error("Profile picture path is null for pet ID: {}", id);
+            throw new RuntimeException("Pet does not have a profile picture");
+        }
         return fileStorageService.loadFile(filename);
     }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    @Transactional(readOnly = true)
    public Resource getProfilePicture(Long id) {
        Optional<Pet> pet = petRepository.findById(id);
        if (pet.isEmpty()) {
            logger.error("Pet not found with ID: {}", id);
            throw new PetNotFoundException("Pet with id " + id + " not found");
        }
        String filename = pet.get().getProfilePicturePath();
        if (filename == null) {
            logger.error("Profile picture path is null for pet ID: {}", id);
            throw new RuntimeException("Pet does not have a profile picture");
        }
        return fileStorageService.loadFile(filename);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -67,4 +71,32 @@
         petRepository.deleteById(id);
         logger.info("Pet deleted: {}", id);
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        logger.info("Uploading profile picture to pet with ID: {}", id);
+        Optional<Pet> pet = petRepository.findById(id);
+        if (pet.isEmpty()) {
+            logger.error("Pet not found with ID: {}", id);
+            throw new PetNotFoundException("Pet with id " + id + " not found");
+        }
+
+        if (pet.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(pet.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        pet.get().setProfilePicturePath(filename);
+        petRepository.save(pet.get());
+    }
path: src/main/java/se/storkforge/petconnect/service/PetService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing @Transactional annotation and null check**

This method should be annotated with @Transactional to ensure consistency between file operations and database updates. Also, there's no null check for the file parameter before attempting to store it.

```diff
+    @Transactional
     public void uploadProfilePicture(Long id, MultipartFile file) {
         logger.info("Uploading profile picture to pet with ID: {}", id);
+        if (file == null || file.isEmpty()) {
+            logger.error("File is null or empty for pet ID: {}", id);
+            throw new IllegalArgumentException("File cannot be null or empty");
+        }
+        
         Optional<Pet> pet = petRepository.findById(id);
         if (pet.isEmpty()) {
             logger.error("Pet not found with ID: {}", id);
             throw new PetNotFoundException("Pet with id " + id + " not found");
         }

         if (pet.get().getProfilePicturePath() != null) {
-            fileStorageService.delete(pet.get().getProfilePicturePath());
+            try {
+                fileStorageService.delete(pet.get().getProfilePicturePath());
+            } catch (RuntimeException e) {
+                logger.warn("Failed to delete old profile picture: {}", e.getMessage());
+                // Continue with upload even if delete fails
+            }
         }

         String filename = fileStorageService.store(file);
         pet.get().setProfilePicturePath(filename);
         petRepository.save(pet.get());
     }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    @Transactional
    public void uploadProfilePicture(Long id, MultipartFile file) {
        logger.info("Uploading profile picture to pet with ID: {}", id);
        if (file == null || file.isEmpty()) {
            logger.error("File is null or empty for pet ID: {}", id);
            throw new IllegalArgumentException("File cannot be null or empty");
        }
        
        Optional<Pet> pet = petRepository.findById(id);
        if (pet.isEmpty()) {
            logger.error("Pet not found with ID: {}", id);
            throw new PetNotFoundException("Pet with id " + id + " not found");
        }

        if (pet.get().getProfilePicturePath() != null) {
            try {
                fileStorageService.delete(pet.get().getProfilePicturePath());
            } catch (RuntimeException e) {
                logger.warn("Failed to delete old profile picture: {}", e.getMessage());
                // Continue with upload even if delete fails
            }
        }

        String filename = fileStorageService.store(file);
        pet.get().setProfilePicturePath(filename);
        petRepository.save(pet.get());
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -124,4 +127,28 @@ private boolean isInvalidEmail(String email) {
         Matcher matcher = emailPattern.matcher(email);
         return !matcher.matches();
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        Optional<User> user = userRepository.findById(id);
+        if (user.isEmpty()) {
+            throw new UserNotFoundException("User with id " + id + " not found");
+        }
+
+        if (user.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(user.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        user.get().setProfilePicturePath(filename);
+        userRepository.save(user.get());
+    }
+
+    public Resource getProfilePicture(Long id) {
+        Optional<User> user = userRepository.findById(id);
+        if (user.isEmpty()) {
+            throw new UserNotFoundException("User with id " + id + " not found");
+        }
+        String filename = user.get().getProfilePicturePath();
+        return fileStorageService.loadFile(filename);
+    }
path: src/main/java/se/storkforge/petconnect/service/UserService.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle null profile picture path case.**

The method doesn't check if the profile picture path is null before trying to load it, which could cause a NullPointerException if the user doesn't have a profile picture.

```diff
public Resource getProfilePicture(Long id) {
    Optional<User> user = userRepository.findById(id);
    if (user.isEmpty()) {
        throw new UserNotFoundException("User with id " + id + " not found");
    }
    String filename = user.get().getProfilePicturePath();
+    if (filename == null) {
+        throw new IllegalStateException("User does not have a profile picture");
+    }
    return fileStorageService.loadFile(filename);
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    public Resource getProfilePicture(Long id) {
        Optional<User> user = userRepository.findById(id);
        if (user.isEmpty()) {
            throw new UserNotFoundException("User with id " + id + " not found");
        }
        String filename = user.get().getProfilePicturePath();
        if (filename == null) {
            throw new IllegalStateException("User does not have a profile picture");
        }
        return fileStorageService.loadFile(filename);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 80e16dc to 9462128
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -124,4 +127,28 @@
         Matcher matcher = emailPattern.matcher(email);
         return !matcher.matches();
     }
+
+    public void uploadProfilePicture(Long id, MultipartFile file) {
+        Optional<User> user = userRepository.findById(id);
+        if (user.isEmpty()) {
+            throw new UserNotFoundException("User with id " + id + " not found");
+        }
+
+        if (user.get().getProfilePicturePath() != null) {
+            fileStorageService.delete(user.get().getProfilePicturePath());
+        }
+
+        String filename = fileStorageService.store(file);
+        user.get().setProfilePicturePath(filename);
+        userRepository.save(user.get());
+    }
path: src/main/java/se/storkforge/petconnect/service/UserService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for null or empty file parameters.**

The method should validate that the file parameter is not null before proceeding with operations, similar to how the FileStorageService validates files.

```diff
public void uploadProfilePicture(Long id, MultipartFile file) {
+    if (file == null) {
+        throw new IllegalArgumentException("File cannot be null");
+    }
    Optional<User> user = userRepository.findById(id);
    if (user.isEmpty()) {
        throw new UserNotFoundException("User with id " + id + " not found");
    }

    if (user.get().getProfilePicturePath() != null) {
        fileStorageService.delete(user.get().getProfilePicturePath());
    }

    String filename = fileStorageService.store(file);
    user.get().setProfilePicturePath(filename);
    userRepository.save(user.get());
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    public void uploadProfilePicture(Long id, MultipartFile file) {
        if (file == null) {
            throw new IllegalArgumentException("File cannot be null");
        }
        Optional<User> user = userRepository.findById(id);
        if (user.isEmpty()) {
            throw new UserNotFoundException("User with id " + id + " not found");
        }

        if (user.get().getProfilePicturePath() != null) {
            fileStorageService.delete(user.get().getProfilePicturePath());
        }

        String filename = fileStorageService.store(file);
        user.get().setProfilePicturePath(filename);
        userRepository.save(user.get());
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            Files.copy(file.getInputStream(), destination);
path: src/main/java/se/storkforge/petconnect/service/FileStorageService.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add path traversal protection.**

Ensure the resolved path is within the upload directory to prevent path traversal attacks.

```diff
//Save file
Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+ if (!destination.startsWith(this.root.toAbsolutePath())) {
+     throw new RuntimeException("Invalid path outside upload directory.");
+ }
Files.copy(file.getInputStream(), destination);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
            //Save file
            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
            if (!destination.startsWith(this.root.toAbsolutePath())) {
                throw new RuntimeException("Invalid path outside upload directory.");
            }
            Files.copy(file.getInputStream(), destination);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            Files.copy(file.getInputStream(), destination);
+
+            LOG.info("Stored file: {}", destination);
+            return filename;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to store file.", e);
+        }
+    }
+
+    private String generateFileName(String contentType) {
+        String filename = UUID.randomUUID() + "." + contentType.split("/")[1];
+
+        //if name is already taken
+        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+        if (Files.exists(destination)) {
+            filename = generateFileName(contentType);
+        }
+
+        return filename;
+    }
+
+
+    public void delete(String filename) {
+        try {
+            Path file = root.resolve(filename);
+            Files.deleteIfExists(file);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not delete file: " + filename, e);
+        }
+    }
path: src/main/java/se/storkforge/petconnect/service/FileStorageService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add null check for filename parameter.**

The delete method should validate that the filename parameter is not null before proceeding with operations.

```diff
public void delete(String filename) {
+    if (filename == null) {
+        LOG.warn("Attempted to delete a null filename");
+        return;
+    }
    try {
        Path file = root.resolve(filename);
        Files.deleteIfExists(file);
+        LOG.info("Deleted file: {}", filename);
    } catch (IOException e) {
        throw new RuntimeException("Could not delete file: " + filename, e);
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    public void delete(String filename) {
        if (filename == null) {
            LOG.warn("Attempted to delete a null filename");
            return;
        }
        try {
            Path file = root.resolve(filename);
            Files.deleteIfExists(file);
            LOG.info("Deleted file: {}", filename);
        } catch (IOException e) {
            throw new RuntimeException("Could not delete file: " + filename, e);
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            Files.copy(file.getInputStream(), destination);
+
+            LOG.info("Stored file: {}", destination);
+            return filename;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to store file.", e);
+        }
+    }
path: src/main/java/se/storkforge/petconnect/service/FileStorageService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use try-with-resources for file operations to prevent resource leaks.**

The file input stream should be closed automatically using try-with-resources to ensure proper resource cleanup.

```diff
public String store(MultipartFile file) {
    try {
        //Validation
        if (file.isEmpty()) {
            throw new RuntimeException("Unable to store empty file.");
        }

        if (file.getSize() > maxFileSize) {
            throw new RuntimeException("File size exceeds the allowed limit.");
        }

        String contentType = file.getContentType();
        if (contentType == null || !allowedTypes.contains(contentType)) {
            throw new RuntimeException("File type not allowed.");
        }

        //Generate unique filename
        String filename = generateFileName(contentType);

        //Save file
        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
-       Files.copy(file.getInputStream(), destination);
+       try (var inputStream = file.getInputStream()) {
+           Files.copy(inputStream, destination);
+       }

        LOG.info("Stored file: {}", destination);
        return filename;
    } catch (IOException e) {
        throw new RuntimeException("Failed to store file.", e);
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    public String store(MultipartFile file) {
        try {
            //Validation
            if (file.isEmpty()) {
                throw new RuntimeException("Unable to store empty file.");
            }

            if (file.getSize() > maxFileSize) {
                throw new RuntimeException("File size exceeds the allowed limit.");
            }

            String contentType = file.getContentType();
            if (contentType == null || !allowedTypes.contains(contentType)) {
                throw new RuntimeException("File type not allowed.");
            }

            //Generate unique filename
            String filename = generateFileName(contentType);

            //Save file
            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
            try (var inputStream = file.getInputStream()) {
                Files.copy(inputStream, destination);
            }

            LOG.info("Stored file: {}", destination);
            return filename;
        } catch (IOException e) {
            throw new RuntimeException("Failed to store file.", e);
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,114 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            Files.copy(file.getInputStream(), destination);
+
+            LOG.info("Stored file: {}", destination);
+            return filename;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to store file.", e);
+        }
+    }
+
+    private String generateFileName(String contentType) {
+        String filename = UUID.randomUUID() + "." + contentType.split("/")[1];
+
+        //if name is already taken
+        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+        if (Files.exists(destination)) {
+            filename = generateFileName(contentType);
+        }
+
+        return filename;
+    }
+
+
+    public void delete(String filename) {
+        try {
+            Path file = root.resolve(filename);
+            Files.deleteIfExists(file);
+        } catch (IOException e) {
+            throw new RuntimeException("Could not delete file: " + filename, e);
+        }
+    }
+
+    //Does not return the actual file, just the path to the file
+    public Resource loadFile(String filename) {
+        try {
+            Path file = root.resolve(filename);
+            Resource resource = new UrlResource(file.toUri());
+
+            if (resource.exists() || resource.isReadable()) {
+                return resource;
+            } else {
+                throw new RuntimeException("Could not read file: " + filename);
+            }
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("Could not read file: " + filename, e);
+        }
+    }
path: src/main/java/se/storkforge/petconnect/service/FileStorageService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Correct misleading comment and improve file existence check.**

The comment on line 99 is misleading, and the conditional check on line 105 is using logical OR (||) when it should use logical AND (&&).

```diff
- //Does not return the actual file, just the path to the file
+ //Returns a Resource representing the file
public Resource loadFile(String filename) {
    try {
        Path file = root.resolve(filename);
        Resource resource = new UrlResource(file.toUri());

-       if (resource.exists() || resource.isReadable()) {
+       if (resource.exists() && resource.isReadable()) {
            return resource;
        } else {
            throw new RuntimeException("Could not read file: " + filename);
        }
    } catch (MalformedURLException e) {
        throw new RuntimeException("Could not read file: " + filename, e);
    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    //Returns a Resource representing the file
    public Resource loadFile(String filename) {
        try {
            Path file = root.resolve(filename);
            Resource resource = new UrlResource(file.toUri());

            if (resource.exists() && resource.isReadable()) {
                return resource;
            } else {
                throw new RuntimeException("Could not read file: " + filename);
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException("Could not read file: " + filename, e);
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T07:48:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,123 @@
+package se.storkforge.petconnect.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+    static final Logger LOG = LoggerFactory.getLogger(FileStorageService.class);
+
+    private Path root;
+
+    //all these values are edited in the application.properties
+    @Value("${file.upload-dir}")
+    private String uploadDir;
+
+    @Value("${file.max-size}")
+    private long maxFileSize;
+
+    @Value("${file.allowed-types}")
+    private List<String> allowedTypes;
+
+    @PostConstruct //Runs after properties
+    private void init() {
+        this.root = Paths.get(uploadDir);
+        try{
+            Files.createDirectories(root);  // Creates the upload directory if it doesn't exist
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create directory", e);
+        }
+
+    }
+
+    public String store(MultipartFile file) {
+        try {
+            //Validation
+            if (file.isEmpty()) {
+                throw new RuntimeException("Unable to store empty file.");
+            }
+
+            if (file.getSize() > maxFileSize) {
+                throw new RuntimeException("File size exceeds the allowed limit.");
+            }
+
+            String contentType = file.getContentType();
+            if (contentType == null || !allowedTypes.contains(contentType)) {
+                throw new RuntimeException("File type not allowed.");
+            }
+
+            //Generate unique filename
+            String filename = generateFileName(contentType);
+
+            //Save file
+            Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+            if (!destination.startsWith(this.root.toAbsolutePath())) {
+                 throw new RuntimeException("Invalid path outside upload directory.");
+            }
+            try (var inputStream = file.getInputStream()) {
+                Files.copy(inputStream, destination);
+            }
+            LOG.info("Stored file: {}", destination);
+            return filename;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to store file.", e);
+        }
+    }
+
+    private String generateFileName(String contentType) {
+        String filename = UUID.randomUUID() + "." + contentType.split("/")[1];
+
+        //if name is already taken
+        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+        if (Files.exists(destination)) {
+            filename = generateFileName(contentType);
+        }
+
+        return filename;
+    }
path: src/main/java/se/storkforge/petconnect/service/FileStorageService.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve filename generation to avoid potential stack overflow.**

The `generateFileName` method uses recursion to handle filename collisions, which could lead to a stack overflow if many collisions occur. Using a loop would be more robust.

```diff
    private String generateFileName(String contentType) {
-        String filename = UUID.randomUUID() + "." + contentType.split("/")[1];
+        // Validate content type format
+        String[] parts = contentType.split("/");
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Invalid content type format: " + contentType);
+        }
+        
+        String extension = parts[1];
+        String filename;
+        Path destination;
+        
+        do {
+            filename = UUID.randomUUID() + "." + extension;
+            destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
+        } while (Files.exists(destination));
+        
+        return filename;
-        //if name is already taken
-        Path destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
-        if (Files.exists(destination)) {
-            filename = generateFileName(contentType);
-        }
-
-        return filename;
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    private String generateFileName(String contentType) {
        // Validate content type format
        String[] parts = contentType.split("/");
        if (parts.length != 2) {
            throw new IllegalArgumentException("Invalid content type format: " + contentType);
        }
        
        String extension = parts[1];
        String filename;
        Path destination;
        
        do {
            filename = UUID.randomUUID() + "." + extension;
            destination = this.root.resolve(Paths.get(filename)).normalize().toAbsolutePath();
        } while (Files.exists(destination));
        
        return filename;
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T08:29:40Z
author_association: NONE
diff_hunk: @@ -69,4 +75,18 @@ public ResponseEntity<Void> deletePet(@PathVariable Long id) {
             return ResponseEntity.notFound().build();
         }
     }
+
+    @PostMapping("/{id}/PFP")
+    public ResponseEntity<String> uploadPetProfilePicture(
+            @PathVariable Long id, @RequestParam("file") MultipartFile file) {
+        petService.uploadProfilePicture(id, file);
+        return ResponseEntity.ok().build();
+    }
path: src/main/java/se/storkforge/petconnect/controller/PetController.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent API path naming and missing content type in response**

Two issues with this endpoint:
1. "PFP" is not a standard RESTful naming convention. Profile pictures are a resource and should follow REST conventions.
2. The method returns `ResponseEntity<String>` but doesn't include any actual content in the response body.

```diff
-    @PostMapping("/{id}/PFP")
+    @PostMapping("/{id}/profile-picture")
     public ResponseEntity<String> uploadPetProfilePicture(
             @PathVariable Long id, @RequestParam("file") MultipartFile file) {
         petService.uploadProfilePicture(id, file);
-        return ResponseEntity.ok().build();
+        return ResponseEntity.ok("Profile picture uploaded successfully");
     }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
    @PostMapping("/{id}/profile-picture")
    public ResponseEntity<String> uploadPetProfilePicture(
            @PathVariable Long id, @RequestParam("file") MultipartFile file) {
        petService.uploadProfilePicture(id, file);
        return ResponseEntity.ok("Profile picture uploaded successfully");
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-31T12:33:40Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-30T07:45:25Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
Multiple components have been updated to support profile picture management for pets and users. New endpoints were added in the controllers, and corresponding methods were implemented in the service layer. The entities now include a new field to store profile picture paths. A new file storage service was introduced to handle file operations such as storing, deleting, and retrieving files. Configuration properties for file handling were added, and associated unit tests were implemented. Additionally, a previously commented-out security test dependency was activated in the build configuration.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `pom.xml` | Uncommented the `spring-security-test` dependency for testing. |
| `src/main/java/se/storkforge/petconnect/controller/PetController.java`<br>`src/main/java/se/storkforge/petconnect/controller/UserController.java` | Added POST and GET endpoints at `/{id}/PFP` for uploading and retrieving profile pictures. Updated `PetController` constructor to include `PetRepository`. |
| `src/main/java/se/storkforge/petconnect/entity/Pet.java`<br>`src/main/java/se/storkforge/petconnect/entity/User.java` | Added a private `profilePicturePath` field with its corresponding getter and setter methods. |
| `src/main/java/se/storkforge/petconnect/service/FileStorageService.java`<br>`src/main/java/se/storkforge/petconnect/service/PetService.java`<br>`src/main/java/se/storkforge/petconnect/service/UserService.java` | Introduced `FileStorageService` for file operations; updated `PetService` and `UserService` by injecting the service and adding methods to upload and retrieve profile pictures. |
| `src/main/resources/application.properties` | Added new file storage properties: `file.upload-dir`, `file.max-size`, and `file.allowed-types`. |
| `src/test/java/se/storkforge/petconnect/service/FileStorageServiceTest.java` | Added tests for the `FileStorageService` covering profile picture upload, deletion, and retrieval. |

## Suggested reviewers
- MarcGust

## Poem
> I hopped through lines of code with glee,  
> Embedding pictures as sprightly as can be.  
> Endpoints and services join the fun,  
> Storing files until the job is done.  
> With tests and changes in a joyous parade,  
> This rabbit cheers for every update made! 🐰🎉

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAMRJqbH8eCnwAM3hInngxJI8zAEYABmjIAHc0ZAZ/aii5MNgPbERKSAAheCIieDJqeHwsTHpUDEcBSiiAZgAWDRhG6zsGTEhx9AYGaRb6agaPNPwffDL4DCJIAFUbABkuAG1YXFxuRA4Aejfu3FhsAQ0mZhvRAECgAawOFFIbysJFwAGEBuQxG9uN4vG8ZgBdAAUj2erw+Xx+fwBQJB4PwkJI0NhCIwSNwKLRGOmAEo5u5IEpcNovMhePgJPAlMMMLQcrUGrsGLBMKRkM4PONTudmGglGF8ChmNxImwMKE0JA0glcHk/CQvLV6AQUulMh5uDkzf5EBpzJYEax2MgHE4XEYAJJYb4eAH63C+0TiAb8NJ7Tw+C1BEIAGnanW6vRjWFlyFO4uWNBtstCofkipQIYWAs2iGQ6UgatBKoTDi6ISiMrl0g5jWSlYwWvV4pzaF84Z9/D44sQDGaiH6GH5qSFIqrCcnBqjYiX7oMFksAFEAB5IcRnfjCaOSaT2Rxqlya1ZNbi0a1udSRLjxRLJAUZFkTq5MkhRFJAkAGAAcmMlBcDMEEGBcLQUFwHRdD0GB9LGkEGPoxjgFAZD0I2aB4IQpDkFQxYKN6BpcLwV4iLuUgyPITBKFQqjqFoOj4SYUBwKgqArGRBDEGQyg0VunB+GgZT3v68j1BxyjcZo2i6GAhgEaYBjcCwGgnswXgcAY0QWQelgAIKBhJVHWopj7yI23ZnNIbgLG58obqGkAAAYGcwRkmf5xoOlWEiHFICYjuoS5xugXIkNwxFkAw8gQgFiC8CqYAtPOFDqLIYA0MC/l9o6/hCvgzReOxLARnQYC1aEAhePgDCgolqmQHmL5kJA2AYDJdDps2rZ+f5OVFWc+WiEkxWlSEYVKKlYrpRWLEkH2qBrWlI3yKcDBeNgSjIH5RCpNg3CQIGAAiAWUkQGgzSqaRUGwZSUqCr0LUVuCyP56Z+c44hpGgYh3Y9025XNBWLYDy3lemQwoL6TCpfQWVlReL3zB4grKEmwIUNgIGE/Gfn7RtI09Mg/hqqcyDDd5Y3lOosANCJtCjglZCynTl5+bjrbLNwKiZPFd7+FaNG2gAynD5wK/9xX7h6kDWV4NDUUuF1atTohWnrAwNvGJAngZFA0ZSnjtTkkDsNLiBGFA90pQdGXoLzdBcAYEFQP5wA00oh16GFJzfAFV21dwga0GF/nPa9ysfWgX0/X9hXFcDAVg/AENiAnSdvfDatI7jedo9NmMkEnVcboFhnGV4/nmZZeF6YgFAMG8TMYG8QhoBIaBAtSwI/RCUKpbgTD0tGbzz7gqQ+JQNLwgMK+HJEFAaMPo9mRZ0RWVrdmUVJUR+s5PWC/KnkeP5MKbwaq+72FJ2VMg/XjINN3vvLLUx1ToanIApNIrMxxeGKsaO2aosLdEvLPO0gFHTOjyG6LWkAwEpEkJKNgzBxgUHTIFWENgUr4EXCCIG6Yf4kEGiOKIto/Kf3rOmMoCAZToyrPFcc8AABeTDYDXSIFzFY/9HLz1JuTEEUpQhDgUpDTY3BIyrHwNHJ+sJVYUCFJsMKNdn7kIMlQykQN0D8mcBnWElBMFuG+tgkgCliIGVOKo2UMVf5YFcakWg5M/YGAKHMayAUrAAHkFbQDCs4/Arj0ChH8m8AA3sKAAvtCWIVgwpZRuh1dUrYjSzwAOQrntEBdB/h2GcLERsFKqijT+QALLeHEBLG2sQHT6IVDwSxbBdboDFIwccfIEz+RyfgdUVhUioKsOU+uTZYSwEUPwasj9Z7aN0fXd0AAmIJAUADiR5IlOzFC4g0cSApJNSekzJsC+D+BXj0IUl4CmwmKSgiKwEXQkEqTkLm9ykjLiSv5chiBaq9zmYIZihoBnLB8BdBY/lSC4EmaUkgMyKZhV6YskiKyAprMoBsiqD8WiMDvneAWmBNgjOfnSbea8KBhXqEM44rZ1DPkFrQICUyPmzOQFlWeiAQb9hIPUS2EsxSstURAkaUD1aa21rrbCgLmELCUJ/U2gLGxispLbPgqIHYMGOeEembtIANIWUsxcRAsJfJ9iKLggVfjQMNSCgyy4SBHgNMVYACsHlnD0ENXU4zaDPxRdM2Z2IAACVhqCwAAGrOHgCoLI1wBjnGFOmSN5CUzAhjZ9bE0RUHRFZGa5p8BWm4HaVkVBrIwqnEYFaesITaRbzfpQOtWBpq937toQeB8x4tDJFPZ61JZ7zwZEvVtO9140qnfS/eI80DtygOa74lrOg2vNIw2gDr9XOsgK6s2HqvWA2ACCsFmwA1ItDdyyI6KvlRpjd8BNRVk0eFTZeYUtaNysOQJol+dL35Nx7n3AeQ9F3jyHWCaeo7YTjsXsvNtFAN60qQwu0ey7IB0mkWIO2VrN3JAkcWXdTrHaztftOig2Jn7rJyI6LRBK6Pfvrb+5tAGkMduyt2sD/bIOT2gyOlEcHEQIbnbvFDYnKDoaXZAQASYR4tI4a8jgHKDUYYzoujPB1MbPTIYihJinyzyMZQ9QpjmNYFY/+1DlHONdtA728Do8+Pkhg0JueInkSIcoxJij87+3t2Pm7bu3GHO8cHfxikVI3Pwc85J5DyFKDWb84uo+lkLBn3spfeg19nAuXjGzV2BhOQFarNvXxVLcD2JwdE1xBsRkJYoEloDrGsocugUg2E7yykU0wZyDIFBgR2virGESITwlHKxUstU3AsbPgSck2gaSrAZLzhw356AjhlBZihOrYz1RJWQQBHlFNdrIEmzaNAoI7z1I/emxO5iDuxsgKPV97UPBo3qU0nW5awZVv2B0h7Rp/A5tCK0qxut0ynCii2YWCK9shtvWi2ZmKLUkSpnDlCtG9FzEDKEf5FBAX1MPe6z14RZA+r9UQCOHNo4rAABLQGgFYewPIzTm0gFsoo4FsShIANIlpurGBwNT6wQN8PD/cnICoDB2LzYbXiun+QORN1HTY0AzaYVqebVzluZJ+Vw+5RUSCsQTIdxH2QKbmNBQwJNNEo5cz8jlUQhcej0GaK0B6p35lrp2HyQ2l3rsBVuyge7lRHvRxe0mt7/T6D48JwFYnLRSfevPUkS9edl69smgsV0F79hwcaNjVIzARnu4oFj+ubzEWwjDQ6e9eQUc+9Rh1S89v+mQAZ0zlniR2ec+53z9kxKwxkuQBSkaHg/LSt3AMPhgNEpTQa019tqx5C9AdrD18uSdgDMN488ciUzeoot18t0Rh5U6ykvrZ8Rt1VKvZ9qm2UQ7Z7sds7cQHk8JmtV9ukjBqD3SDdSTxPXJ19Vmip0DS3xvVRXr38EfVjRfSjxTTTRD0zWzWCFzR6QLSLRLS+xaV+wihrU40s0Xzi1sxAx7VOEcwHQnhc0EzHQ80ZC83pTeBIN813mk0w1XWxTtT9gUz/0T2PTJzPWkDzyvRrxr0RxgJIDgOfUTTfUgGDy/SIMbT/VYJUwZWAxC0oLCxoOHSi3oIXlizYPXjULQ38w7hPi7jACMHIJ4wg3C1oP0OE0MMZDf1kA3mk1SxPnS1sky2oivgfFy1vh7EK2KxH1Kx8T8SShwVylHhoHCktHoECkkNmSfVgDChVUfmfg/hUP1y5mEgNDShtC1H4wnxrCe1Ii01wDeSO26y+TmARAoFdDdXFDOHqhBnsRf0NXOwVAHF9h3RjgkOgNSNjWxG/Syl32N2zw8FHlOkpjxRSIpjSOrgGWmiGPDSWNGNANbFqKR02O+HGLw1hDxhQFUVmOCEqhJTihzFHwwEFgqwRWyIbS/hXw2w6hOA3zVwQRIAjESgKXN0+XNAlm+CFQmFFStiGElT5UgSXFn1kA1lPgVUvzNmv1VWNksRuMSgf11Xtn3TfxNU/3aUSJ4IGOSLwXiO2KQUWK+WWJ/RULYzIK0L7XsN0IEycPcxcLeDcI8PMJXW/36N/33UpPOGvWpLyDSLGOUJeP/UZPs20JZKg0ixnmcInW5Ofg4M/y4KWR/z4P3SimFHsHWLrxGO+GxGFK6z2JpNjXM2eKbRlM0LlOZKcwcL0OVI5NVOAJ5MXQC07ggGsIMFsNCwVIi1cwMI9LJxYJQk8IsMRPPkkn8Oy0CKfFchHwflJRCIiMUCiKNBiKKjiP+2JOSOGP2PSNRMfgaxyK/jyO4VcSKOfFKNNwqPRyaBQhqIBNmSCRb3ODb2+B5j5gGA6K1C6O92xV6PewFMGORTFP8AlMOLuVhCNyeXOD8nOPmKLI2KtO+BWKSJaCnOLM3NgDNMpwtKkLSLnMNKeChOe3HAuIJhJWYEUBdyLCvzHwePLJQkrKbSZU20lWfHgTQFIHb2uNhN8GoAeQEDwA8FlkcltDLwtNPzPx8Ivw1TqxvxNjvyxKth1Sfz1UUyNRdlNSJK8Fl3tQWPJI8HNN2NPNjSlLtIrIdIoKdOoMVNDJVMXm5Iaw1L5J9xJMFMdnNNFP3PFNGJtOII/IYrsOdNZKVNg3dPYs9M4t5K/x4p1MdT/31OyyNLvRNMPMounLRWtNotUPEvrTs0YqoOc1dNkpi1cIUqjPMMCysJsKZIspdLZLdJsvHg002DeD+19UpAApIAr2jMCx8LjIchohy2TPy1TIMGCRwUsz8pBECorzCjoUGm8SzM2FFBGRaFekcNIA0DDOjD+m8rmQli6kCq9xQg2S+MCr5Qinh0FRZwCtIFRh3wXL3y8HTDwCln4VbCVjALeVDk2irChQSltHngyCICBNSFShtnpnvC4TDz8nhy5HgH8FwxcHGjQDPGYEcHCiyEXEEXat93eKiFQTCFkFSj5WL1ihm2dSVTtHmvfwQuK3pOATOkD38mjUoRfhw1wEb24O+F2FOF4WgUEXhQ8H40CvWs2uoXTGqFNF/MLlOK5HwDvCHFCEtnPFOoaFSC2ySjJi9TYCdhPGURzBQHjHFHhtMUYBqEpohkyF6wRVKKBqWVmOFFqBZiDXVAuodDYlJVEBh3TWbIrGSB+JUVkEh2bI2vsAEQ8Etk2DoChrVz2oOugWYHUDxtRtDDluXnYCutSm4WZTKDoDmAuDdSWrrEQDF2vOgQASXEh1CAvn8K6WGngBTEOt6FJuaHyUuAuAenTEQBHjvEnwikyOSnw0chpujFMTxo6iIFVshhzBx3jEwCOnrHQPQCKhJV8TAO9r6iGHayIFRhmzmtfXiOJvEFJqVtqSv0rG+AJowElwRSUEiBoHZrOuOD5TtiNg7oSkbFQUFr1r4FQSwjYHTETsQXOCNAqAJ1ZXTuwTRG9ono8FQAFDXHZjRjax2PUWdn30DFCTJopv1kqgCi3z+y7otAeWN0D0urDyJxELT3rkRsaC6lbCynUFHzPGBHSnew6vVElmgUBjxqbpZUvDrpUSv11oWEuuWHpHURfHWBtpwotHVHPvHqsUgFdqesTtf2XAwTkSpqSiwdJvHBqFoFX1/sjFRgcQUg9q9rIfXuQFwZok2qSEXCkHqn3FCSkCoB8FBNtNuPFgcDljvEuphsAqn1lTnzbwBF4BIEaGXFvCdiaLtl3ueQGU5sdtjDYDciQGYAQqQsVUxMjrVXQsxK1SwsfxIlwr/3xI/ygDhHpNUqStaqCsYz0QkqDKkpYroLYuRBqro18odH8qoFIGCqUq1JIt4LUqFOPNKOxFwJ+zaQIIdFEvpP8ncYic8bKtlPMp0P8fZM8uCZ8pyZSq8Z2mif5NIvicdg0uSg7ukPNOYcyelIqciaqYKckuYpDICbkqCaqdCciHCcqbKq4uUu4NUuHNT3BUgEvodCPILraaMoCk6bycJR8flL8f6ZKYYK8o2RGaCuSq6YmZqZUonLJPzMgAEvjNqD+ygisWWbFi3nYGgGupIHabtI2dSu2aYssvcusoObKepF+aqY1Mcr9OcsdNcuktYsGcZFBY3iiZSxjLCr8Mciiry3TPclCMeO0zo0/O/jD08SbEfIyE1yrBOi+uNBhJnxAcyjgUwAAp2M612OPwwV2RwRGsOhIXBfydoVJfoUGH6LLOEbAa8jNhXhkQ0eFb/jfBguHBqRUW5ijDKo2zTUXA1D8hxuBFbH/WMwMyBjmDsS1BiLwp6KLo8RFczPKzifh1r20opmxEUNoHTBSYrT+29u/RrkEo3LyFdeQKUNNaKzh15oRyEv8GvplGFvzGbLtC3uxgihGBXrRnAcJpWH8kDDXiIHHGskhEcHYFPFPoGDrXjDZSQBxzx06rvsVyePqG/uhinvwAwi0bUdSD4HTeEQgdnrYygnUViFqjFBLfrrLZIb8mQVoAxuQCxrJvPDTtN06woYSCoaLq6Q5cBIqTRvbusVVsOHoC3ZbOmPWbCdOc2aJfPtzKPyPe4VDCwFKPdZjyXeqJKUutveBPyJ5oASiHrWpmoBUEqB2jTOrz3IDejZHKWUmJN3qQbaOlUSDvlq1pNnqgWdbZnvb0oE7efYzf9v/QHcrWHdoFHegfHdgfo1CBGCQYOGGloGraFq6njdN3bMt0/e4VGEEbQ7bdnqwCw7tm7YJrw5sGGhro9XJrHYwHLbRqrbuiXp0e6Wjj1dobRug7DprBY9tT9s+OybPY8dSt4f4aGVBJJRK1fLKKyMJe8dYyZTl0+OAZgVgojefdU6qItM5ddGb2QLbyRC2CCL474E0ZenP1Mav3MfRJQswuthxOHMccK24u4Ojs08VeI11LI0s+kL02MVM0MzIX02y9kG+b/Ro26f+bheKY8pBeGeK/Oe9Lk1S6U3S7U1wCNfy6qJa4RsgAFdqqkYvc2EK7Yz+dMsDJ2b6YKuBc5OReq42UmZid4vq+exiTdwjadctMDbdY9bLS9fSciH6//UG87WG4Bbcpkui0q7KpRYhYuemaudmefvmf9eNJdbdd26m6JdK6Kb2Yq4m6q/S8hd9OC1hY+7G9O++/O4a1RcPnRZsnCqyyciCJTJCJA/B5K9YyG5cqB6spB4nWReR5q4w3XYGiwCIypeOkpGtklBkZApgSyn/Iw7gs3d5W5ccWSnWjDgyn5d09ydSqFZkFte3XFakRla2urIURQdqXhX8EdB6WsQGwdQa3a9MTzifi/m+goGI5GkUHbTxp09GfPf09wmgCq2Z+HKtfcSVFtcyvtdJMdf0qDc/Sfc9fwOrQyf0VWIe+dYfWe4qgJgCht6jbmXOwY9BCY8ndXGFH5qyFTc49w/bezdzfzcLYjBI5zCk8rcwVxxvsXLU5bNaHqEQ5j77f8gawI6Hdo+T6XCk9WpQnRsxqQaU8Xar9aH6hWCU52I07yGdqab3eIdva0+XIRS68vZ9+vffdmTveUZav8Cfe7Zz4oDbJvbH7Y9QGJ5yv/Z5AECA9Dc5FA5W6kOvtU/dur7z8ejRljcY9uXnb/vHyFUE+eQCmL8HaI/L7I4Tbgqo9CBo7FHo7P+D4nYWDgub5ucP2saAANwkMsanfDNormE4k0xOpbSTou3+IL9LcSnZqmyjjyq1b2j9BPHd28b1At8v5D6CwATCSNz2+4I8OowC7F1Ww6VXjncUpS/ssAAgdRFzCta2gyADgf8HNVaC08fihtRsPT3b4edqWICMWO/V/5ZQOOvgYes+1b6Xg4Km1SkLQGMY2RkKGFMLrfisYWwbG0XPCrF1NSzdEu5oFfnxUNS48Nk2IeXnl2oRDQUICvbatYBV5KDPUqkPVE4LV4uDNeFAXbuYLe5o9AewZYHsVSGZg9Mcl3WrvJnqZmCwhZVSwXYOsF00y89g6Wo4PrCq91erg7pGkOcEa9OI6YQflSlQRjMzmGyHwTEK2b+DCmgQzHsEKRbDNfBmwGbrUzibDlGmfvcDtIXW6lpvsW3Z3jtzWZF9yhfgg7uj2qFAssei8HHkMMaFXdtSN3PCnMypTu9VusBL3nSWlINC5klQ3poCxO61DDmITTYX90sLQsAyLlXPC/UQBvB1cuoCUKnQFAvV6YXhWMpi0ipJkcWBWNMiVkt7Zl6GhpafFgCyikCPGCgDANNSSBPU/2CKG4Y9XuHcCFq0gLJA6D7CS9nqlAV6jnXHJ1NUEGgeHGABpqrYqk9gVKDbkpaq1Y6W1JlmPUaoRsFQf6Jqv5DAH+QcRaoE8PlAVqEj1su5VWqyPgD7US8kjBWgsz5FspbQ/kYoNMAAAcAAVgADsAANkZSyAyokAbEAUDNRtBWQYAmuDiNNrNRAYN1TkVwiUAZByAydTbBHwnyfNe6fAJqg9gABSVgI8Hsl0xQQXRz7PZIGFiDah6qcwAdmiIRHIAzayQRmETDsbktxQlLWgGAJYThFQUkQVDlInPDz4FgwFWMI2FDAkoEqAwcERqgvJ4wEKwXZEsqkNholNBV+axlFzQYxcT0BJKAFBGN7wi58qlHEXiIJGlcLh4KK4TCLuF7gHh6I+mJhgbEKR+xNsCsFcxZG7V2RgiHpg5k7F1hrhD1XsQMCKpNjBxn+YcQGObETjkReo2gKVGtGzjKC846QIuNuHPkVxo416j6ROEA83guMMrp93G7Y9hmBQkgNABCAhU0s0PV4QESUjBE8WRgeKsz1xjCNT2uvPTlUw/HlQCeZLH4dlV8is1u0D4jHuMP2GgswoFVUEFVXmCoBUeyAc6BuipagTJ89LLCIyxTGPw3xrvYYKolrJUAARNOB3AS1wD68M+vVCGneHtEXAMAbKaURf1FhaN6ADSTqC2FtA09RJrYXljbl7A+9QJqPEaCAkDy7kbokHbfFjUcht4fqbQEgBCA9SQx0i1ZMGuID4SQ0gUTxSFNGDoaNJRJjvNJpEG3JVhgQDA9nDrxOaQT8mz7Pbt03Pr+RWRflDkSQj3EfNDR2vVIOoiRGJFXJ1EzEReVsEGtyEaQSIACOgm4ALg4gPkESjDaojQJVrSsCaNOBRB7cUIsMCoTMgBI5gAAKkqmzxd+OlAjjBB8DVSuAQkC6CEGeyUAXcanXYGgDSB9J4c/tBnpbiygvJcAQjIabaiX6zskGUg+jg+XP5TRTRtANoLIBLhqTEohrBIS4AyJag487efAKR3ImudT+4gzAUIKl604+prQVahG33A7JIA1U+HH9iPBSAMAgYNIKEmIp/ZAwiAPZAMBIDNTcJbUwbD/3hQ9SgBY/BBsg3hxRA76XiBNooxqjNBC6tArvsWHo4HBNsXSRcMh2cDGh3i4rfrINlxjpgOBi0WHLsFD7IDJpT2RmL2mQDydepfSfwGAAGlnB9wkwKqZVLjx/ZAZrUsIO1LJmuhiGVM0fpbihlrBVOiZEXLbTRAVgrpfAZUPIIjbm07ooQUGSLIWBXBrglEzPo8iiAnj2OM0tEK3QnwhAWYGUoUV/AHEokiBJeayPWHRH2jnwOjSULqytjRgn8eAAENdgGRCzzOCgJotGBfDuJ+gfADMXAwig9dn2yCUFioK1hqCzGpYjwBYwxIVjtBVY8MTWONRONIAm4+Sa4yuZvjUpBTFCWML2GBM6h+TGMl3B0gCRjkaOHAOJDYZdhGo7ALgFQAUjYtXirglQGoA0h8RtIBgeueGHUAAB9YUIgDHnVQegZtWgGPOck2wtIdcwiMaFoCyjaA8ogQAAE5JR0wBgPvO3nSiCgaQaYAqNoBbJpR0oooHvLSDSjaA28goJKNoCTABAtABgPKJlHLzh5q87edMHlFbICgWyZ+bKLPknzJg68yYCQEvnHyCgkwbeRDEmBwKCgAgWUdvK2RILJgaQLZN/PrlpACgaAEgPfOWCSiigAgeBZKO3nyiGAko2UWQulHTBpgtAAoIwumAEL5RaALZNMHoWTAcF+EH+RAFoha1cAE85QdPKmKOI6AY84iMvKAA== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=storkforge/PetConnect&utm_content=34):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Biggienation
created_at: 2025-03-30T08:00:53Z
body: Making the suggested changes

author_association: CONTRIBUTOR
user: login: Lhlmlund
created_at: 2025-03-31T12:33:57Z
body: Great implementation, some suggestions I am moving over into a new issue/feature you could look at.
author_association: CONTRIBUTOR