From 788fa0ea4e16bc935dccd33408e658da979c0bf3 Mon Sep 17 00:00:00 2001
From: ZAYTOUN abdellatif <zaytoun.abdellatif@gmail.com>
Date: Thu, 13 Mar 2025 19:03:25 +0100
Subject: [PATCH] feat(): update partial for request ( procedure id).

---
 .../controller/RequestControllerV2.java       |  36 ++++++-
 .../beta/selexpert/exception/ErrorEnum.java   |   4 +
 .../beta/selexpert/mapper/RequestMapper.java  |  16 +++
 .../request/RequestUpdateRequest.java         |  21 ++++
 .../selexpert/service/RequestService.java     |  33 +++++-
 .../controller/RequestControllerV2Test.java   |  95 ++++++++++++++++-
 .../selexpert/mapper/RequestMapperTest.java   |  72 +++++++++++++
 .../request/RequestUpdateRequestTest.java     |  84 +++++++++++++++
 .../selexpert/service/RequestServiceTest.java | 100 +++++++++++++++++-
 9 files changed, 451 insertions(+), 10 deletions(-)
 create mode 100644 src/main/java/fr/gouv/beta/selexpert/mapper/RequestMapper.java
 create mode 100644 src/main/java/fr/gouv/beta/selexpert/request/RequestUpdateRequest.java
 create mode 100644 src/test/java/fr/gouv/beta/selexpert/mapper/RequestMapperTest.java
 create mode 100644 src/test/java/fr/gouv/beta/selexpert/request/RequestUpdateRequestTest.java

diff --git a/src/main/java/fr/gouv/beta/selexpert/controller/RequestControllerV2.java b/src/main/java/fr/gouv/beta/selexpert/controller/RequestControllerV2.java
index d1becdf..76916fc 100644
--- a/src/main/java/fr/gouv/beta/selexpert/controller/RequestControllerV2.java
+++ b/src/main/java/fr/gouv/beta/selexpert/controller/RequestControllerV2.java
@@ -3,6 +3,7 @@ package fr.gouv.beta.selexpert.controller;
 import fr.gouv.beta.selexpert.exception.HttpConstants;
 import fr.gouv.beta.selexpert.exception.model.ErrorResponse;
 import fr.gouv.beta.selexpert.request.RequestExpertUpdateRequest;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
 import fr.gouv.beta.selexpert.service.RequestService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Content;
@@ -25,7 +26,7 @@ public class RequestControllerV2 {
   private final RequestService requestService;
 
   @PatchMapping("{requestId}/experts/{expertId}")
-  @Operation(summary = "Update a request expert", description = "Update a request expert")
+  @Operation(summary = "Update a request expert", description = "Partially update a request expert")
   @ApiResponses(
       value = {
         @ApiResponse(
@@ -49,7 +50,7 @@ public class RequestControllerV2 {
             content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
       })
   @PreAuthorize(
-      "hasRole('ADMIN') or (hasAnyRole('MAGISTRATE', 'CLERK', 'ASSISTANT', 'ATTACHE') and @securityService.justiceUserMatchesRequest(#requestId))")
+      "hasAnyRole('MAGISTRATE', 'CLERK', 'ASSISTANT', 'ATTACHE') and @securityService.justiceUserMatchesRequest(#requestId)")
   public ResponseEntity<Void> updateRequestExpert(
       @PathVariable Integer requestId,
       @PathVariable Integer expertId,
@@ -58,4 +59,35 @@ public class RequestControllerV2 {
     return ResponseEntity.noContent().build();
   }
 
+  @PatchMapping("{requestId}")
+  @Operation(summary = "Update a request", description = "Partially update a request")
+  @ApiResponses(
+      value = {
+        @ApiResponse(
+            responseCode = HttpConstants.HTTP_NO_CONTENT,
+            description = HttpConstants.HTTP_NO_CONTENT_MESSAGE),
+        @ApiResponse(
+            responseCode = HttpConstants.HTTP_BAD_REQUEST,
+            description = HttpConstants.HTTP_BAD_REQUEST_MESSAGE,
+            content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+        @ApiResponse(
+            responseCode = HttpConstants.HTTP_FORBIDDEN,
+            description = HttpConstants.HTTP_FORBIDDEN_MESSAGE,
+            content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+        @ApiResponse(
+            responseCode = HttpConstants.HTTP_NOT_FOUND,
+            description = HttpConstants.HTTP_NOT_FOUND_MESSAGE,
+            content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
+        @ApiResponse(
+            responseCode = HttpConstants.HTTP_INTERNAL_SERVER_ERROR,
+            description = HttpConstants.HTTP_INTERNAL_SERVER_ERROR_MESSAGE,
+            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
+      })
+  @PreAuthorize(
+      "hasAnyRole('MAGISTRATE', 'CLERK', 'ASSISTANT', 'ATTACHE') and @securityService.justiceUserMatchesRequest(#requestId)")
+  public ResponseEntity<Void> updateRequest(
+      @PathVariable Integer requestId, @RequestBody @Valid RequestUpdateRequest request) {
+    requestService.updateRequest(requestId, request);
+    return ResponseEntity.noContent().build();
+  }
 }
diff --git a/src/main/java/fr/gouv/beta/selexpert/exception/ErrorEnum.java b/src/main/java/fr/gouv/beta/selexpert/exception/ErrorEnum.java
index 5f71724..539412f 100644
--- a/src/main/java/fr/gouv/beta/selexpert/exception/ErrorEnum.java
+++ b/src/main/java/fr/gouv/beta/selexpert/exception/ErrorEnum.java
@@ -23,6 +23,7 @@ public enum ErrorEnum implements IApiErrorDefinition {
   REQUEST_EXPERT_NOT_FOUND_BY_USER_ID(
       "request.expert.not.found",
       "No Request_Expert found for couple (requestId, userId)= (%s, %s)"),
+  REQUEST_NOT_FOUND("request.not.found", "No Request found for id %s"),
   REQUEST_EXPERT_DELETION_NOT_ALLOWED(
       "request.expert.change.status.not.allowed",
       "change to DELETED status is not allowed. Its only possible for Request_Experts in QUEUED state"),
@@ -32,6 +33,9 @@ public enum ErrorEnum implements IApiErrorDefinition {
   REQUEST_EXPERT_UPDATE_NOT_ALLOWED(
       "request.expert.update.not.allowed",
       "Request_expert update is not allowed. the request is archived or not in status OPEN/REFUSED"),
+  REQUEST_UPDATE_NOT_ALLOWED(
+      "request.update.not.allowed",
+      "Request update is not allowed. the request is archived or not in status OPEN"),
   EXPERT_NOT_SELECTABLE(
       "expert.selection.not.allowed",
       "Expert selection is not allowed for expert ID %s due to invalid status, missing email, or unavailability"),
diff --git a/src/main/java/fr/gouv/beta/selexpert/mapper/RequestMapper.java b/src/main/java/fr/gouv/beta/selexpert/mapper/RequestMapper.java
new file mode 100644
index 0000000..3360abb
--- /dev/null
+++ b/src/main/java/fr/gouv/beta/selexpert/mapper/RequestMapper.java
@@ -0,0 +1,16 @@
+package fr.gouv.beta.selexpert.mapper;
+
+import fr.gouv.beta.selexpert.model.Request;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+
+@Mapper(
+    componentModel = "spring",
+    uses = {JsonNullableMapper.class},
+    nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
+public interface RequestMapper {
+
+  void update(RequestUpdateRequest request, @MappingTarget Request entity);
+}
diff --git a/src/main/java/fr/gouv/beta/selexpert/request/RequestUpdateRequest.java b/src/main/java/fr/gouv/beta/selexpert/request/RequestUpdateRequest.java
new file mode 100644
index 0000000..93add4f
--- /dev/null
+++ b/src/main/java/fr/gouv/beta/selexpert/request/RequestUpdateRequest.java
@@ -0,0 +1,21 @@
+package fr.gouv.beta.selexpert.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.openapitools.jackson.nullable.JsonNullable;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class RequestUpdateRequest {
+
+  @Schema(description = "Request procedure id", implementation = String.class, example = "12/25/54")
+  @NotBlank
+  private JsonNullable<String> procedureId;
+}
diff --git a/src/main/java/fr/gouv/beta/selexpert/service/RequestService.java b/src/main/java/fr/gouv/beta/selexpert/service/RequestService.java
index 353c105..e9221c5 100644
--- a/src/main/java/fr/gouv/beta/selexpert/service/RequestService.java
+++ b/src/main/java/fr/gouv/beta/selexpert/service/RequestService.java
@@ -10,9 +10,12 @@ import fr.gouv.beta.selexpert.exception.EntityNotFoundException;
 import fr.gouv.beta.selexpert.exception.ErrorEnum;
 import fr.gouv.beta.selexpert.mapper.JsonNullableMapper;
 import fr.gouv.beta.selexpert.mapper.RequestExpertMapper;
+import fr.gouv.beta.selexpert.mapper.RequestMapper;
 import fr.gouv.beta.selexpert.model.*;
 import fr.gouv.beta.selexpert.repository.RequestExpertRepository;
+import fr.gouv.beta.selexpert.repository.RequestRepository;
 import fr.gouv.beta.selexpert.request.RequestExpertUpdateRequest;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
 import fr.gouv.beta.selexpert.util.DateUtils;
 import fr.gouv.beta.selexpert.util.RequestUtils;
 import jakarta.transaction.Transactional;
@@ -31,8 +34,11 @@ import org.springframework.stereotype.Service;
 public class RequestService {
 
   private final RequestExpertRepository requestExpertRepository;
+  private final RequestRepository requestRepository;
+
   private final ApplicationEventPublisher eventPublisher;
   private final RequestExpertMapper requestExpertMapper;
+  private final RequestMapper requestMapper;
   private final JsonNullableMapper jsonNullableMapper;
   private final RequestSchedulerProperties properties;
 
@@ -53,13 +59,13 @@ public class RequestService {
                       requestExpert.getSentOn(),
                       RequestUtils.getWeekdaysToReply(weekdaysToReply, requestExpert));
 
-
               if ((requestExpert.getExtraDaysNoRespUpdatedOn() == null
                       && deadlineExtenderThresholdHours
                           <= DateUtils.calculateWorkingHours(LocalDateTime.now(), deadline))
-                  || requestExpert.getExtraDaysNoRespUpdatedOn() != null && deadlineExtenderThresholdHours
-                      <= DateUtils.calculateWorkingHours(
-                          requestExpert.getExtraDaysNoRespUpdatedOn(), LocalDateTime.now())) {
+                  || requestExpert.getExtraDaysNoRespUpdatedOn() != null
+                      && deadlineExtenderThresholdHours
+                          <= DateUtils.calculateWorkingHours(
+                              requestExpert.getExtraDaysNoRespUpdatedOn(), LocalDateTime.now())) {
 
                 requestExpert.setExtraDaysNoResponse(
                     Optional.ofNullable(requestExpert.getExtraDaysNoResponse()).orElse(0) + 1);
@@ -202,4 +208,23 @@ public class RequestService {
                 new EntityNotFoundException(
                     ErrorEnum.REQUEST_EXPERT_NOT_FOUND_BY_USER_ID, requestId, userId));
   }
+
+  public void updateRequest(Integer requestId, RequestUpdateRequest updateRequest) {
+
+    Request request = fetchRequestById(requestId);
+
+    if (request.isArchived() || !RequestStatus.OPEN.equals(request.getStatus())) {
+      throw new BadRequestException(ErrorEnum.REQUEST_UPDATE_NOT_ALLOWED);
+    }
+
+    requestMapper.update(updateRequest, request);
+
+    requestRepository.save(request);
+  }
+
+  Request fetchRequestById(Integer requestId) {
+    return requestRepository
+        .findById(requestId)
+        .orElseThrow(() -> new EntityNotFoundException(ErrorEnum.REQUEST_NOT_FOUND, requestId));
+  }
 }
diff --git a/src/test/java/fr/gouv/beta/selexpert/controller/RequestControllerV2Test.java b/src/test/java/fr/gouv/beta/selexpert/controller/RequestControllerV2Test.java
index 13e8447..9d22522 100644
--- a/src/test/java/fr/gouv/beta/selexpert/controller/RequestControllerV2Test.java
+++ b/src/test/java/fr/gouv/beta/selexpert/controller/RequestControllerV2Test.java
@@ -7,7 +7,6 @@ import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -19,6 +18,7 @@ import fr.gouv.beta.selexpert.exception.BadRequestException;
 import fr.gouv.beta.selexpert.exception.EntityNotFoundException;
 import fr.gouv.beta.selexpert.exception.ErrorEnum;
 import fr.gouv.beta.selexpert.request.RequestExpertUpdateRequest;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
 import fr.gouv.beta.selexpert.service.RequestService;
 import fr.gouv.beta.selexpert.service.SecurityService;
 import fr.gouv.beta.selexpert.util.TestUtils;
@@ -148,4 +148,97 @@ class RequestControllerV2Test {
     verifyNoInteractions(requestService);
   }
 
+  @Test
+  @WithMockUser(username = "2", roles = "MAGISTRATE")
+  void updateRequest_success() throws Exception {
+    // GIVEN
+    when(securityService.justiceUserMatchesRequest(eq(1))).thenReturn(true);
+
+    // WHEN
+    ResultActions result = mockMvc.perform(TestUtils.patch("/api/v2/requests/1", Map.of()));
+
+    // THEN
+    result.andExpect(status().isNoContent());
+    verify(requestService, times(1)).updateRequest(eq(1), any(RequestUpdateRequest.class));
+  }
+
+  @Test
+  @WithMockUser(username = "2", roles = "MAGISTRATE")
+  void updateRequest_shouldReturnNotFound_whenEntityNotFoundException() throws Exception {
+    // GIVEN
+    when(securityService.justiceUserMatchesRequest(eq(1))).thenReturn(true);
+
+    doThrow(new EntityNotFoundException(ErrorEnum.REQUEST_NOT_FOUND, 1))
+        .when(requestService)
+        .updateRequest(eq(1), any(RequestUpdateRequest.class));
+
+    // WHEN
+    ResultActions result = mockMvc.perform(TestUtils.patch("/api/v2/requests/1", Map.of()));
+
+    // THEN
+    result.andExpect(status().isNotFound());
+    verify(requestService, times(1)).updateRequest(eq(1), any(RequestUpdateRequest.class));
+  }
+
+  @Test
+  @WithMockUser(username = "2", roles = "MAGISTRATE")
+  void updateRequest_shouldReturnBadRequest_whenRequestInvalid() throws Exception {
+    // WHEN
+    ResultActions result =
+        mockMvc.perform(
+            patch("/api/v2/requests/1")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content("{ \"procedureId\": \" \"}"));
+
+    // THEN
+    result
+        .andExpect(status().isBadRequest())
+        .andExpect(content().contentType("application/vnd.error+json"))
+        .andExpect(jsonPath("$.message").isNotEmpty());
+
+    verifyNoInteractions(requestService);
+  }
+
+  @Test
+  @WithMockUser(roles = {"MAGISTRATE"})
+  void updateRequest_shouldReturnBadRequest_whenBadRequestExceptionIsThrown() throws Exception {
+    // GIVEN
+    when(securityService.justiceUserMatchesRequest(eq(1))).thenReturn(true);
+
+    Mockito.doThrow(new BadRequestException(ErrorEnum.REQUEST_UPDATE_NOT_ALLOWED))
+        .when(requestService)
+        .updateRequest(eq(1), any(RequestUpdateRequest.class));
+
+    // WHEN
+    ResultActions result = mockMvc.perform(TestUtils.patch("/api/v2/requests/1", Map.of()));
+
+    // THEN
+    result.andExpect(status().isBadRequest());
+    verify(requestService, times(1)).updateRequest(eq(1), any(RequestUpdateRequest.class));
+  }
+
+  @Test
+  @WithMockUser(roles = "EXPERT")
+  void updateRequest_shouldReturnForbidden_whenUserDoesNotHaveRequiredRole() throws Exception {
+
+    // WHEN
+    ResultActions result = mockMvc.perform(TestUtils.patch("/api/v2/requests/1", Map.of()));
+
+    // THEN
+    result.andExpect(status().isForbidden());
+    verifyNoInteractions(requestService);
+  }
+
+  @Test
+  @WithMockUser(roles = "MAGISTRATE")
+  void updateRequest_shouldReturnForbidden_whenMagistrateNotOwnRequest() throws Exception {
+
+    // WHEN
+    ResultActions result = mockMvc.perform(TestUtils.patch("/api/v2/requests/1", Map.of()));
+
+    // THEN
+    result.andExpect(status().isForbidden());
+    verify(securityService).justiceUserMatchesRequest(1);
+    verifyNoInteractions(requestService);
+  }
 }
diff --git a/src/test/java/fr/gouv/beta/selexpert/mapper/RequestMapperTest.java b/src/test/java/fr/gouv/beta/selexpert/mapper/RequestMapperTest.java
new file mode 100644
index 0000000..4faef04
--- /dev/null
+++ b/src/test/java/fr/gouv/beta/selexpert/mapper/RequestMapperTest.java
@@ -0,0 +1,72 @@
+package fr.gouv.beta.selexpert.mapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import fr.gouv.beta.selexpert.model.Request;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+import org.openapitools.jackson.nullable.JsonNullable;
+import org.springframework.test.util.ReflectionTestUtils;
+
+class RequestMapperTest {
+
+  private final RequestMapper mapper = Mappers.getMapper(RequestMapper.class);
+
+  @BeforeEach
+  void before() {
+    ReflectionTestUtils.setField(
+        mapper, "jsonNullableMapper", Mappers.getMapper(JsonNullableMapper.class));
+  }
+
+  @Test
+  void testUpdateWithNonNullableValues() {
+    // GIVEN
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    updateRequest.setProcedureId(JsonNullable.of("145/15/25"));
+
+    Request request = new Request();
+    request.setProcedureId("");
+
+    // WHEN
+    mapper.update(updateRequest, request);
+
+    // THEN
+    assertEquals("145/15/25", request.getProcedureId(), "ProcedureId should be updated");
+  }
+
+  @Test
+  void testUpdateWithUndefinedValues() {
+    // GIVEN
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    updateRequest.setProcedureId(JsonNullable.undefined());
+
+    Request request = new Request();
+    request.setProcedureId("17/25/69");
+
+    // WHEN
+    mapper.update(updateRequest, request);
+
+    // THEN
+    assertEquals("17/25/69", request.getProcedureId(), "ProcedureId should remain unchanged");
+  }
+
+  @Test
+  void testUpdateWithNullValues() {
+    // GIVEN
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    updateRequest.setProcedureId(JsonNullable.of(null));
+
+    Request request = new Request();
+    request.setProcedureId("17/25/69");
+
+    // WHEN
+    mapper.update(updateRequest, request);
+
+    // THEN
+    assertNull(request.getProcedureId(), "ProcedureId should be updated to null");
+  }
+}
diff --git a/src/test/java/fr/gouv/beta/selexpert/request/RequestUpdateRequestTest.java b/src/test/java/fr/gouv/beta/selexpert/request/RequestUpdateRequestTest.java
new file mode 100644
index 0000000..f58b257
--- /dev/null
+++ b/src/test/java/fr/gouv/beta/selexpert/request/RequestUpdateRequestTest.java
@@ -0,0 +1,84 @@
+package fr.gouv.beta.selexpert.request;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.openapitools.jackson.nullable.JsonNullable;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@ExtendWith(SpringExtension.class)
+class RequestUpdateRequestTest {
+
+  private static Validator validator;
+
+  @BeforeAll
+  static void setup() {
+    validator = Validation.buildDefaultValidatorFactory().getValidator();
+  }
+
+  @Test
+  void whenRequestIsValid_thenNoViolations() {
+    // GIVEN
+    RequestUpdateRequest request = new RequestUpdateRequest();
+    request.setProcedureId(JsonNullable.of("12/58/58"));
+
+    // WHEN
+    Set<ConstraintViolation<RequestUpdateRequest>> violations = validator.validate(request);
+
+    // THEN
+    assertTrue(violations.isEmpty(), "There should be no validation errors for null inputs.");
+  }
+
+  @Test
+  void whenRequestFieldsAreUndefined_thenNoViolations() {
+    // GIVEN
+    RequestUpdateRequest request = new RequestUpdateRequest();
+    request.setProcedureId(JsonNullable.undefined());
+
+    // WHEN
+    Set<ConstraintViolation<RequestUpdateRequest>> violations = validator.validate(request);
+
+    // THEN
+    assertTrue(violations.isEmpty(), "There should be no validation errors for undefined inputs.");
+  }
+
+  @Test
+  void whenProcedureId_isNull_thenValidationFails() {
+    // GIVEN
+    RequestUpdateRequest request = new RequestUpdateRequest();
+    request.setProcedureId(JsonNullable.of(null));
+
+    // WHEN
+    Set<ConstraintViolation<RequestUpdateRequest>> violations = validator.validate(request);
+
+    // THEN
+    assertFalse(violations.isEmpty(), "There should be validation errors.");
+    assertEquals(1, violations.size());
+    ConstraintViolation<RequestUpdateRequest> violation = violations.iterator().next();
+    assertEquals("must not be blank", violation.getMessage());
+    assertEquals("procedureId", violation.getPropertyPath().toString());
+  }
+
+  @Test
+  void whenProcedureId_isBlank_thenValidationFails() {
+    // GIVEN
+    RequestUpdateRequest request = new RequestUpdateRequest();
+    request.setProcedureId(JsonNullable.of(" "));
+
+    // WHEN
+    Set<ConstraintViolation<RequestUpdateRequest>> violations = validator.validate(request);
+
+    // THEN
+    assertFalse(violations.isEmpty(), "There should be validation errors.");
+    assertEquals(1, violations.size());
+    ConstraintViolation<RequestUpdateRequest> violation = violations.iterator().next();
+    assertEquals("must not be blank", violation.getMessage());
+    assertEquals("procedureId", violation.getPropertyPath().toString());
+  }
+}
diff --git a/src/test/java/fr/gouv/beta/selexpert/service/RequestServiceTest.java b/src/test/java/fr/gouv/beta/selexpert/service/RequestServiceTest.java
index 6d11331..cb4a868 100644
--- a/src/test/java/fr/gouv/beta/selexpert/service/RequestServiceTest.java
+++ b/src/test/java/fr/gouv/beta/selexpert/service/RequestServiceTest.java
@@ -1,6 +1,7 @@
 package fr.gouv.beta.selexpert.service;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -14,25 +15,26 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
-import fr.gouv.beta.selexpert.config.properties.RequestSchedulerProperties;
 import fr.gouv.beta.selexpert.event.DeadlineExtendedEvent;
 import fr.gouv.beta.selexpert.exception.BadRequestException;
 import fr.gouv.beta.selexpert.exception.EntityNotFoundException;
 import fr.gouv.beta.selexpert.exception.ErrorEnum;
 import fr.gouv.beta.selexpert.mapper.JsonNullableMapper;
 import fr.gouv.beta.selexpert.mapper.RequestExpertMapper;
+import fr.gouv.beta.selexpert.mapper.RequestMapper;
 import fr.gouv.beta.selexpert.model.Request;
 import fr.gouv.beta.selexpert.model.RequestExpert;
 import fr.gouv.beta.selexpert.model.RequestExpertStatus;
 import fr.gouv.beta.selexpert.model.RequestStatus;
 import fr.gouv.beta.selexpert.model.Role;
 import fr.gouv.beta.selexpert.repository.RequestExpertRepository;
+import fr.gouv.beta.selexpert.repository.RequestRepository;
 import fr.gouv.beta.selexpert.request.RequestExpertUpdateRequest;
+import fr.gouv.beta.selexpert.request.RequestUpdateRequest;
 import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -48,10 +50,12 @@ import org.springframework.test.util.ReflectionTestUtils;
 class RequestServiceTest {
 
   @Mock private RequestExpertRepository requestExpertRepository;
+  @Mock private RequestRepository requestRepository;
   @Mock private ApplicationEventPublisher eventPublisher;
   @Mock private RequestExpertMapper requestExpertMapper;
+  @Mock private RequestMapper requestMapper;
   @Mock private JsonNullableMapper jsonNullableMapper;
-  @Mock private RequestSchedulerProperties properties;
+
   @InjectMocks private RequestService requestService;
 
   @BeforeEach
@@ -297,6 +301,37 @@ class RequestServiceTest {
                 userExpertId));
   }
 
+  @Test
+  void fetchRequestById_whenRequestExists_shouldReturnRequest() {
+    // GIVEN
+    Integer requestId = 1;
+    Request mockRequest = new Request();
+    mockRequest.setId(requestId);
+
+    when(requestRepository.findById(requestId)).thenReturn(Optional.of(mockRequest));
+
+    // WHEN
+    Request result = requestService.fetchRequestById(requestId);
+
+    // THEN
+    assertThat(result).isNotNull();
+    assertThat(result.getId()).isEqualTo(requestId);
+  }
+
+  @Test
+  void fetchRequestById_whenRequestDoesNotExist_shouldThrowEntityNotFoundException() {
+    // GIVEN
+    Integer requestId = 1;
+
+    when(requestRepository.findById(requestId)).thenReturn(Optional.empty());
+
+    // WHEN & THEN
+    assertThatThrownBy(() -> requestService.fetchRequestById(requestId))
+        .isInstanceOf(EntityNotFoundException.class)
+        .hasMessageContaining(
+            String.format(ErrorEnum.REQUEST_NOT_FOUND.getMessageTemplate(), requestId));
+  }
+
   @Test
   void shouldReturnTrueWhenRequestHasOnlyOneActiveExpert() {
     // GIVEN
@@ -345,4 +380,63 @@ class RequestServiceTest {
     assertFalse(result);
   }
 
+  @Test
+  void updateRequest_whenRequestIsValid_shouldUpdateRequest() {
+    // GIVEN
+    Integer requestId = 1;
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    Request request = new Request();
+    request.setId(requestId);
+    request.setArchived(false);
+    request.setStatus(RequestStatus.OPEN);
+
+    when(requestRepository.findById(requestId)).thenReturn(Optional.of(request));
+
+    // WHEN
+    requestService.updateRequest(requestId, updateRequest);
+
+    // THEN
+    verify(requestMapper).update(updateRequest, request);
+    verify(requestRepository).save(request);
+  }
+
+  @Test
+  void updateRequest_whenRequestIsArchived_shouldThrowBadRequestException() {
+    // GIVEN
+    Integer requestId = 1;
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    Request request = new Request();
+    request.setId(requestId);
+    request.setArchived(true); // Request is archived
+    request.setStatus(RequestStatus.OPEN);
+
+    when(requestRepository.findById(requestId)).thenReturn(Optional.of(request));
+
+    // WHEN & THEN
+    assertThatThrownBy(() -> requestService.updateRequest(requestId, updateRequest))
+        .isInstanceOf(BadRequestException.class)
+        .hasMessageContaining(ErrorEnum.REQUEST_UPDATE_NOT_ALLOWED.getMessageTemplate());
+    verifyNoInteractions(requestMapper);
+    verify(requestRepository, never()).save(any());
+  }
+
+  @Test
+  void updateRequest_whenRequestStatusIsNotOpen_shouldThrowBadRequestException() {
+    // GIVEN
+    Integer requestId = 1;
+    RequestUpdateRequest updateRequest = new RequestUpdateRequest();
+    Request request = new Request();
+    request.setId(requestId);
+    request.setArchived(false);
+    request.setStatus(RequestStatus.ACCEPTED); // Invalid status for update
+
+    when(requestRepository.findById(requestId)).thenReturn(Optional.of(request));
+
+    // WHEN & THEN
+    assertThatThrownBy(() -> requestService.updateRequest(requestId, updateRequest))
+        .isInstanceOf(BadRequestException.class)
+        .hasMessageContaining(ErrorEnum.REQUEST_UPDATE_NOT_ALLOWED.getMessageTemplate());
+    verifyNoInteractions(requestMapper);
+    verify(requestRepository, never()).save(any());
+  }
 }
-- 
GitLab