From 526e35ab287bb73690bc081cf1d90c44b971fe05 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:09:56 +0530 Subject: [PATCH 1/2] Merge pull request #207 from Shyam-Vishwakarma/integration-tests test: add integration tests for PATCH v1/endorsements/{id} endpoint --- .../exceptions/GlobalExceptionHandler.java | 6 +- .../external/RdsServiceImplementation.java | 3 +- .../com/RDS/skilltree/utils/Constants.java | 7 + .../UpdateEndorsementViewModel.java | 3 +- .../resources/application-test.properties | 2 +- .../UpdateEndorsementsIntegrationTest.java | 355 ++++++++++++++++++ 6 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java index 3af16875..447f65fc 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.RDS.skilltree.exceptions; +import com.RDS.skilltree.utils.Constants.ExceptionMessages; import com.RDS.skilltree.utils.GenericResponse; import jakarta.validation.ConstraintViolationException; import java.util.List; @@ -27,10 +28,7 @@ public ResponseEntity> handleNoEntityException(NoEntityE @ExceptionHandler({AuthenticationException.class, InsufficientAuthenticationException.class}) public ResponseEntity> handleInvalidBearerTokenException(Exception ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body( - new GenericResponse<>( - null, - "The access token provided is expired, revoked, malformed, or invalid for other reasons.")); + .body(new GenericResponse<>(ExceptionMessages.INVALID_ACCESS_TOKEN)); } @ExceptionHandler({AccessDeniedException.class}) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/external/RdsServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/external/RdsServiceImplementation.java index 8bbb98db..99ed4f5e 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/external/RdsServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/external/RdsServiceImplementation.java @@ -2,6 +2,7 @@ import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; import com.RDS.skilltree.exceptions.UserNotFoundException; +import com.RDS.skilltree.utils.Constants.ExceptionMessages; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +27,7 @@ public RdsGetUserDetailsResDto getUserDetails(String id) { return restTemplate.getForObject(url, RdsGetUserDetailsResDto.class); } catch (RestClientException error) { log.error("Error calling url {}, error: {}", url, error.getMessage()); - throw new UserNotFoundException("Error getting user details"); + throw new UserNotFoundException(ExceptionMessages.USER_NOT_FOUND); } } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java index da443a25..26e61621 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java @@ -8,5 +8,12 @@ public static final class ExceptionMessages { public static final String SKILL_NOT_FOUND = "Skill does not exist"; public static final String ENDORSEMENT_ALREADY_EXISTS = "Endorsement already exists"; public static final String ENDORSEMENT_NOT_FOUND = "Endorsement not found"; + public static final String ENDORSEMENT_MESSAGE_EMPTY = "Endorsement message cannot be empty"; + public static final String USER_NOT_FOUND = "Error getting user details"; + public static final String UNAUTHORIZED_ENDORSEMENT_UPDATE = + "Not authorized to update this endorsement"; + public static final String INVALID_ACCESS_TOKEN = + "The access token provided is expired, revoked, malformed, or invalid for other reasons."; + public static final String ACCESS_DENIED = "Access Denied"; } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java index 0b1685a7..010fa53c 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java @@ -1,5 +1,6 @@ package com.RDS.skilltree.viewmodels; +import com.RDS.skilltree.utils.Constants.ExceptionMessages; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; @@ -7,6 +8,6 @@ @Getter @Setter public class UpdateEndorsementViewModel { - @NotNull(message = "Message cannot be empty") + @NotNull(message = ExceptionMessages.ENDORSEMENT_MESSAGE_EMPTY) private String message; } diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties index 55ca50d8..5a1e9459 100644 --- a/skill-tree/src/main/resources/application-test.properties +++ b/skill-tree/src/main/resources/application-test.properties @@ -1,4 +1,4 @@ -cookieName=rds-session-v2-development +cookieName=rds-session-v2 test.db.mysql-image=mysql:8.1.0 spring.flyway.enabled=true spring.flyway.locations=classpath:db/migrations diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java new file mode 100644 index 00000000..94e1cd77 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java @@ -0,0 +1,355 @@ +package com.RDS.skilltree.integration.skills; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static utils.TestDataHelper.createEndorsementViewModel; +import static utils.TestDataHelper.createUserDetails; + +import com.RDS.skilltree.TestContainerManager; +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.exceptions.UserNotFoundException; +import com.RDS.skilltree.models.Endorsement; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.repositories.EndorsementRepository; +import com.RDS.skilltree.repositories.SkillRepository; +import com.RDS.skilltree.repositories.UserSkillRepository; +import com.RDS.skilltree.services.external.RdsService; +import com.RDS.skilltree.utils.Constants.ExceptionMessages; +import com.RDS.skilltree.utils.JWTUtils; +import com.RDS.skilltree.viewmodels.EndorsementViewModel; +import com.RDS.skilltree.viewmodels.UpdateEndorsementViewModel; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; +import java.util.Map; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import utils.RestAPIHelper; +import utils.TestDataHelper; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import({TestContainerManager.class}) +public class UpdateEndorsementsIntegrationTest { + @Autowired private EndorsementRepository endorsementRepository; + @Autowired private SkillRepository skillRepository; + @Autowired private UserSkillRepository userSkillRepository; + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + + @MockBean private RdsService rdsService; + @MockBean private JWTUtils jwtUtils; + + private final String superUserId = "super-user-id"; + private final String userId1 = "user-id-1"; + private final String userId2 = "user-id-2"; + + private final String SKILL_NAME = "Java"; + private final String INITIAL_MESSAGE = "Initial message"; + private final String NEW_MESSAGE = "Updated message"; + + @BeforeEach + void setUp() { + skillRepository.deleteAll(); + endorsementRepository.deleteAll(); + userSkillRepository.deleteAll(); + + RdsGetUserDetailsResDto superUserDetails = createUserDetails(superUserId, true); + + RdsGetUserDetailsResDto user1Details = createUserDetails(userId1, false); + RdsGetUserDetailsResDto user2Details = createUserDetails(userId2, false); + + when(rdsService.getUserDetails(superUserId)).thenReturn(superUserDetails); + when(rdsService.getUserDetails(userId1)).thenReturn(user1Details); + when(rdsService.getUserDetails(userId2)).thenReturn(user2Details); + } + + private Skill createAndSaveSkill(String skillName) { + return skillRepository.save(TestDataHelper.createSkill(skillName, superUserId)); + } + + private Endorsement createAndSaveEndorsement( + Skill skill, String endorseId, String endorserId, String message) { + Endorsement endorsement = + TestDataHelper.createEndorsement(skill, endorseId, endorserId, message); + return endorsementRepository.save(endorsement); + } + + private EndorsementViewModel extractEndorsementFromResult(MvcResult result) throws Exception { + String responseJson = result.getResponse().getContentAsString(); + return objectMapper.readValue(responseJson, EndorsementViewModel.class); + } + + private void assertIsEqual( + EndorsementViewModel actualEndorsement, EndorsementViewModel expectedEndorsement) { + expectedEndorsement.setId(actualEndorsement.getId()); + assertThat(actualEndorsement).usingRecursiveComparison().isEqualTo(expectedEndorsement); + } + + private MvcResult performPatchRequest(String url, String requestBody) throws Exception { + return mockMvc + .perform( + MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andReturn(); + } + + private String createUrl(Integer endorsementId) { + return String.format("/v1/endorsements/%d", endorsementId); + } + + private UpdateEndorsementViewModel createRequestModel(String newMessage) { + UpdateEndorsementViewModel updateEndorsementViewModel = new UpdateEndorsementViewModel(); + updateEndorsementViewModel.setMessage(newMessage); + return updateEndorsementViewModel; + } + + @Test + @DisplayName("Happy flow for superuser - Successfully update endorsement message") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void updateEndorsement_superUser_whenRequestValid_shouldUpdateEndorsement() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId1, superUserId, INITIAL_MESSAGE); + + UpdateEndorsementViewModel updateEndorsementViewModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(updateEndorsementViewModel); + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + + existingEndorsement.setMessage(updateEndorsementViewModel.getMessage()); + EndorsementViewModel expectedEndorsement = + createEndorsementViewModel(existingEndorsement, rdsService); + assertIsEqual(actualEndorsement, expectedEndorsement); + + Endorsement dbEndorsement = + endorsementRepository.findById(existingEndorsement.getId()).orElseThrow(); + assertThat(dbEndorsement.getMessage()).isEqualTo(updateEndorsementViewModel.getMessage()); + } + + @Test + @DisplayName("Happy flow for non-superuser - Successfully update endorsement message") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_user_whenRequestValid_shouldUpdateEndorsement() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel updateEndorsementViewModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(updateEndorsementViewModel); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + + existingEndorsement.setMessage(updateEndorsementViewModel.getMessage()); + EndorsementViewModel expectedEndorsement = + createEndorsementViewModel(existingEndorsement, rdsService); + assertIsEqual(actualEndorsement, expectedEndorsement); + + Endorsement dbEndorsement = + endorsementRepository.findById(existingEndorsement.getId()).orElseThrow(); + assertThat(dbEndorsement.getMessage()).isEqualTo(updateEndorsementViewModel.getMessage()); + } + + @Test + @DisplayName("Endorsement does not exist, should return endorsement not found") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_whenEndorsementIdDoesNotExist_shouldReturnNotFound() + throws Exception { + Integer nonExistentEndorsementId = 999; + + UpdateEndorsementViewModel requestModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(requestModel); + + MvcResult result = performPatchRequest(createUrl(nonExistentEndorsementId), updateBody); + assertThat(result.getResponse().getStatus()).isEqualTo(404); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.ENDORSEMENT_NOT_FOUND); + } + + @Test + @Disabled("Fails due to authorization bug tracked in #206 – re-enable once fixed") + @DisplayName("when user is not the endorser, should not update endorsement") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_othersEndorsement_shouldNotUpdateEndorsement() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, superUserId, INITIAL_MESSAGE); + + UpdateEndorsementViewModel updateEndorsementViewModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(updateEndorsementViewModel); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(403); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.UNAUTHORIZED_ENDORSEMENT_UPDATE); + + Endorsement dbEndorsement = + endorsementRepository.findById(existingEndorsement.getId()).orElseThrow(); + assertThat(dbEndorsement).usingRecursiveComparison().isEqualTo(existingEndorsement); + } + + @Test + @Disabled("Fails due to validation bug tracked in #206 – re-enable once fixed") + @DisplayName("Message is empty string, request is not valid") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_whenMessageIsValidAndEmpty_shouldReturnBadRequest() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel requestModel = createRequestModel(""); + String updateBody = objectMapper.writeValueAsString(requestModel); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + assertThat(result.getResponse().getStatus()).isEqualTo(400); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.ENDORSEMENT_MESSAGE_EMPTY); + } + + @Test + @Disabled("Fails due to bug tracked in #206 – re-enable once fixed") + @DisplayName("RdsService fails to get 'endorser' details, should return 404") + @WithCustomMockUser( + username = "non-existent-endorser-id", + authorities = {"USER"}) + public void updateEndorsement_whenRdsServiceFailsForEndorserDetails_shouldReturn404() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorserId = "non-existent-endorser-id"; + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, endorserId, INITIAL_MESSAGE); + + UpdateEndorsementViewModel requestModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(requestModel); + + when(rdsService.getUserDetails(endorserId)) + .thenThrow(new UserNotFoundException(ExceptionMessages.USER_NOT_FOUND)); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + assertThat(result.getResponse().getStatus()).isEqualTo(404); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.USER_NOT_FOUND); + + Endorsement dbEndorsement = + endorsementRepository.findById(existingEndorsement.getId()).orElseThrow(); + assertThat(dbEndorsement).usingRecursiveComparison().isEqualTo(existingEndorsement); + } + + @Test + @Disabled("Fails due to bug tracked in #206 – re-enable once fixed") + @DisplayName("RdsService fails to get 'endorse' details, should return 404") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_whenRdsServiceFailsForEndorseDetails_shouldReturn404() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = "non-existent-endorse-id"; + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, endorseId, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel requestModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(requestModel); + + when(rdsService.getUserDetails(endorseId)) + .thenThrow(new UserNotFoundException(ExceptionMessages.USER_NOT_FOUND)); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + assertThat(result.getResponse().getStatus()).isEqualTo(404); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.USER_NOT_FOUND); + + Endorsement dbEndorsement = + endorsementRepository.findById(existingEndorsement.getId()).orElseThrow(); + assertThat(dbEndorsement).usingRecursiveComparison().isEqualTo(existingEndorsement); + } + + @Test + @DisplayName("Message field is missing, request is not valid") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_whenRequestBodyMessageFieldIsMissing_shouldReturnBadRequest() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + String requestBody = "{}"; + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), requestBody); + assertThat(result.getResponse().getStatus()).isEqualTo(400); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.ENDORSEMENT_MESSAGE_EMPTY); + } + + @Test + @DisplayName("User is unauthorized, should return 403") + public void updateEndorsement_whenUserIsUnauthorized_shouldReturn403() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel requestModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(requestModel); + Map.Entry cookie = + RestAPIHelper.getGuestUserCookie().entrySet().iterator().next(); + MvcResult result = + mockMvc + .perform( + MockMvcRequestBuilders.patch(createUrl(existingEndorsement.getId())) + .contentType(MediaType.APPLICATION_JSON) + .content(updateBody) + .cookie(new Cookie(cookie.getKey(), cookie.getValue()))) + .andReturn(); + + assertThat(result.getResponse().getStatus()).isEqualTo(403); + assertThat(result.getResponse().getContentAsString()).contains(ExceptionMessages.ACCESS_DENIED); + } + + @Test + @DisplayName("User is unauthenticated, should return 401") + public void updateEndorsement_whenUserIsUnauthenticated_shouldReturn401() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel requestModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(requestModel); + + MvcResult result = performPatchRequest(createUrl(existingEndorsement.getId()), updateBody); + assertThat(result.getResponse().getStatus()).isEqualTo(401); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.INVALID_ACCESS_TOKEN); + } +} From 096518f487e84f891fdafc3be0d6910235ad9969 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:53:25 +0530 Subject: [PATCH 2/2] fix: fix bugs in the endpoint for updating endorsements (#208) * fix: ensure empty endorsements msg not allowed * fix: add feature-flag in endorsement controller * fix: update service impl to ensure no unauthorized endorsement update, user-not-found bug * fix: enable disabled test cases * fix: disable endorsements update when not in dev mode * fix: run all tests in dev mode, add test case to validate updation is no allowed in non-dev mode * fix: update name of test case * fix: import only used exceptions * fix: update control flow --- .../RDS/skilltree/apis/EndorsementsApi.java | 6 +- .../exceptions/GlobalExceptionHandler.java | 7 +++ .../services/EndorsementService.java | 3 +- .../EndorsementServiceImplementation.java | 58 +++++++++++-------- .../com/RDS/skilltree/utils/Constants.java | 2 + .../UpdateEndorsementViewModel.java | 4 +- .../UpdateEndorsementsIntegrationTest.java | 28 +++++++-- 7 files changed, 74 insertions(+), 34 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/EndorsementsApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/EndorsementsApi.java index cd3d5212..25f96949 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/EndorsementsApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/EndorsementsApi.java @@ -22,7 +22,9 @@ public class EndorsementsApi { @PatchMapping("/{id}") public ResponseEntity update( - @PathVariable Integer id, @Valid @RequestBody UpdateEndorsementViewModel body) { - return new ResponseEntity<>(endorsementService.update(id, body), HttpStatus.OK); + @PathVariable Integer id, + @Valid @RequestBody UpdateEndorsementViewModel body, + @RequestParam(name = "dev", required = false, defaultValue = "false") boolean isDev) { + return new ResponseEntity<>(endorsementService.update(id, body, isDev), HttpStatus.OK); } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java index 447f65fc..86f1ca9c 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java @@ -140,4 +140,11 @@ public ResponseEntity handleEndorsementAlreadyExistsException( return new ResponseEntity<>( new GenericResponse<>(ex.getMessage()), HttpStatus.METHOD_NOT_ALLOWED); } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalStateException(IllegalStateException ex) { + log.error("IllegalStateException - Error : {}", ex.getMessage()); + return new ResponseEntity<>( + new GenericResponse<>(ex.getMessage()), HttpStatus.METHOD_NOT_ALLOWED); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementService.java b/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementService.java index 36ca6653..a2e310c7 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementService.java @@ -10,5 +10,6 @@ public interface EndorsementService { EndorsementViewModel create(CreateEndorsementViewModel endorsement); - EndorsementViewModel update(Integer endorsementId, UpdateEndorsementViewModel endorsement); + EndorsementViewModel update( + Integer endorsementId, UpdateEndorsementViewModel endorsement, boolean isDev); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementServiceImplementation.java index 1c6d25c2..34a8e4dc 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/EndorsementServiceImplementation.java @@ -3,6 +3,7 @@ import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; import com.RDS.skilltree.exceptions.EndorsementAlreadyExistsException; import com.RDS.skilltree.exceptions.EndorsementNotFoundException; +import com.RDS.skilltree.exceptions.ForbiddenException; import com.RDS.skilltree.exceptions.SelfEndorsementNotAllowedException; import com.RDS.skilltree.exceptions.SkillNotFoundException; import com.RDS.skilltree.models.Endorsement; @@ -130,30 +131,39 @@ public EndorsementViewModel create(CreateEndorsementViewModel endorsementViewMod } @Override - public EndorsementViewModel update(Integer endorsementId, UpdateEndorsementViewModel body) { - Optional exitingEndorsement = endorsementRepository.findById(endorsementId); - - if (exitingEndorsement.isEmpty()) { - log.info(String.format("Endorsement with id: %s not found", endorsementId)); - throw new EndorsementNotFoundException(ExceptionMessages.ENDORSEMENT_NOT_FOUND); + public EndorsementViewModel update( + Integer endorsementId, UpdateEndorsementViewModel body, boolean isDev) { + if (isDev) { + Optional existingEndorsement = endorsementRepository.findById(endorsementId); + + if (existingEndorsement.isEmpty()) { + log.info("Endorsement with id: {} not found", endorsementId); + throw new EndorsementNotFoundException(ExceptionMessages.ENDORSEMENT_NOT_FOUND); + } + + Endorsement endorsement = existingEndorsement.get(); + + JwtUser jwtDetails = + (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String userId = jwtDetails.getRdsUserId(); + + if (endorsement.getEndorserId().equals(userId)) { + RdsGetUserDetailsResDto endorseDetails = + rdsService.getUserDetails(endorsement.getEndorseId()); + RdsGetUserDetailsResDto endorserDetails = rdsService.getUserDetails(userId); + + endorsement.setMessage(body.getMessage()); + Endorsement savedEndorsementDetails = endorsementRepository.save(endorsement); + + return EndorsementViewModel.toViewModel( + savedEndorsementDetails, + UserViewModel.toViewModel(endorseDetails.getUser()), + UserViewModel.toViewModel(endorserDetails.getUser())); + } else { + log.warn("User: {} is not authorized to update endorsement: {}", userId, endorsementId); + throw new ForbiddenException(ExceptionMessages.UNAUTHORIZED_ENDORSEMENT_UPDATE); + } } - - Endorsement endorsement = exitingEndorsement.get(); - String updatedMessage = body.getMessage(); - - if (updatedMessage != null) { - endorsement.setMessage(updatedMessage); - } - - Endorsement savedEndorsementDetails = endorsementRepository.save(endorsement); - RdsGetUserDetailsResDto endorseDetails = - rdsService.getUserDetails(savedEndorsementDetails.getEndorseId()); - RdsGetUserDetailsResDto endorserDetails = - rdsService.getUserDetails(savedEndorsementDetails.getEndorserId()); - - return EndorsementViewModel.toViewModel( - savedEndorsementDetails, - UserViewModel.toViewModel(endorseDetails.getUser()), - UserViewModel.toViewModel(endorserDetails.getUser())); + throw new IllegalStateException(ExceptionMessages.UPDATE_DISABLED_IN_NON_DEV_MODE); } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java index 26e61621..340c2860 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java @@ -15,5 +15,7 @@ public static final class ExceptionMessages { public static final String INVALID_ACCESS_TOKEN = "The access token provided is expired, revoked, malformed, or invalid for other reasons."; public static final String ACCESS_DENIED = "Access Denied"; + public static final String UPDATE_DISABLED_IN_NON_DEV_MODE = + "Update is not allowed outside of development mode"; } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java index 010fa53c..638f653a 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/UpdateEndorsementViewModel.java @@ -1,13 +1,13 @@ package com.RDS.skilltree.viewmodels; import com.RDS.skilltree.utils.Constants.ExceptionMessages; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UpdateEndorsementViewModel { - @NotNull(message = ExceptionMessages.ENDORSEMENT_MESSAGE_EMPTY) + @NotBlank(message = ExceptionMessages.ENDORSEMENT_MESSAGE_EMPTY) private String message; } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java index 94e1cd77..317f3414 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/UpdateEndorsementsIntegrationTest.java @@ -108,7 +108,8 @@ private MvcResult performPatchRequest(String url, String requestBody) throws Exc } private String createUrl(Integer endorsementId) { - return String.format("/v1/endorsements/%d", endorsementId); + String isDev = "?dev=true"; + return String.format("/v1/endorsements/%d" + isDev, endorsementId); } private UpdateEndorsementViewModel createRequestModel(String newMessage) { @@ -192,7 +193,6 @@ public void updateEndorsement_whenEndorsementIdDoesNotExist_shouldReturnNotFound } @Test - @Disabled("Fails due to authorization bug tracked in #206 – re-enable once fixed") @DisplayName("when user is not the endorser, should not update endorsement") @WithCustomMockUser( username = userId1, @@ -217,7 +217,6 @@ public void updateEndorsement_othersEndorsement_shouldNotUpdateEndorsement() thr } @Test - @Disabled("Fails due to validation bug tracked in #206 – re-enable once fixed") @DisplayName("Message is empty string, request is not valid") @WithCustomMockUser( username = userId1, @@ -238,7 +237,6 @@ public void updateEndorsement_whenMessageIsValidAndEmpty_shouldReturnBadRequest( } @Test - @Disabled("Fails due to bug tracked in #206 – re-enable once fixed") @DisplayName("RdsService fails to get 'endorser' details, should return 404") @WithCustomMockUser( username = "non-existent-endorser-id", @@ -267,7 +265,6 @@ public void updateEndorsement_whenRdsServiceFailsForEndorserDetails_shouldReturn } @Test - @Disabled("Fails due to bug tracked in #206 – re-enable once fixed") @DisplayName("RdsService fails to get 'endorse' details, should return 404") @WithCustomMockUser( username = userId1, @@ -352,4 +349,25 @@ public void updateEndorsement_whenUserIsUnauthenticated_shouldReturn401() throws assertThat(result.getResponse().getContentAsString()) .contains(ExceptionMessages.INVALID_ACCESS_TOKEN); } + + @Test + @DisplayName("Endorsement update not allowed in non-dev mode, should return 405") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void updateEndorsement_nonDevMode_shouldReturn405() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + Endorsement existingEndorsement = + createAndSaveEndorsement(skill, userId2, userId1, INITIAL_MESSAGE); + + UpdateEndorsementViewModel updateEndorsementViewModel = createRequestModel(NEW_MESSAGE); + String updateBody = objectMapper.writeValueAsString(updateEndorsementViewModel); + + String url = String.format("/v1/endorsements/%d", existingEndorsement.getId()); + MvcResult result = performPatchRequest(url, updateBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(405); + assertThat(result.getResponse().getContentAsString()) + .contains(ExceptionMessages.UPDATE_DISABLED_IN_NON_DEV_MODE); + } }