From 36f6a577b110dbe0f7a25453564b41597112264a Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:15:58 +0530 Subject: [PATCH 01/10] Add integration tests for GET v1/skills route (#161) * add integration tests for GET v1/skills, add test DB configuration * fix format violation --- .../resources/application-test.properties | 8 ++ .../skills/GetAllSkillsIntegrationTest.java | 95 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 skill-tree/src/main/resources/application-test.properties create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties new file mode 100644 index 00000000..02c453f3 --- /dev/null +++ b/skill-tree/src/main/resources/application-test.properties @@ -0,0 +1,8 @@ +cookieName=rds-session-v2-development + +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/skilltreetestdb +spring.datasource.username=${MYSQL_DB_USERNAME} +spring.datasource.password=${MYSQL_DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java new file mode 100644 index 00000000..b16ac200 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java @@ -0,0 +1,95 @@ +package com.RDS.skilltree.skills; + +import com.RDS.skilltree.enums.SkillTypeEnum; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.repositories.SkillRepository; +import com.RDS.skilltree.services.SkillService; +import jakarta.servlet.http.Cookie; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class GetAllSkillsIntegrationTest { + + @Autowired private SkillService skillService; + + @Autowired private SkillRepository skillRepository; + + @Autowired private MockMvc mockMvc; + + private Cookie authCookie; + + @BeforeEach + public void setUp() { + authCookie = + new Cookie( + "rds-session-v2-development", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJzOXpRVW00WGJWRXo3eHpSa2FadiIsInJvbGUiOiJzdXBlcl91c2VyIiwiaWF0IjoxNzI4NjY0NjA2LCJleHAiOjE3MzEyNTY2MDZ9.EyOFKrVcbleuTjUGic3GzOzYRDoLU4IShyoboe0MHlvWFOAfU2pchpXLE4NcyvdGUZ_tvoUecHd4kUkR8MkhxnkRNU3HE7N-1c1tFeYXZL0KfScJE9YzDXAl113Hx3eZVvYbhNjNUttbDlH4s_kR6YABC3sdbLGKEiLfmp9VeAs"); + + skillRepository.deleteAll(); + Skill skill1 = new Skill(); + skill1.setName("Java"); + skill1.setType(SkillTypeEnum.ATOMIC); + skill1.setCreatedBy("s9zQUm4XbVEz7xzRkaZv"); + + Skill skill2 = new Skill(); + skill2.setName("Springboot"); + skill2.setType(SkillTypeEnum.ATOMIC); + skill2.setCreatedBy("s9zQUm4XbVEz7xzRkaZv"); + + skillRepository.saveAll(Arrays.asList(skill1, skill2)); + } + + @Test + @DisplayName("happy flow - returns all skills that are in db") + public void getAllSkillsHappyFlow() throws Exception { + + mockMvc + .perform( + MockMvcRequestBuilders.get("/v1/skills") + .cookie(authCookie) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Java")) + .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Springboot")) + .andDo(MockMvcResultHandlers.print()); + } + + @Test + @DisplayName("if no skills available, return empty list") + public void noSkillsAvailable_shouldReturnEmptyList() throws Exception { + skillRepository.deleteAll(); + + mockMvc + .perform( + MockMvcRequestBuilders.get("/v1/skills") + .cookie(authCookie) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$").isEmpty()); + } + + @Test + @DisplayName("if invalid cookie, return 401") + public void ifInvalidCoolie_returnUnauthorized() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get("/v1/skills") + .cookie(new Cookie("cookie1", "eyJhbGciOiJSUz.eyJhbGciOiJSUz.EyJhbGciOiJSUz")) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + } +} From 7e27cc5ed5900c17d5f0a68b9df258ca7f383608 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:26:26 +0530 Subject: [PATCH 02/10] Add integration tests for GET /v1/skills/requests route (#169) * add integration tests for GET v1/skills, add test DB configuration * fix format violation * Update test configuration to create schema using Flyway * Add integration test for v1/skills/requests route * Add custom result matcher for mockMvc responses * Add security config for test environment to mock logged in users * Add Use of CustomMockUser instead of AuthCookie * Add spring-security test dependency --- skill-tree/pom.xml | 5 + .../resources/application-test.properties | 6 +- .../GetAllSkillRequestIntegrationTest.java | 285 ++++++++++++++++++ .../skills/GetAllSkillsIntegrationTest.java | 34 +-- .../test/java/utils/CustomResultMatchers.java | 99 ++++++ .../utils/CustomSecurityContextFactory.java | 39 +++ .../test/java/utils/WithCustomMockUser.java | 13 + 7 files changed, 462 insertions(+), 19 deletions(-) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java create mode 100644 skill-tree/src/test/java/utils/CustomResultMatchers.java create mode 100644 skill-tree/src/test/java/utils/CustomSecurityContextFactory.java create mode 100644 skill-tree/src/test/java/utils/WithCustomMockUser.java diff --git a/skill-tree/pom.xml b/skill-tree/pom.xml index ac132714..c840309b 100644 --- a/skill-tree/pom.xml +++ b/skill-tree/pom.xml @@ -90,6 +90,11 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.security + spring-security-test + test + diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties index 02c453f3..5d9cfc71 100644 --- a/skill-tree/src/main/resources/application-test.properties +++ b/skill-tree/src/main/resources/application-test.properties @@ -3,6 +3,10 @@ cookieName=rds-session-v2-development spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/skilltreetestdb spring.datasource.username=${MYSQL_DB_USERNAME} spring.datasource.password=${MYSQL_DB_PASSWORD} + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.hibernate.ddl-auto=create-drop + +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migrations + spring.jpa.show-sql=true diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java new file mode 100644 index 00000000..90dbcd91 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java @@ -0,0 +1,285 @@ +package com.RDS.skilltree.skills; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.enums.SkillTypeEnum; +import com.RDS.skilltree.enums.UserSkillStatusEnum; +import com.RDS.skilltree.models.Endorsement; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.models.UserSkills; +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.JWTUtils; +import com.RDS.skilltree.viewmodels.RdsUserViewModel; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.Cookie; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.CustomResultMatchers; +import utils.WithCustomMockUser; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class GetAllSkillRequestIntegrationTest { + @Autowired private MockMvc mockMvc; + + @Autowired private UserSkillRepository userSkillRepository; + + @Autowired private EndorsementRepository endorsementRepository; + + @MockBean private RdsService rdsService; + + @Autowired private SkillRepository skillRepository; + + @MockBean private JWTUtils jwtUtils; + + private final String route = "/v1/skills/requests"; + + @BeforeEach + void setUp() { + // Clean up repositories + skillRepository.deleteAll(); + endorsementRepository.deleteAll(); + userSkillRepository.deleteAll(); + + // Setup super-user detail + RdsUserViewModel superUser = new RdsUserViewModel(); + superUser.setId("super-user-id"); + RdsUserViewModel.Roles roles = new RdsUserViewModel.Roles(); + roles.setSuper_user(true); + superUser.setRoles(roles); + + RdsGetUserDetailsResDto superUserDetails = new RdsGetUserDetailsResDto(); + superUserDetails.setUser(superUser); + + // Setup normal-users detail + RdsUserViewModel normalUser = new RdsUserViewModel(); + normalUser.setId("user-id"); + RdsUserViewModel.Roles normalUserRoles = new RdsUserViewModel.Roles(); + normalUserRoles.setSuper_user(false); + normalUser.setRoles(normalUserRoles); + + RdsGetUserDetailsResDto normalUserDetails = new RdsGetUserDetailsResDto(); + normalUserDetails.setUser(normalUser); + + RdsUserViewModel normalUser2 = new RdsUserViewModel(); + normalUser2.setId("user-id-2"); + normalUser2.setRoles(normalUserRoles); + RdsGetUserDetailsResDto normalUser2Details = new RdsGetUserDetailsResDto(); + normalUser2Details.setUser(normalUser2); + + // Setup mock skills + Skill skill1 = new Skill(); + skill1.setName("Java"); + skill1.setType(SkillTypeEnum.ATOMIC); + skill1.setCreatedBy("super-user-id"); + + Skill skill2 = new Skill(); + skill2.setName("Springboot"); + skill2.setType(SkillTypeEnum.ATOMIC); + skill2.setCreatedBy("super-user-id"); + + skillRepository.save(skill1); + skillRepository.save(skill2); + + // Setup mock user-skills + UserSkills userSkills1 = new UserSkills(); + userSkills1.setSkill(skill1); + userSkills1.setUserId("user-id"); + userSkills1.setStatus(UserSkillStatusEnum.PENDING); + + UserSkills userSkills2 = new UserSkills(); + userSkills2.setSkill(skill2); + userSkills2.setUserId("user-id"); + userSkills2.setStatus(UserSkillStatusEnum.APPROVED); + + UserSkills userSkills3 = new UserSkills(); + userSkills3.setSkill(skill2); + userSkills3.setUserId("user-id-2"); + userSkills3.setStatus(UserSkillStatusEnum.PENDING); + + userSkillRepository.save(userSkills1); + userSkillRepository.save(userSkills2); + userSkillRepository.save(userSkills3); + + // Setup mock endorsements + Endorsement endorsement1 = new Endorsement(); + endorsement1.setId(1); + endorsement1.setEndorserId("super-user-id"); + endorsement1.setEndorseId("user-id"); + endorsement1.setSkill(skill1); + endorsement1.setMessage("endorsement message"); + + Endorsement endorsement2 = new Endorsement(); + endorsement2.setId(3); + endorsement2.setEndorserId("user-id-2"); + endorsement2.setEndorseId("user-id"); + endorsement2.setSkill(skill2); + endorsement2.setMessage("skill2 for user-id"); + + Endorsement endorsement3 = new Endorsement(); + endorsement3.setId(4); + endorsement3.setEndorserId("super-user-id"); + endorsement3.setEndorseId("user-id-2"); + endorsement3.setSkill(skill2); + endorsement3.setMessage("skill2 for user-id-2"); + + endorsementRepository.save(endorsement1); + endorsementRepository.save(endorsement2); + endorsementRepository.save(endorsement3); + + // Setup RDS service mock responses + when(rdsService.getUserDetails("super-user-id")).thenReturn(superUserDetails); + when(rdsService.getUserDetails("user-id-2")).thenReturn(normalUser2Details); + when(rdsService.getUserDetails("user-id")).thenReturn(normalUserDetails); + + // Mock JWTUtils to bypass actual JWT verification + Claims mockClaims = mock(Claims.class); + when(mockClaims.get("userId", String.class)).thenReturn("super-user-id"); + when(jwtUtils.validateToken(anyString())).thenReturn(mockClaims); + } + + @Test + @DisplayName("Happy flow for SuperUser - should return all requests") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void getAllRequests_asSuperUser_shouldReturnAllRequests() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(CustomResultMatchers.hasSkillRequest("Java", "user-id", "PENDING")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Java", "user-id", "super-user-id", "endorsement message")) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id", "user-id-2", "skill2 for user-id")) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id-2", "PENDING")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id-2", "super-user-id", "skill2 for user-id-2")) + .andExpect(CustomResultMatchers.hasUser("user-id", " ")) + .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")) + .andExpect(CustomResultMatchers.hasUser("super-user-id", " ")); + } + + @Test + @DisplayName("Happy flow for normal user - Get all requests where user is endorser") + @WithCustomMockUser( + username = "user-id-2", + authorities = {"USER"}) + public void getAllRequests_asNormalUser_shouldReturnAllRequestsByEndorser() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id", "user-id-2", "skill2 for user-id")) + .andExpect(CustomResultMatchers.hasUser("user-id", " ")) + .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); + } + + @Test + @DisplayName("Filter requests by status - should return filtered requests") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void getAllRequests_ByStatus_ShouldReturnFilteredRequests() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=APPROVED") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id", "user-id-2", "skill2 for user-id")) + .andExpect(CustomResultMatchers.hasUser("user-id", " ")) + .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); + } + + @Test + @DisplayName("If no skill Requests endorsed by user then return empty lists") + @WithCustomMockUser( + username = "user-id", + authorities = {"USER"}) + public void noSkillRequestsEndorsedByUser_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) + .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); + } + + @Test + @DisplayName("If no matching skill requests by status then return empty lists") + @WithCustomMockUser( + username = "user-id", + authorities = {"USER"}) + public void noMatchingRequestsByStatus_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=REJECTED") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) + .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); + } + + @Test + @DisplayName("If no skill requests in DB - return empty lists") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void getAllRequests_NoData_ShouldReturnEmptyLists() throws Exception { + skillRepository.deleteAll(); + endorsementRepository.deleteAll(); + userSkillRepository.deleteAll(); + + mockMvc + .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) + .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); + } + + @Test + @DisplayName("if invalid cookie, return 401") + public void ifInvalidCoolie_ShouldReturnUnauthorized() throws Exception { + Cookie authCookie = + new Cookie( + "cookie", + "eyJhbGciOiJSUzI1NiIsInR5cCI.eyJ1c2VySWQiOiI2N2lSeXJOTWQ.E-EtcPOj7Ca5l8JuE0hwky0rRikYSNZBvC"); + + mockMvc + .perform( + MockMvcRequestBuilders.get(route) + .cookie(authCookie) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + } +} diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java index b16ac200..cca60e09 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java @@ -18,6 +18,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; @SpringBootTest @AutoConfigureMockMvc @@ -30,15 +31,10 @@ public class GetAllSkillsIntegrationTest { @Autowired private MockMvc mockMvc; - private Cookie authCookie; + private final String route = "/v1/skills"; @BeforeEach public void setUp() { - authCookie = - new Cookie( - "rds-session-v2-development", - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJzOXpRVW00WGJWRXo3eHpSa2FadiIsInJvbGUiOiJzdXBlcl91c2VyIiwiaWF0IjoxNzI4NjY0NjA2LCJleHAiOjE3MzEyNTY2MDZ9.EyOFKrVcbleuTjUGic3GzOzYRDoLU4IShyoboe0MHlvWFOAfU2pchpXLE4NcyvdGUZ_tvoUecHd4kUkR8MkhxnkRNU3HE7N-1c1tFeYXZL0KfScJE9YzDXAl113Hx3eZVvYbhNjNUttbDlH4s_kR6YABC3sdbLGKEiLfmp9VeAs"); - skillRepository.deleteAll(); Skill skill1 = new Skill(); skill1.setName("Java"); @@ -54,14 +50,13 @@ public void setUp() { } @Test + @WithCustomMockUser( + username = "rds-user", + authorities = {"SUPERUSER"}) @DisplayName("happy flow - returns all skills that are in db") public void getAllSkillsHappyFlow() throws Exception { - mockMvc - .perform( - MockMvcRequestBuilders.get("/v1/skills") - .cookie(authCookie) - .accept(MediaType.APPLICATION_JSON)) + .perform(MockMvcRequestBuilders.get(route).accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Java")) .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Springboot")) @@ -70,14 +65,14 @@ public void getAllSkillsHappyFlow() throws Exception { @Test @DisplayName("if no skills available, return empty list") + @WithCustomMockUser( + username = "rds-user", + authorities = {"SUPERUSER"}) public void noSkillsAvailable_shouldReturnEmptyList() throws Exception { skillRepository.deleteAll(); mockMvc - .perform( - MockMvcRequestBuilders.get("/v1/skills") - .cookie(authCookie) - .accept(MediaType.APPLICATION_JSON)) + .perform(MockMvcRequestBuilders.get(route).accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$").isEmpty()); } @@ -85,11 +80,14 @@ public void noSkillsAvailable_shouldReturnEmptyList() throws Exception { @Test @DisplayName("if invalid cookie, return 401") public void ifInvalidCoolie_returnUnauthorized() throws Exception { + Cookie authCookie = + new Cookie( + "cookie", + "eyJhbGciOiJSUzI1NiIsInR5cCI.eyJ1c2VySWQiOiI2N2lSeXJOTWQ.E-EtcPOj7Ca5l8JuE0hwky0rRikYSNZBvC"); + mockMvc .perform( - MockMvcRequestBuilders.get("/v1/skills") - .cookie(new Cookie("cookie1", "eyJhbGciOiJSUz.eyJhbGciOiJSUz.EyJhbGciOiJSUz")) - .accept(MediaType.APPLICATION_JSON)) + MockMvcRequestBuilders.get(route).cookie(authCookie).accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isUnauthorized()); } } diff --git a/skill-tree/src/test/java/utils/CustomResultMatchers.java b/skill-tree/src/test/java/utils/CustomResultMatchers.java new file mode 100644 index 00000000..5eb85e6c --- /dev/null +++ b/skill-tree/src/test/java/utils/CustomResultMatchers.java @@ -0,0 +1,99 @@ +package utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.test.web.servlet.ResultMatcher; + +public class CustomResultMatchers { + + public static ResultMatcher hasSkillRequest(String skillName, String endorseId, String status) { + return result -> { + String json = result.getResponse().getContentAsString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode root = objectMapper.readTree(json); + + // Find the request for the given skillName and endorseId + JsonNode matchingSkillRequest = findMatchingSkillRequest(root, skillName, endorseId); + assertThat(matchingSkillRequest).isNotNull().isNotEmpty(); + + assertThat(matchingSkillRequest.get("endorseId").asText()).isEqualTo(endorseId); + assertThat(matchingSkillRequest.get("status").asText()).isEqualTo(status); + }; + } + + public static ResultMatcher hasEndorsement( + String skillName, String endorseId, String endorserId, String message) { + return result -> { + String json = result.getResponse().getContentAsString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode root = objectMapper.readTree(json); + + // Find the request for the given skillName and endorseId + JsonNode matchingSkillRequest = findMatchingSkillRequest(root, skillName, endorseId); + assertThat(matchingSkillRequest).isNotNull().isNotEmpty(); + + // Check endorsements + JsonNode endorsements = matchingSkillRequest.get("endorsements"); + assertThat(endorsements).isNotNull().isNotEmpty(); + + // Find matching endorsement by endorserId + JsonNode matchingEndorsement = findByField(endorsements, "endorserId", endorserId); + assertThat(matchingEndorsement).isNotNull(); + + // Assert endorsement details + assertThat(matchingEndorsement.get("endorserId").asText()).isEqualTo(endorserId); + assertThat(matchingEndorsement.get("message").asText()).isEqualTo(message); + }; + } + + public static ResultMatcher hasUser(String userId, String name) { + return result -> { + String json = result.getResponse().getContentAsString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode root = objectMapper.readTree(json); + + JsonNode users = root.get("users"); + assertThat(users).isNotNull().isNotEmpty(); + + // Find user by userId + JsonNode matchingUser = findByField(users, "id", userId); + assertThat(matchingUser).isNotNull(); + + // Assert user details + assertThat(matchingUser.get("id").asText()).isEqualTo(userId); + assertThat(matchingUser.get("name").asText()).isEqualTo(name); + }; + } + + private static JsonNode findByField(JsonNode array, String fieldName, String value) { + for (JsonNode node : array) { + if (node.has(fieldName) && node.get(fieldName).asText().equals(value)) { + return node; + } + } + return null; + } + + private static JsonNode findMatchingSkillRequest( + JsonNode root, String skillName, String endorseId) { + JsonNode requests = root.get("requests"); + assertThat(requests).isNotNull().isNotEmpty(); + + return findBySkillAndEndorseId(requests, skillName, endorseId); + } + + private static JsonNode findBySkillAndEndorseId( + JsonNode array, String skillName, String endorseId) { + for (JsonNode node : array) { + if (node.has("skillName") + && node.get("skillName").asText().equals(skillName) + && node.has("endorseId") + && node.get("endorseId").asText().equals(endorseId)) { + return node; + } + } + return null; + } +} diff --git a/skill-tree/src/test/java/utils/CustomSecurityContextFactory.java b/skill-tree/src/test/java/utils/CustomSecurityContextFactory.java new file mode 100644 index 00000000..bcb98cac --- /dev/null +++ b/skill-tree/src/test/java/utils/CustomSecurityContextFactory.java @@ -0,0 +1,39 @@ +package utils; + +import com.RDS.skilltree.enums.UserRoleEnum; +import com.RDS.skilltree.models.JwtUser; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class CustomSecurityContextFactory + implements WithSecurityContextFactory { + @Override + public SecurityContext createSecurityContext(WithCustomMockUser customMockUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + List roles = + Arrays.stream(customMockUser.authorities()).map(UserRoleEnum::valueOf).toList(); + UserRoleEnum mainRole = roles.isEmpty() ? UserRoleEnum.USER : roles.get(0); + + // Create jwt user + JwtUser jwtUser = new JwtUser(customMockUser.username(), mainRole); + + // Map roles to Spring Security authorities + List authorities = + roles.stream() + .map(role -> new SimpleGrantedAuthority(role.name())) + .collect(Collectors.toList()); + + // Set JwtUser as the principal in Authentication + Authentication auth = new UsernamePasswordAuthenticationToken(jwtUser, null, authorities); + context.setAuthentication(auth); + return context; + } +} diff --git a/skill-tree/src/test/java/utils/WithCustomMockUser.java b/skill-tree/src/test/java/utils/WithCustomMockUser.java new file mode 100644 index 00000000..c913d2fd --- /dev/null +++ b/skill-tree/src/test/java/utils/WithCustomMockUser.java @@ -0,0 +1,13 @@ +package utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = CustomSecurityContextFactory.class) +public @interface WithCustomMockUser { + String username() default "test-user"; + + String[] authorities() default {}; +} From 52cb8d97b0bfa48df285a24817521e2a1446cb46 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:57:49 +0530 Subject: [PATCH 03/10] add integration test for POST /v1/skills/requests/{skillId}/action (#177) --- .../SkillRequestActionIntegrationTest.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java new file mode 100644 index 00000000..dca1d197 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java @@ -0,0 +1,200 @@ +package com.RDS.skilltree.skills; + +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.enums.SkillTypeEnum; +import com.RDS.skilltree.enums.UserSkillStatusEnum; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.models.UserSkills; +import com.RDS.skilltree.repositories.SkillRepository; +import com.RDS.skilltree.repositories.UserSkillRepository; +import com.RDS.skilltree.services.external.RdsService; +import com.RDS.skilltree.utils.JWTUtils; +import com.RDS.skilltree.viewmodels.RdsUserViewModel; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class SkillRequestActionIntegrationTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private UserSkillRepository userSkillRepository; + @Autowired + private SkillRepository skillRepository; + @MockBean + private RdsService rdsService; + @MockBean + private JWTUtils jwtUtils; + @Autowired + private ObjectMapper objectMapper; + private Skill skill; + private final String baseRoute = "/v1/skills/requests"; + + @BeforeEach + void setUp() { + // Clean up repositories + skillRepository.deleteAll(); + userSkillRepository.deleteAll(); + + // Setup super-user detail + RdsUserViewModel superUser = new RdsUserViewModel(); + superUser.setId("super-user-id"); + RdsUserViewModel.Roles roles = new RdsUserViewModel.Roles(); + roles.setSuper_user(true); + superUser.setRoles(roles); + + RdsGetUserDetailsResDto superUserDetails = new RdsGetUserDetailsResDto(); + superUserDetails.setUser(superUser); + + // Setup mock skill + skill = new Skill(); + skill.setName("Java"); + skill.setType(SkillTypeEnum.ATOMIC); + skill.setCreatedBy("super-user-id"); + skill = skillRepository.save(skill); + + // Setup mock user-skill + UserSkills userSkill = new UserSkills(); + userSkill.setSkill(skill); + userSkill.setUserId("test-user-id"); + userSkill.setStatus(UserSkillStatusEnum.PENDING); + userSkillRepository.save(userSkill); + + // Setup RDS service mock responses + when(rdsService.getUserDetails("super-user-id")).thenReturn(superUserDetails); + + // Mock JWTUtils to bypass actual JWT verification + Claims mockClaims = mock(Claims.class); + when(mockClaims.get("userId", String.class)).thenReturn("super-user-id"); + when(jwtUtils.validateToken(anyString())).thenReturn(mockClaims); + } + + @Test + @DisplayName("Happy flow - Super user can approve a skill request") + @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + public void approveSkillRequest_validRequest_shouldApproveSkillRequest() throws Exception { + String requestBody = """ + { + "endorseId": "test-user-id", + "action": "APPROVED" + } + """; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(String.valueOf(requestBody))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("approved")); + + // Verify the status was updated in database + UserSkills updatedUserSkill = userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); + Assertions.assertEquals(UserSkillStatusEnum.APPROVED, updatedUserSkill.getStatus()); + } + + @Test + @DisplayName("Happy flow - Super user can reject a skill request") + @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + public void rejectSkillRequest_validRequest_shouldRejectSkillRequest() throws Exception { + String requestBody = """ + { + "endorseId": "test-user-id", + "action": "REJECTED" + } + """; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("rejected")); + + // Verify the status was updatedUserSkill in database + UserSkills updatedUserSkill = userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); + Assertions.assertEquals(UserSkillStatusEnum.REJECTED, updatedUserSkill.getStatus()); + } + + @Test + @DisplayName("Error case - Request with non-existent skill ID") + @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + public void approveSkillRequest_NonExistentSkillId_ShouldFail() throws Exception { + String requestBody = """ + { + "endorseId": "test-user-id", + "action": "APPROVED" + } + """; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/123/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + + @Test + @DisplayName("Error case - Request with non-existent user ID") + @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + public void approveSkillRequest_NonExistentUserId_ShouldFail() throws Exception { + String requestBody = """ + { + "endorseId": "non-existent-user", + "action": "APPROVED" + } + """; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + + @Test + @DisplayName("Validation test - Missing required fields") + @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + public void approveSkillRequest_MissingRequiredFields_ShouldFail() throws Exception { + String requestBody = "{}"; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + @Test + @DisplayName("Authorization test - Non-super user cannot access endpoint") + @WithCustomMockUser(username = "normal-user", authorities = {"USER"}) + public void approveSkillRequest_NonSuperUser_ShouldFail() throws Exception { + String requestBody = """ + { + "endorseId": "test-user-id", + "action": "APPROVED" + } + """; + + mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + } +} \ No newline at end of file From 907bf83176accf235f1fdf8a6f7114c170a836c9 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Sat, 1 Mar 2025 03:55:01 +0530 Subject: [PATCH 04/10] Test: Integration Test for POST /v1/skills (#182) * change status code from 200 to 201 * add integration tests for POST v1/skills --- .../com/RDS/skilltree/apis/SkillsApi.java | 2 +- .../skills/CreateSkillIntegrationTest.java | 159 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java index c829e997..0515e27e 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java @@ -57,7 +57,7 @@ public ResponseEntity> approveRejectSkillRequest( @PostMapping @AuthorizedRoles({UserRoleEnum.SUPERUSER}) public ResponseEntity create(@Valid @RequestBody CreateSkillViewModel skill) { - return ResponseEntity.ok(skillService.create(skill)); + return ResponseEntity.status(HttpStatus.CREATED).body(skillService.create(skill)); } @GetMapping("/{id}/endorsements") diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java new file mode 100644 index 00000000..4c2f56d9 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java @@ -0,0 +1,159 @@ +package com.RDS.skilltree.skills; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.enums.SkillTypeEnum; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.repositories.SkillRepository; +import com.RDS.skilltree.services.external.RdsService; +import com.RDS.skilltree.utils.JWTUtils; +import com.RDS.skilltree.viewmodels.RdsUserViewModel; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class CreateSkillIntegrationTest { + @Autowired private MockMvc mockMvc; + @Autowired private SkillRepository skillRepository; + @MockBean private RdsService rdsService; + @MockBean private JWTUtils jwtUtils; + @Autowired private ObjectMapper objectMapper; + + private final String baseRoute = "/v1/skills"; + + @BeforeEach + void setUp() { + // Clean up repository + skillRepository.deleteAll(); + + // Setup super-user detail + RdsUserViewModel superUser = new RdsUserViewModel(); + superUser.setId("super-user-id"); + RdsUserViewModel.Roles roles = new RdsUserViewModel.Roles(); + roles.setSuper_user(true); + superUser.setRoles(roles); + + RdsGetUserDetailsResDto superUserDetails = new RdsGetUserDetailsResDto(); + superUserDetails.setUser(superUser); + + // Setup RDS service mock responses + when(rdsService.getUserDetails("super-user-id")).thenReturn(superUserDetails); + + // Mock JWTUtils to bypass actual JWT verification + Claims mockClaims = mock(Claims.class); + when(mockClaims.get("userId", String.class)).thenReturn("super-user-id"); + when(jwtUtils.validateToken(anyString())).thenReturn(mockClaims); + } + + @Test + @DisplayName("Happy flow - Super user can create a new skill") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void createSkill_validRequest_shouldCreateSkill() throws Exception { + String requestBody = "{" + "\"name\": \"Java\"," + "\"type\": \"ATOMIC\"" + "}"; + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Java")) + .andExpect(MockMvcResultMatchers.jsonPath("$.type").value("ATOMIC")); + + assert skillRepository.existsByName("Java"); + ; + } + + @Test + @DisplayName("Error case - Cannot create duplicate skill") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void createSkill_duplicateName_shouldFail() throws Exception { + Skill existingSkill = new Skill(); + existingSkill.setName("Java"); + existingSkill.setType(SkillTypeEnum.ATOMIC); + existingSkill.setCreatedBy("super-user-id"); + skillRepository.save(existingSkill); + + String requestBody = "{" + "\"name\": \"Java\"," + "\"type\": \"ATOMIC\"" + "}"; + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isConflict()); + } + + @Test + @DisplayName("Validation test - Missing required fields") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void createSkill_missingRequiredFields_shouldFail() throws Exception { + String requestBody = "{" + "\"type\": \"ATOMIC\"" + "}"; + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + @Test + @DisplayName("Validation test - Invalid skill type") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void createSkill_invalidSkillType_shouldFail() throws Exception { + String requestBody = "{" + "\"name\": \"Java\"," + "\"type\": \"INVALID_TYPE\"" + "}"; + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().is5xxServerError()); + } + + @Test + @DisplayName("Authorization test - Non-super user cannot create skill") + @WithCustomMockUser( + username = "normal-user", + authorities = {"USER"}) + public void createSkill_nonSuperUser_shouldFail() throws Exception { + String requestBody = "{" + "\"name\": \"Java\"," + "\"type\": \"ATOMIC\"" + "}"; + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + } +} From c71d76729cca3d59c8904d6bf4e64c894d7b7dd8 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:51:51 +0530 Subject: [PATCH 05/10] fix: update formatting of integration tests for POST /v1/skills/requests/{skillId}/action (#188) * fix:update formatting of request-body * replace hardcoded JSON strings by serializing object of SkillRequestActionRequestDto --- .../SkillRequestActionIntegrationTest.java | 169 ++++++++++-------- 1 file changed, 90 insertions(+), 79 deletions(-) diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java index dca1d197..1510741c 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java @@ -1,6 +1,11 @@ package com.RDS.skilltree.skills; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.dtos.SkillRequestActionRequestDto; import com.RDS.skilltree.enums.SkillTypeEnum; import com.RDS.skilltree.enums.UserSkillStatusEnum; import com.RDS.skilltree.models.Skill; @@ -28,26 +33,16 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import utils.WithCustomMockUser; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") public class SkillRequestActionIntegrationTest { - @Autowired - private MockMvc mockMvc; - @Autowired - private UserSkillRepository userSkillRepository; - @Autowired - private SkillRepository skillRepository; - @MockBean - private RdsService rdsService; - @MockBean - private JWTUtils jwtUtils; - @Autowired - private ObjectMapper objectMapper; + @Autowired private MockMvc mockMvc; + @Autowired private UserSkillRepository userSkillRepository; + @Autowired private SkillRepository skillRepository; + @MockBean private RdsService rdsService; + @MockBean private JWTUtils jwtUtils; + @Autowired private ObjectMapper objectMapper; private Skill skill; private final String baseRoute = "/v1/skills/requests"; @@ -92,109 +87,125 @@ void setUp() { @Test @DisplayName("Happy flow - Super user can approve a skill request") - @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) public void approveSkillRequest_validRequest_shouldApproveSkillRequest() throws Exception { - String requestBody = """ - { - "endorseId": "test-user-id", - "action": "APPROVED" - } - """; - - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") - .contentType(MediaType.APPLICATION_JSON) - .content(String.valueOf(requestBody))) + SkillRequestActionRequestDto requestDto = new SkillRequestActionRequestDto(); + requestDto.setEndorseId("test-user-id"); + requestDto.setAction(UserSkillStatusEnum.APPROVED); + String requestBody = objectMapper.writeValueAsString(requestDto); + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("approved")); // Verify the status was updated in database - UserSkills updatedUserSkill = userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); + UserSkills updatedUserSkill = + userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); Assertions.assertEquals(UserSkillStatusEnum.APPROVED, updatedUserSkill.getStatus()); } @Test @DisplayName("Happy flow - Super user can reject a skill request") - @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) public void rejectSkillRequest_validRequest_shouldRejectSkillRequest() throws Exception { - String requestBody = """ - { - "endorseId": "test-user-id", - "action": "REJECTED" - } - """; - - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) + SkillRequestActionRequestDto requestDto = new SkillRequestActionRequestDto(); + requestDto.setEndorseId("test-user-id"); + requestDto.setAction(UserSkillStatusEnum.REJECTED); + String requestBody = objectMapper.writeValueAsString(requestDto); + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("rejected")); - // Verify the status was updatedUserSkill in database - UserSkills updatedUserSkill = userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); + // Verify the status was updated in database + UserSkills updatedUserSkill = + userSkillRepository.findByUserIdAndSkillId("test-user-id", skill.getId()).get(0); Assertions.assertEquals(UserSkillStatusEnum.REJECTED, updatedUserSkill.getStatus()); } @Test @DisplayName("Error case - Request with non-existent skill ID") - @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) public void approveSkillRequest_NonExistentSkillId_ShouldFail() throws Exception { - String requestBody = """ - { - "endorseId": "test-user-id", - "action": "APPROVED" - } - """; - - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/123/action") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) + SkillRequestActionRequestDto requestDto = new SkillRequestActionRequestDto(); + requestDto.setEndorseId("test-user-id"); + requestDto.setAction(UserSkillStatusEnum.APPROVED); + String requestBody = objectMapper.writeValueAsString(requestDto); + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/123/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andExpect(MockMvcResultMatchers.status().isNotFound()); } @Test @DisplayName("Error case - Request with non-existent user ID") - @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) public void approveSkillRequest_NonExistentUserId_ShouldFail() throws Exception { - String requestBody = """ - { - "endorseId": "non-existent-user", - "action": "APPROVED" - } - """; - - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) + SkillRequestActionRequestDto requestDto = new SkillRequestActionRequestDto(); + requestDto.setEndorseId("non-existent-user"); + requestDto.setAction(UserSkillStatusEnum.APPROVED); + String requestBody = objectMapper.writeValueAsString(requestDto); + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andExpect(MockMvcResultMatchers.status().isNotFound()); } @Test @DisplayName("Validation test - Missing required fields") - @WithCustomMockUser(username = "super-user-id", authorities = {"SUPERUSER"}) + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) public void approveSkillRequest_MissingRequiredFields_ShouldFail() throws Exception { String requestBody = "{}"; - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andExpect(MockMvcResultMatchers.status().isBadRequest()); } @Test @DisplayName("Authorization test - Non-super user cannot access endpoint") - @WithCustomMockUser(username = "normal-user", authorities = {"USER"}) + @WithCustomMockUser( + username = "normal-user", + authorities = {"USER"}) public void approveSkillRequest_NonSuperUser_ShouldFail() throws Exception { - String requestBody = """ - { - "endorseId": "test-user-id", - "action": "APPROVED" - } - """; - - mockMvc.perform(MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) + SkillRequestActionRequestDto requestDto = new SkillRequestActionRequestDto(); + requestDto.setEndorseId("test-user-id"); + requestDto.setAction(UserSkillStatusEnum.APPROVED); + String requestBody = objectMapper.writeValueAsString(requestDto); + + mockMvc + .perform( + MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) .andExpect(MockMvcResultMatchers.status().isForbidden()); } -} \ No newline at end of file +} From 637fdc1444d424ce16e88e8474905ef236ce3e81 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:59:32 +0530 Subject: [PATCH 06/10] fix: setup integration tests using test containers (#192) * fix: update failsafe config, add dotenv dependency * fix: replace hardcoded test-db-name with env variable * fix: move integration test classes to integration package * add test container manager * fix: remove test-db config as it is not required * fix: remove print statements * fix: remove disabled from unit tests, update config of surefire to locate unite tests correctly * fix: upgrade github actions runner version to v4 * fix: make version of mysql db to a more specific version tag * fix: update config to include multilevel folder * fix: update expected status to status code * fix: remove unnecessary config and functions * fix: fix typos in commands --------- Co-authored-by: yash raj --- .github/workflows/ci.yml | 26 +++++++------- skill-tree/pom.xml | 4 +-- .../resources/application-test.properties | 9 ----- .../RDS/skilltree/TestContainerManager.java | 12 +++++++ .../skills/CreateSkillIntegrationTest.java | 23 ++++++------ .../GetAllSkillRequestIntegrationTest.java | 34 +++++++++--------- .../skills/GetAllSkillsIntegrationTest.java | 35 ++++++++++++++----- .../SkillRequestActionIntegrationTest.java | 27 +++++++------- .../utils/UUIDValidationInterceptorTest.java | 12 ++----- 9 files changed, 95 insertions(+), 87 deletions(-) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java rename skill-tree/src/test/java/com/RDS/skilltree/{ => integration}/skills/CreateSkillIntegrationTest.java (89%) rename skill-tree/src/test/java/com/RDS/skilltree/{ => integration}/skills/GetAllSkillRequestIntegrationTest.java (92%) rename skill-tree/src/test/java/com/RDS/skilltree/{ => integration}/skills/GetAllSkillsIntegrationTest.java (71%) rename skill-tree/src/test/java/com/RDS/skilltree/{ => integration}/skills/SkillRequestActionIntegrationTest.java (91%) rename skill-tree/src/test/java/com/RDS/skilltree/{ => unit}/utils/UUIDValidationInterceptorTest.java (92%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3c0bc83..5f620681 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,19 +6,19 @@ on: - "**" jobs: build: - name: Maven Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Java 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: "temurin" - - - name: Build with Maven - run: mvn verify --file skill-tree/pom.xml -Pgit-build-profile -Dskip-tests=true + name: Maven Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Java 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: "temurin" + + - name: Build with Maven + run: mvn verify --file skill-tree/pom.xml -P git-build-profile -Dskip-tests=true unit-test: name: Unit Tests diff --git a/skill-tree/pom.xml b/skill-tree/pom.xml index c840309b..d53dee5a 100644 --- a/skill-tree/pom.xml +++ b/skill-tree/pom.xml @@ -219,7 +219,7 @@ ${skip-ut} ${skip-tests} - **/unit/*.java + **/unit/**/*.java @@ -231,7 +231,7 @@ ${skip-tests} - **/integration/*.java + **/integration/**/*.java diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties index 5d9cfc71..fd45b40c 100644 --- a/skill-tree/src/main/resources/application-test.properties +++ b/skill-tree/src/main/resources/application-test.properties @@ -1,12 +1,3 @@ cookieName=rds-session-v2-development - -spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/skilltreetestdb -spring.datasource.username=${MYSQL_DB_USERNAME} -spring.datasource.password=${MYSQL_DB_PASSWORD} - -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - spring.flyway.enabled=true spring.flyway.locations=classpath:db/migrations - -spring.jpa.show-sql=true diff --git a/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java new file mode 100644 index 00000000..e37ab25c --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java @@ -0,0 +1,12 @@ +package com.RDS.skilltree; + +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; + +public abstract class TestContainerManager { + @ServiceConnection static MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.1.0"); + + static { + mySQLContainer.start(); + } +} diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java similarity index 89% rename from skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java rename to skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java index 4c2f56d9..941543e5 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/CreateSkillIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java @@ -1,9 +1,10 @@ -package com.RDS.skilltree.skills; +package com.RDS.skilltree.integration.skills; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.RDS.skilltree.TestContainerManager; import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; import com.RDS.skilltree.enums.SkillTypeEnum; import com.RDS.skilltree.models.Skill; @@ -16,6 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -24,14 +26,14 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import utils.WithCustomMockUser; -@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -public class CreateSkillIntegrationTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class CreateSkillIntegrationTest extends TestContainerManager { @Autowired private MockMvc mockMvc; @Autowired private SkillRepository skillRepository; @MockBean private RdsService rdsService; @@ -77,13 +79,11 @@ public void createSkill_validRequest_shouldCreateSkill() throws Exception { MockMvcRequestBuilders.post(baseRoute) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.status().is(201)) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Java")) .andExpect(MockMvcResultMatchers.jsonPath("$.type").value("ATOMIC")); assert skillRepository.existsByName("Java"); - ; } @Test @@ -105,8 +105,7 @@ public void createSkill_duplicateName_shouldFail() throws Exception { MockMvcRequestBuilders.post(baseRoute) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isConflict()); + .andExpect(MockMvcResultMatchers.status().is(409)); } @Test @@ -122,7 +121,7 @@ public void createSkill_missingRequiredFields_shouldFail() throws Exception { MockMvcRequestBuilders.post(baseRoute) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isBadRequest()); + .andExpect(MockMvcResultMatchers.status().is(400)); } @Test @@ -138,7 +137,7 @@ public void createSkill_invalidSkillType_shouldFail() throws Exception { MockMvcRequestBuilders.post(baseRoute) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().is5xxServerError()); + .andExpect(MockMvcResultMatchers.status().is(500)); } @Test @@ -154,6 +153,6 @@ public void createSkill_nonSuperUser_shouldFail() throws Exception { MockMvcRequestBuilders.post(baseRoute) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isForbidden()); + .andExpect(MockMvcResultMatchers.status().is(403)); } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java similarity index 92% rename from skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java rename to skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java index 90dbcd91..ac2686ab 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillRequestIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java @@ -1,9 +1,10 @@ -package com.RDS.skilltree.skills; +package com.RDS.skilltree.integration.skills; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.RDS.skilltree.TestContainerManager; import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; import com.RDS.skilltree.enums.SkillTypeEnum; import com.RDS.skilltree.enums.UserSkillStatusEnum; @@ -18,7 +19,10 @@ import com.RDS.skilltree.viewmodels.RdsUserViewModel; import io.jsonwebtoken.Claims; import jakarta.servlet.http.Cookie; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -27,15 +31,15 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import utils.CustomResultMatchers; import utils.WithCustomMockUser; -@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -public class GetAllSkillRequestIntegrationTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class GetAllSkillRequestIntegrationTest extends TestContainerManager { @Autowired private MockMvc mockMvc; @Autowired private UserSkillRepository userSkillRepository; @@ -162,8 +166,7 @@ void setUp() { public void getAllRequests_asSuperUser_shouldReturnAllRequests() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(CustomResultMatchers.hasSkillRequest("Java", "user-id", "PENDING")) .andExpect( CustomResultMatchers.hasEndorsement( @@ -189,7 +192,7 @@ public void getAllRequests_asSuperUser_shouldReturnAllRequests() throws Exceptio public void getAllRequests_asNormalUser_shouldReturnAllRequestsByEndorser() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) .andExpect( CustomResultMatchers.hasEndorsement( @@ -208,8 +211,7 @@ public void getAllRequests_ByStatus_ShouldReturnFilteredRequests() throws Except .perform( MockMvcRequestBuilders.get(route + "?status=APPROVED") .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) .andExpect( CustomResultMatchers.hasEndorsement( @@ -226,8 +228,7 @@ public void getAllRequests_ByStatus_ShouldReturnFilteredRequests() throws Except public void noSkillRequestsEndorsedByUser_ShouldReturnEmptyLists() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); } @@ -242,8 +243,7 @@ public void noMatchingRequestsByStatus_ShouldReturnEmptyLists() throws Exception .perform( MockMvcRequestBuilders.get(route + "?status=REJECTED") .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); } @@ -260,8 +260,7 @@ public void getAllRequests_NoData_ShouldReturnEmptyLists() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); } @@ -279,7 +278,6 @@ public void ifInvalidCoolie_ShouldReturnUnauthorized() throws Exception { MockMvcRequestBuilders.get(route) .cookie(authCookie) .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + .andExpect(MockMvcResultMatchers.status().is(401)); } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java similarity index 71% rename from skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java rename to skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java index cca60e09..ba475422 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/GetAllSkillsIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java @@ -1,35 +1,48 @@ -package com.RDS.skilltree.skills; +package com.RDS.skilltree.integration.skills; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.RDS.skilltree.TestContainerManager; import com.RDS.skilltree.enums.SkillTypeEnum; import com.RDS.skilltree.models.Skill; import com.RDS.skilltree.repositories.SkillRepository; import com.RDS.skilltree.services.SkillService; +import com.RDS.skilltree.services.external.RdsService; +import com.RDS.skilltree.utils.JWTUtils; +import io.jsonwebtoken.Claims; import jakarta.servlet.http.Cookie; import java.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; 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.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import utils.WithCustomMockUser; -@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -public class GetAllSkillsIntegrationTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class GetAllSkillsIntegrationTest extends TestContainerManager { @Autowired private SkillService skillService; @Autowired private SkillRepository skillRepository; @Autowired private MockMvc mockMvc; + @MockBean private JWTUtils jwtUtils; + + @MockBean private RdsService rdsService; private final String route = "/v1/skills"; @@ -47,6 +60,11 @@ public void setUp() { skill2.setCreatedBy("s9zQUm4XbVEz7xzRkaZv"); skillRepository.saveAll(Arrays.asList(skill1, skill2)); + + // Mock JWTUtils to bypass actual JWT verification + Claims mockClaims = mock(Claims.class); + when(mockClaims.get("userId", String.class)).thenReturn("rds-user"); + when(jwtUtils.validateToken(anyString())).thenReturn(mockClaims); } @Test @@ -57,10 +75,9 @@ public void setUp() { public void getAllSkillsHappyFlow() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).accept(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Java")) - .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Springboot")) - .andDo(MockMvcResultHandlers.print()); + .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Springboot")); } @Test @@ -73,7 +90,7 @@ public void noSkillsAvailable_shouldReturnEmptyList() throws Exception { mockMvc .perform(MockMvcRequestBuilders.get(route).accept(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$").isEmpty()); } @@ -88,6 +105,6 @@ public void ifInvalidCoolie_returnUnauthorized() throws Exception { mockMvc .perform( MockMvcRequestBuilders.get(route).cookie(authCookie).accept(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + .andExpect(MockMvcResultMatchers.status().is(401)); } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java similarity index 91% rename from skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java rename to skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java index 1510741c..7ee5b016 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/skills/SkillRequestActionIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java @@ -1,9 +1,10 @@ -package com.RDS.skilltree.skills; +package com.RDS.skilltree.integration.skills; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.RDS.skilltree.TestContainerManager; import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; import com.RDS.skilltree.dtos.SkillRequestActionRequestDto; import com.RDS.skilltree.enums.SkillTypeEnum; @@ -17,10 +18,7 @@ import com.RDS.skilltree.viewmodels.RdsUserViewModel; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +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; @@ -29,14 +27,14 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import utils.WithCustomMockUser; -@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -public class SkillRequestActionIntegrationTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class SkillRequestActionIntegrationTest extends TestContainerManager { @Autowired private MockMvc mockMvc; @Autowired private UserSkillRepository userSkillRepository; @Autowired private SkillRepository skillRepository; @@ -101,8 +99,7 @@ public void approveSkillRequest_validRequest_shouldApproveSkillRequest() throws MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("approved")); // Verify the status was updated in database @@ -127,7 +124,7 @@ public void rejectSkillRequest_validRequest_shouldRejectSkillRequest() throws Ex MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.status().is(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("rejected")); // Verify the status was updated in database @@ -152,7 +149,7 @@ public void approveSkillRequest_NonExistentSkillId_ShouldFail() throws Exception MockMvcRequestBuilders.post(baseRoute + "/123/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isNotFound()); + .andExpect(MockMvcResultMatchers.status().is(404)); } @Test @@ -171,7 +168,7 @@ public void approveSkillRequest_NonExistentUserId_ShouldFail() throws Exception MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isNotFound()); + .andExpect(MockMvcResultMatchers.status().is(404)); } @Test @@ -187,7 +184,7 @@ public void approveSkillRequest_MissingRequiredFields_ShouldFail() throws Except MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isBadRequest()); + .andExpect(MockMvcResultMatchers.status().is(400)); } @Test @@ -206,6 +203,6 @@ public void approveSkillRequest_NonSuperUser_ShouldFail() throws Exception { MockMvcRequestBuilders.post(baseRoute + "/" + skill.getId() + "/action") .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) - .andExpect(MockMvcResultMatchers.status().isForbidden()); + .andExpect(MockMvcResultMatchers.status().is(403)); } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/utils/UUIDValidationInterceptorTest.java b/skill-tree/src/test/java/com/RDS/skilltree/unit/utils/UUIDValidationInterceptorTest.java similarity index 92% rename from skill-tree/src/test/java/com/RDS/skilltree/utils/UUIDValidationInterceptorTest.java rename to skill-tree/src/test/java/com/RDS/skilltree/unit/utils/UUIDValidationInterceptorTest.java index f17e6cc1..b0d431da 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/utils/UUIDValidationInterceptorTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/unit/utils/UUIDValidationInterceptorTest.java @@ -1,14 +1,14 @@ -package com.RDS.skilltree.utils; +package com.RDS.skilltree.unit.utils; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import com.RDS.skilltree.exceptions.InvalidParameterException; +import com.RDS.skilltree.utils.UUIDValidationInterceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.UUID; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -25,7 +25,6 @@ class UUIDValidationInterceptorTest { @InjectMocks private UUIDValidationInterceptor interceptor; @Test - @Disabled public void itShouldReturnTrueIfValidUserIDIsGiven() { when(request.getParameter("skillID")).thenReturn(null); when(request.getParameter("userID")).thenReturn(UUID.randomUUID().toString()); @@ -34,7 +33,6 @@ public void itShouldReturnTrueIfValidUserIDIsGiven() { } @Test - @Disabled public void itShouldReturnTrueIfValidSkillIDIsGiven() { when(request.getParameter("userID")).thenReturn(null); when(request.getParameter("skillID")).thenReturn(UUID.randomUUID().toString()); @@ -43,7 +41,6 @@ public void itShouldReturnTrueIfValidSkillIDIsGiven() { } @Test - @Disabled public void itShouldReturnTrueIfValidUserIDAndValidSkillIDIsGiven() { when(request.getParameter("userID")).thenReturn(UUID.randomUUID().toString()); when(request.getParameter("skillID")).thenReturn(UUID.randomUUID().toString()); @@ -52,7 +49,6 @@ public void itShouldReturnTrueIfValidUserIDAndValidSkillIDIsGiven() { } @Test - @Disabled public void itShouldReturnFalseIfInvalidUserIDIsGiven() { when(request.getParameter("userID")).thenReturn("null"); when(request.getParameter("skillID")).thenReturn(UUID.randomUUID().toString()); @@ -62,7 +58,6 @@ public void itShouldReturnFalseIfInvalidUserIDIsGiven() { } @Test - @Disabled public void itShouldReturnFalseIfInvalidSkillIDIsGiven() { when(request.getParameter("userID")).thenReturn(UUID.randomUUID().toString()); when(request.getParameter("skillID")).thenReturn("null"); @@ -72,7 +67,6 @@ public void itShouldReturnFalseIfInvalidSkillIDIsGiven() { } @Test - @Disabled public void itShouldReturnFalseIfInvalidUserIDAndInvalidSkillIDIsGiven() { when(request.getParameter("userID")).thenReturn("invalid-user-id"); when(request.getParameter("skillID")).thenReturn("invalid-skill-id"); From fab2c173fc573dcf557d08738d0be72cbfd8adc9 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Wed, 7 May 2025 00:52:05 +0530 Subject: [PATCH 07/10] test: integration test for GET /v1/skills/{skillId}/endorsements (#194) * fix: remove harcoded value from test container * fix: add integration tests for GET /v1/skills/{id}/endorsements * fix: refactor the code, remove unused dependence, redundant comments --- .../resources/application-test.properties | 1 + .../RDS/skilltree/TestContainerManager.java | 17 +- .../skills/CreateSkillIntegrationTest.java | 4 +- .../GetAllSkillRequestIntegrationTest.java | 4 +- .../skills/GetAllSkillsIntegrationTest.java | 4 +- ...tEndorsementsBySkillIdIntegrationTest.java | 238 ++++++++++++++++++ .../SkillRequestActionIntegrationTest.java | 4 +- .../src/test/java/utils/TestDataHelper.java | 64 +++++ 8 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetEndorsementsBySkillIdIntegrationTest.java create mode 100644 skill-tree/src/test/java/utils/TestDataHelper.java diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties index fd45b40c..104ec521 100644 --- a/skill-tree/src/main/resources/application-test.properties +++ b/skill-tree/src/main/resources/application-test.properties @@ -1,3 +1,4 @@ cookieName=rds-session-v2-development +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/TestContainerManager.java b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java index e37ab25c..99883b13 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java @@ -1,12 +1,21 @@ package com.RDS.skilltree; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; import org.testcontainers.containers.MySQLContainer; -public abstract class TestContainerManager { - @ServiceConnection static MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8.1.0"); +@TestConfiguration +public class TestContainerManager { + @Value("${test.db.mysql-image}") + private String MYSQL_IMAGE_NAME; - static { - mySQLContainer.start(); + @Bean + @ServiceConnection + public MySQLContainer mySQLContainer() { + MySQLContainer mysqlContainer = new MySQLContainer<>(MYSQL_IMAGE_NAME); + mysqlContainer.start(); + return mysqlContainer; } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java index 941543e5..de7963c5 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java @@ -22,6 +22,7 @@ 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; @@ -33,7 +34,8 @@ @ActiveProfiles("test") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class CreateSkillIntegrationTest extends TestContainerManager { +@Import(TestContainerManager.class) +public class CreateSkillIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private SkillRepository skillRepository; @MockBean private RdsService rdsService; diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java index ac2686ab..3c57a997 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java @@ -27,6 +27,7 @@ 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; @@ -39,7 +40,8 @@ @ActiveProfiles("test") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class GetAllSkillRequestIntegrationTest extends TestContainerManager { +@Import(TestContainerManager.class) +public class GetAllSkillRequestIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserSkillRepository userSkillRepository; diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java index ba475422..2c9bfa30 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java @@ -22,6 +22,7 @@ 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; @@ -33,7 +34,8 @@ @ActiveProfiles("test") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class GetAllSkillsIntegrationTest extends TestContainerManager { +@Import(TestContainerManager.class) +public class GetAllSkillsIntegrationTest { @Autowired private SkillService skillService; diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetEndorsementsBySkillIdIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetEndorsementsBySkillIdIntegrationTest.java new file mode 100644 index 00000000..f91046c9 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetEndorsementsBySkillIdIntegrationTest.java @@ -0,0 +1,238 @@ +package com.RDS.skilltree.integration.skills; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static utils.TestDataHelper.*; + +import com.RDS.skilltree.TestContainerManager; +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +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.services.external.RdsService; +import com.RDS.skilltree.utils.JWTUtils; +import com.RDS.skilltree.viewmodels.EndorsementViewModel; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +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.TestDataHelper; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import({TestContainerManager.class}) +public class GetEndorsementsBySkillIdIntegrationTest { + @Autowired private EndorsementRepository endorsementRepository; + @Autowired private SkillRepository skillRepository; + + @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"; + + @BeforeEach + void setUp() { + skillRepository.deleteAll(); + endorsementRepository.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); + + Claims mockClaims = mock(Claims.class); + when(jwtUtils.validateToken(anyString())).thenReturn(mockClaims); + } + + private String createUrl(Integer skillId) { + return "/v1/skills/" + skillId + "/endorsements"; + } + + 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 createEndorsementViewModel(Endorsement endorsement) { + return TestDataHelper.createEndorsementViewModel(endorsement, rdsService); + } + + private List extractEndorsementsFromResult(MvcResult result) + throws UnsupportedEncodingException, JsonProcessingException { + String responseJson = result.getResponse().getContentAsString(); + return objectMapper.readValue(responseJson, new TypeReference>() {}); + } + + private MvcResult performGetRequest(String url) throws Exception { + return mockMvc + .perform(MockMvcRequestBuilders.get(url).contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + } + + @Test + @DisplayName("Get endorsements for a skill with multiple endorsements") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void getEndorsements_forSkillWithMultipleEndorsements_shouldReturnAllEndorsements() + throws Exception { + Skill skill = createAndSaveSkill("Java"); + + String endorseId = userId1; + String endorserId1 = userId2; + String endorserId2 = superUserId; + String endorsementMessage1 = "Good Java knowledge"; + String endorsementMessage2 = "Excellent Java skills"; + + Endorsement endorsement1 = + createAndSaveEndorsement(skill, endorseId, endorserId1, endorsementMessage1); + Endorsement endorsement2 = + createAndSaveEndorsement(skill, endorseId, endorserId2, endorsementMessage2); + + MvcResult result = performGetRequest(createUrl(skill.getId())); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + List actualEndorsements = extractEndorsementsFromResult(result); + List expectedEndorsements = + Arrays.asList( + createEndorsementViewModel(endorsement1), createEndorsementViewModel(endorsement2)); + + assertThat(actualEndorsements).hasSize(expectedEndorsements.size()); + assertThat(actualEndorsements).usingRecursiveComparison().isEqualTo(expectedEndorsements); + } + + @Test + @DisplayName("Get endorsements for a skill with single endorsement") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void getEndorsements_forSkillWithSingleEndorsement_shouldReturnOneEndorsement() + throws Exception { + Skill skill = createAndSaveSkill("Python"); + + String endorseId = userId2; + String endorserId = superUserId; + String endorsementMessage = "Good Python knowledge"; + + Endorsement endorsement = + createAndSaveEndorsement(skill, endorseId, endorserId, endorsementMessage); + + MvcResult result = performGetRequest(createUrl(skill.getId())); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + List actualEndorsements = extractEndorsementsFromResult(result); + EndorsementViewModel expectedEndorsements = createEndorsementViewModel(endorsement); + + assertThat(actualEndorsements).hasSize(1); + assertThat(actualEndorsements.get(0)) + .usingRecursiveComparison() + .isEqualTo(expectedEndorsements); + } + + @Test + @DisplayName("Get endorsements for a skill with no endorsements") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void getEndorsements_forSkillWithNoEndorsements_shouldReturnEmptyList() throws Exception { + Skill skill = createAndSaveSkill("JavaScript"); + + MvcResult result = performGetRequest(createUrl(skill.getId())); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + List actualEndorsements = extractEndorsementsFromResult(result); + assertThat(actualEndorsements).isEmpty(); + } + + @Test + @DisplayName("Get endorsements for non-existent skill ID") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void getEndorsements_forNonExistentSkillId_shouldReturnEmptyList() throws Exception { + Integer nonExistentSkillId = 999; + + MvcResult result = performGetRequest(createUrl(nonExistentSkillId)); + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + List actualEndorsements = extractEndorsementsFromResult(result); + assertThat(actualEndorsements).isEmpty(); + } + + @Test + @DisplayName("non super-user can access endorsements endpoint") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void normalUser_canAccessEndorsements() throws Exception { + Skill skill = createAndSaveSkill("Java"); + String endorseId = userId1; + String endorserId = superUserId; + String endorsementMessage = "Good Java knowledge"; + + Endorsement endorsement = + createAndSaveEndorsement(skill, endorseId, endorserId, endorsementMessage); + + MvcResult result = performGetRequest(createUrl(skill.getId())); + + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + List actualEndorsements = extractEndorsementsFromResult(result); + EndorsementViewModel expectedEndorsements = createEndorsementViewModel(endorsement); + + assertThat(actualEndorsements).hasSize(1); + assertThat(actualEndorsements.get(0)) + .usingRecursiveComparison() + .isEqualTo(expectedEndorsements); + } + + @Test + @DisplayName("Unauthenticated request returns 401") + public void unauthenticatedRequest_returnsUnauthorized() throws Exception { + Skill skill = createAndSaveSkill("Java"); + + MvcResult result = performGetRequest(createUrl(skill.getId())); + assertThat(result.getResponse().getStatus()).isEqualTo(401); + } +} diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java index 7ee5b016..8e406c0e 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java @@ -23,6 +23,7 @@ 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; @@ -34,7 +35,8 @@ @ActiveProfiles("test") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class SkillRequestActionIntegrationTest extends TestContainerManager { +@Import(TestContainerManager.class) +public class SkillRequestActionIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserSkillRepository userSkillRepository; @Autowired private SkillRepository skillRepository; diff --git a/skill-tree/src/test/java/utils/TestDataHelper.java b/skill-tree/src/test/java/utils/TestDataHelper.java new file mode 100644 index 00000000..5daab5ac --- /dev/null +++ b/skill-tree/src/test/java/utils/TestDataHelper.java @@ -0,0 +1,64 @@ +package utils; + +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.enums.SkillTypeEnum; +import com.RDS.skilltree.models.Endorsement; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.services.external.RdsService; +import com.RDS.skilltree.viewmodels.EndorsementViewModel; +import com.RDS.skilltree.viewmodels.RdsUserViewModel; +import com.RDS.skilltree.viewmodels.UserViewModel; + +public class TestDataHelper { + public static RdsGetUserDetailsResDto createUserDetails(String userId, boolean isSuperUser) { + RdsUserViewModel user = new RdsUserViewModel(); + user.setId(userId); + + // set a dummy name + user.setFirst_name(userId); + user.setLast_name(userId); + + RdsUserViewModel.Roles roles = new RdsUserViewModel.Roles(); + roles.setSuper_user(isSuperUser); + user.setRoles(roles); + + RdsGetUserDetailsResDto userDetails = new RdsGetUserDetailsResDto(); + userDetails.setUser(user); + return userDetails; + } + + public static EndorsementViewModel createEndorsementViewModel( + Endorsement endorsement, RdsService rdsService) { + RdsUserViewModel endorseDetail = + rdsService.getUserDetails(endorsement.getEndorseId()).getUser(); + RdsUserViewModel endorserDetail = + rdsService.getUserDetails(endorsement.getEndorserId()).getUser(); + + UserViewModel endorse = + UserViewModel.builder() + .id(endorseDetail.getId()) + .name(endorseDetail.getFirst_name() + " " + endorseDetail.getLast_name()) + .build(); + UserViewModel endorser = + UserViewModel.builder() + .id(endorserDetail.getId()) + .name(endorserDetail.getFirst_name() + " " + endorserDetail.getLast_name()) + .build(); + + return EndorsementViewModel.toViewModel(endorsement, endorse, endorser); + } + + public static Endorsement createEndorsement( + Skill skill, String endorseId, String endorserId, String message) { + return Endorsement.builder() + .endorserId(endorserId) + .endorseId(endorseId) + .skill(skill) + .message(message) + .build(); + } + + public static Skill createSkill(String skillName, String userId) { + return Skill.builder().name(skillName).createdBy(userId).type(SkillTypeEnum.ATOMIC).build(); + } +} From 55c32a2338dd4fc7dc6fc552bcc9d3c34f41b00f Mon Sep 17 00:00:00 2001 From: Mohit Ramani <75924391+mbramani@users.noreply.github.com> Date: Sun, 11 May 2025 00:24:09 +0530 Subject: [PATCH 08/10] fix: restrict endorsement requests visibility to users own requests (#196) * fix: restrict endorsement requests to only those user has endorsed * test: verify endorsement request privacy controls * chore: add dev feature flag * fix: simplify skill request retrieval logic for superusers and non dev feature flag * fix: optimize user view model retrieval * test: enhance integration tests * fix: optimize endorsement retrieval by reducing redundant user detail fetches * fix: update skill request retrieval logic * refactor: rename devMode parameter to isDev for clarity in SkillService and SkillsApi * refactor: remove unused findByEndorseIdAndSkillIdAndEndorserId method from EndorsementRepository * test: add integration negative tests for skill requests with dev flag scenarios --------- Co-authored-by: Mayank Bansal --- .../com/RDS/skilltree/apis/SkillsApi.java | 8 +- .../repositories/UserSkillRepository.java | 27 ++++++- .../RDS/skilltree/services/SkillService.java | 4 +- .../services/SkillServiceImplementation.java | 45 +++++++---- .../GetAllSkillRequestIntegrationTest.java | 74 +++++++++++++++++++ .../test/java/utils/CustomResultMatchers.java | 20 +++++ 6 files changed, 156 insertions(+), 22 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java index 0515e27e..c57fd561 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/SkillsApi.java @@ -36,12 +36,14 @@ public ResponseEntity> getAll() { @GetMapping("/requests") public ResponseEntity getAllRequests( - @RequestParam(value = "status", required = false) UserSkillStatusEnum status) { + @RequestParam(name = "status", required = false) UserSkillStatusEnum status, + @RequestParam(name = "dev", required = false, defaultValue = "false") boolean isDev) { + if (status != null) { - return ResponseEntity.ok(skillService.getRequestsByStatus(status)); + return ResponseEntity.ok(skillService.getRequestsByStatus(status, isDev)); } - return ResponseEntity.ok(skillService.getAllRequests()); + return ResponseEntity.ok(skillService.getAllRequests(isDev)); } @PostMapping("/requests/{skillId}/action") diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java index 12868976..ee45d77d 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java @@ -12,9 +12,28 @@ public interface UserSkillRepository extends JpaRepository List findByUserIdAndSkillId(String userId, Integer skillId); - @Query( - "SELECT us FROM UserSkills us " - + "JOIN Endorsement e ON us.userId = e.endorseId " - + "WHERE e.endorserId = :endorserId") + @Query(""" + SELECT us FROM UserSkills us + JOIN Endorsement e ON us.userId = e.endorseId + WHERE e.endorserId = :endorserId + """) + List findUserSkillsByEndorserIdLegacy(@Param("endorserId") String endorserId); + + @Query(""" + SELECT us FROM UserSkills us + JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id + WHERE e.endorserId = :endorserId + """) List findUserSkillsByEndorserId(@Param("endorserId") String endorserId); + + @Query(""" + SELECT us FROM UserSkills us + JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id + WHERE e.endorserId = :endorserId AND us.status = :status + """) + List findByStatusAndEndorserId( + @Param("status") UserSkillStatusEnum status, + @Param("endorserId") String endorserId + ); + } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/SkillService.java b/skill-tree/src/main/java/com/RDS/skilltree/services/SkillService.java index ce87d119..618e6fdd 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/SkillService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/SkillService.java @@ -12,9 +12,9 @@ public interface SkillService { SkillViewModel create(CreateSkillViewModel skill); - SkillRequestsDto getAllRequests(); + SkillRequestsDto getAllRequests(boolean isDev); - SkillRequestsDto getRequestsByStatus(UserSkillStatusEnum status); + SkillRequestsDto getRequestsByStatus(UserSkillStatusEnum status, boolean isDev); GenericResponse approveRejectSkillRequest( Integer skillId, String endorseId, UserSkillStatusEnum action); diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/SkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/SkillServiceImplementation.java index 153ea33a..e08d1923 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/SkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/SkillServiceImplementation.java @@ -54,7 +54,7 @@ public List getAll() { } @Override - public SkillRequestsDto getAllRequests() { + public SkillRequestsDto getAllRequests(boolean isDev) { JwtUser jwtDetails = (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); @@ -66,8 +66,10 @@ public SkillRequestsDto getAllRequests() { if (userRole.isSuper_user()) { skillRequests = userSkillRepository.findAll(); - } else { + } else if (isDev) { skillRequests = userSkillRepository.findUserSkillsByEndorserId(userId); + } else { + skillRequests = userSkillRepository.findUserSkillsByEndorserIdLegacy(userId); } if (skillRequests == null) { @@ -82,8 +84,26 @@ public SkillRequestsDto getAllRequests() { } @Override - public SkillRequestsDto getRequestsByStatus(UserSkillStatusEnum status) { - List skillRequests = userSkillRepository.findByStatus(status); + public SkillRequestsDto getRequestsByStatus(UserSkillStatusEnum status, boolean isDev) { + JwtUser jwtDetails = + (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + RdsGetUserDetailsResDto userDetails = rdsService.getUserDetails(jwtDetails.getRdsUserId()); + RdsUserViewModel.Roles userRole = userDetails.getUser().getRoles(); + String userId = userDetails.getUser().getId(); + + List skillRequests = null; + + if (!isDev || userRole.isSuper_user()) { + skillRequests = userSkillRepository.findByStatus(status); + } else { + skillRequests = userSkillRepository.findByStatusAndEndorserId(status, userId); + } + + if (skillRequests == null) { + throw new InternalServerErrorException("Unable to fetch skill requests"); + } + SkillRequestsWithUserDetailsViewModel skillRequestsWithUserDetails = toSkillRequestsWithUserDetailsViewModel(skillRequests); @@ -129,39 +149,38 @@ public GenericResponse approveRejectSkillRequest( private SkillRequestsWithUserDetailsViewModel toSkillRequestsWithUserDetailsViewModel( List skills) { + // store all users data that are a part of this request - Map userDetails = new HashMap<>(); + Map userViewModelMap = new HashMap<>(); List skillRequests = skills.stream() .map( skill -> { Integer skillId = skill.getSkill().getId(); - String endorseId = skill.getUserId(); - // Get all endorsement for a specific skill and user Id List endorsements = endorsementRepository.findByEndorseIdAndSkillId(endorseId, skillId); - if (!userDetails.containsKey(endorseId)) { + if (!userViewModelMap.containsKey(endorseId)) { RdsGetUserDetailsResDto endorseRdsDetails = rdsService.getUserDetails(endorseId); UserViewModel endorseDetails = getUserModalFromRdsDetails(endorseId, endorseRdsDetails); - userDetails.put(endorseId, endorseDetails); + userViewModelMap.put(endorseId, endorseDetails); } + // Add details of the endorsers endorsements.forEach( endorsement -> { String endorserId = endorsement.getEndorserId(); - - if (!userDetails.containsKey(endorserId)) { + if (!userViewModelMap.containsKey(endorserId)) { RdsGetUserDetailsResDto endorserRdsDetails = rdsService.getUserDetails(endorserId); UserViewModel endorserDetails = getUserModalFromRdsDetails(endorserId, endorserRdsDetails); - userDetails.put(endorserId, endorserDetails); + userViewModelMap.put(endorserId, endorserDetails); } }); @@ -171,7 +190,7 @@ private SkillRequestsWithUserDetailsViewModel toSkillRequestsWithUserDetailsView return SkillRequestsWithUserDetailsViewModel.builder() .skillRequests(skillRequests) - .users(userDetails.values().stream().toList()) + .users(userViewModelMap.values().stream().toList()) .build(); } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java index 3c57a997..5f13355b 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java @@ -203,6 +203,28 @@ public void getAllRequests_asNormalUser_shouldReturnAllRequestsByEndorser() thro .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); } + @Test + @DisplayName( + "Normal user with dev flag - should only see skills they've endorsed with all endorsements") + @WithCustomMockUser( + username = "user-id-2", + authorities = {"USER"}) + public void getAllRequests_asNormalUserWithDevFlag_shouldOnlySeeSkillsTheyEndorsed() + throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?dev=true").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id", "user-id-2", "skill2 for user-id")) + .andExpect(CustomResultMatchers.doesNotHaveSkillRequest("Java", "user-id")) + .andExpect(CustomResultMatchers.doesNotHaveSkillRequest("Springboot", "user-id-2")) + .andExpect(CustomResultMatchers.hasUser("user-id", " ")) + .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); + } + @Test @DisplayName("Filter requests by status - should return filtered requests") @WithCustomMockUser( @@ -222,6 +244,29 @@ public void getAllRequests_ByStatus_ShouldReturnFilteredRequests() throws Except .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); } + @Test + @DisplayName( + "Normal user with dev flag filtering by status - should only see endorsed skills with matching status") + @WithCustomMockUser( + username = "user-id-2", + authorities = {"USER"}) + public void getRequests_asNormalUserByStatusWithDevFlag_shouldOnlyShowEndorsedMatchingRequests() + throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=APPROVED&dev=true") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(CustomResultMatchers.hasSkillRequest("Springboot", "user-id", "APPROVED")) + .andExpect( + CustomResultMatchers.hasEndorsement( + "Springboot", "user-id", "user-id-2", "skill2 for user-id")) + .andExpect(CustomResultMatchers.doesNotHaveSkillRequest("Java", "user-id")) + .andExpect(CustomResultMatchers.doesNotHaveSkillRequest("Springboot", "user-id-2")) + .andExpect(CustomResultMatchers.hasUser("user-id", " ")) + .andExpect(CustomResultMatchers.hasUser("user-id-2", " ")); + } + @Test @DisplayName("If no skill Requests endorsed by user then return empty lists") @WithCustomMockUser( @@ -235,6 +280,20 @@ public void noSkillRequestsEndorsedByUser_ShouldReturnEmptyLists() throws Except .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); } + @Test + @DisplayName("If no skill Requests endorsed by user with dev flag then return empty lists") + @WithCustomMockUser( + username = "user-id", + authorities = {"USER"}) + public void noSkillRequestsEndorsedByUser_WithDevFlag_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?dev=true").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) + .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); + } + @Test @DisplayName("If no matching skill requests by status then return empty lists") @WithCustomMockUser( @@ -250,6 +309,21 @@ public void noMatchingRequestsByStatus_ShouldReturnEmptyLists() throws Exception .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); } + @Test + @DisplayName("If no matching skill requests by status with dev flag then return empty lists") + @WithCustomMockUser( + username = "user-id", + authorities = {"USER"}) + public void noMatchingRequestsByStatus_WithDevFlag_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=REJECTED&dev=true") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(MockMvcResultMatchers.jsonPath("$.requests").isEmpty()) + .andExpect(MockMvcResultMatchers.jsonPath("$.users").isEmpty()); + } + @Test @DisplayName("If no skill requests in DB - return empty lists") @WithCustomMockUser( diff --git a/skill-tree/src/test/java/utils/CustomResultMatchers.java b/skill-tree/src/test/java/utils/CustomResultMatchers.java index 5eb85e6c..2c354a94 100644 --- a/skill-tree/src/test/java/utils/CustomResultMatchers.java +++ b/skill-tree/src/test/java/utils/CustomResultMatchers.java @@ -67,6 +67,26 @@ public static ResultMatcher hasUser(String userId, String name) { }; } + public static ResultMatcher doesNotHaveSkillRequest(String skillName, String endorseId) { + return result -> { + String json = result.getResponse().getContentAsString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode root = objectMapper.readTree(json); + + JsonNode requests = root.get("requests"); + if (requests == null || requests.isEmpty()) { + // If requests is null or empty, the test passes as the request definitely doesn't exist + return; + } + + // Find the request for the given skillName and endorseId + JsonNode matchingRequest = findBySkillAndEndorseId(requests, skillName, endorseId); + + // Assert that the request is null + assertThat(matchingRequest).isNull(); + }; + } + private static JsonNode findByField(JsonNode array, String fieldName, String value) { for (JsonNode node : array) { if (node.has(fieldName) && node.get(fieldName).asText().equals(value)) { From afb7a766682dafd4cd952ba96ff389dee611043d Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Thu, 15 May 2025 23:00:33 +0530 Subject: [PATCH 09/10] fix: avoid creating duplicate endorsements, add integration-tests for endorsement creation (#198) * fix: avoid creating duplicate endorsements, add integration tests * fix: update name of the test * fix: update formatting * test: add test to validate skill creation on a diff skill than those that have endorsements * fix: update query formatting * fix: update query to scan only endorsement table, as joining user-skills is redundant * test: add test-case to test flow when user is unauthorized * fix: update test name, createUrl(), log * add class to contain constant messages across app * fix: use constants defined in Constants.java * fix: use constants defined in Constants.java * fix: make constructor of Constants private to prevent instantiation * fix: import ExceptionMessages from Constants * fix: update status from 409 to 405 * fix: update query, subquery is redundant * fix: add logging level for each envioronment * fix: avoid appending timestamps, message etc, update error message * fix: add constructor for GenericResponse to create obj with message only --- .../EndorsementAlreadyExistsException.java | 7 + .../exceptions/GlobalExceptionHandler.java | 98 ++--- .../repositories/EndorsementRepository.java | 14 + .../EndorsementServiceImplementation.java | 24 +- .../com/RDS/skilltree/utils/Constants.java | 12 + .../RDS/skilltree/utils/GenericResponse.java | 4 + .../application-production.properties | 3 +- .../resources/application-staging.properties | 3 +- .../resources/application-test.properties | 1 + .../src/main/resources/logback-test.xml | 14 - .../CreateEndorsementIntegrationTest.java | 370 ++++++++++++++++++ 11 files changed, 472 insertions(+), 78 deletions(-) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/exceptions/EndorsementAlreadyExistsException.java create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java delete mode 100644 skill-tree/src/main/resources/logback-test.xml create mode 100644 skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateEndorsementIntegrationTest.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/EndorsementAlreadyExistsException.java b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/EndorsementAlreadyExistsException.java new file mode 100644 index 00000000..b6012021 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/EndorsementAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.RDS.skilltree.exceptions; + +public class EndorsementAlreadyExistsException extends RuntimeException { + public EndorsementAlreadyExistsException(String message) { + super(message); + } +} 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 e5abe97c..3af16875 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 @@ -2,10 +2,7 @@ import com.RDS.skilltree.utils.GenericResponse; import jakarta.validation.ConstraintViolationException; -import java.time.LocalDateTime; -import java.util.HashMap; import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.tomcat.websocket.AuthenticationException; import org.springframework.http.HttpStatus; @@ -16,16 +13,15 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; @Slf4j @ControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler({NoEntityException.class}) public ResponseEntity> handleNoEntityException(NoEntityException ex) { - log.error("NoEntityException - Error : {}", ex.getMessage(), ex); - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new GenericResponse<>(null, ex.getMessage())); + log.error("NoEntityException - Error : {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new GenericResponse<>(ex.getMessage())); } @ExceptionHandler({AuthenticationException.class, InsufficientAuthenticationException.class}) @@ -40,25 +36,21 @@ public ResponseEntity> handleInvalidBearerTokenException @ExceptionHandler({AccessDeniedException.class}) public ResponseEntity> handleAccessDeniedException( AccessDeniedException ex) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new GenericResponse<>(null, ex.getMessage())); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new GenericResponse<>(ex.getMessage())); } @ExceptionHandler({EntityAlreadyExistsException.class}) public ResponseEntity> handleEntityAlreadyExistsException( EntityAlreadyExistsException ex) { - log.error("EntityAlreadyExistsException - Error : {}", ex.getMessage(), ex); - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(new GenericResponse<>(null, ex.getMessage())); + log.error("EntityAlreadyExistsException - Error : {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).body(new GenericResponse<>(ex.getMessage())); } @ExceptionHandler({RuntimeException.class}) public ResponseEntity> handleRuntimeException(RuntimeException ex) { - log.error("Runtime Exception - Error : {}", ex.getMessage(), ex); + log.error("Runtime Exception - Error : {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - new GenericResponse<>( - null, "Runtime Exception - Something went wrong, please try again.")); + .body(new GenericResponse<>("Runtime Exception - Something went wrong, please try again.")); } @ExceptionHandler({MethodArgumentNotValidException.class}) @@ -73,85 +65,81 @@ public ResponseEntity> handleMethodArgumentNotValidExcep } log.error("MethodArgumentNotValidException Exception - Error : {}", ex.getMessage(), ex); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new GenericResponse<>(null, errorString.toString().trim())); + .body(new GenericResponse<>(errorString.toString().trim())); } @ExceptionHandler({Exception.class}) public ResponseEntity> handleException(Exception ex) { - log.error("Exception - Error : {}", ex.getMessage(), ex); + log.error("Exception - Error : {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(new GenericResponse<>(null, "Something unexpected happened, please try again.")); + .body(new GenericResponse<>("Something unexpected happened, please try again.")); } @ExceptionHandler({InvalidParameterException.class}) public ResponseEntity> handleException(InvalidParameterException ex) { - log.error("Exception - Error : {}", ex.getMessage(), ex); + log.error("InvalidParameterException - Error : {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new GenericResponse<>(null, ex.getMessage())); + .body(new GenericResponse<>(ex.getMessage())); } @ExceptionHandler({ConstraintViolationException.class}) public ResponseEntity> handleException(ConstraintViolationException ex) { - log.error("Exception - Error : {}", ex.getMessage(), ex); + log.error("ConstraintViolationException - Error : {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new GenericResponse<>(null, ex.getMessage())); + .body(new GenericResponse<>(ex.getMessage())); } @ExceptionHandler(UserNotFoundException.class) - public ResponseEntity handleUserNotFoundException( - UserNotFoundException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); - return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.NOT_FOUND); + public ResponseEntity handleUserNotFoundException(UserNotFoundException ex) { + log.error("UserNotFoundException - Error : {}", ex.getMessage()); + return new ResponseEntity<>(new GenericResponse<>(ex.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler(SkillAlreadyExistsException.class) - public ResponseEntity handleSkillAlreadyExistsException( - SkillAlreadyExistsException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); - return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.CONFLICT); + public ResponseEntity handleSkillAlreadyExistsException(SkillAlreadyExistsException ex) { + log.error("SkillAlreadyExistsException - Error : {}", ex.getMessage()); + return new ResponseEntity<>(new GenericResponse<>(ex.getMessage()), HttpStatus.CONFLICT); } @ExceptionHandler(SelfEndorsementNotAllowedException.class) public ResponseEntity handleSelfEndorsementNotAllowedException( - SelfEndorsementNotAllowedException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); + SelfEndorsementNotAllowedException ex) { + log.error("SelfEndorsementNotAllowedException - Error : {}", ex.getMessage()); return new ResponseEntity<>( - new GenericResponse<>(null, ex.getMessage()), HttpStatus.METHOD_NOT_ALLOWED); + new GenericResponse<>(ex.getMessage()), HttpStatus.METHOD_NOT_ALLOWED); } @ExceptionHandler(SkillNotFoundException.class) - public ResponseEntity handleSkillNotFoundException( - SkillNotFoundException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); - return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.NOT_FOUND); + public ResponseEntity handleSkillNotFoundException(SkillNotFoundException ex) { + log.error("SkillNotFoundException - Error : {}", ex.getMessage()); + return new ResponseEntity<>(new GenericResponse<>(ex.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler(EndorsementNotFoundException.class) - public ResponseEntity handleEndorsementNotException( - EndorsementNotFoundException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); - return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.NOT_FOUND); + public ResponseEntity handleEndorsementNotFoundException(EndorsementNotFoundException ex) { + log.error("EndorsementNotFoundException - Error : {}", ex.getMessage()); + return new ResponseEntity<>(new GenericResponse<>(ex.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler(ForbiddenException.class) - public ResponseEntity handleForbiddenException(ForbiddenException ex, WebRequest request) { - log.error("Exception - Error : {}", ex.getMessage(), ex); - return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.FORBIDDEN); + public ResponseEntity handleForbiddenException(ForbiddenException ex) { + log.error("ForbiddenException - Error : {}", ex.getMessage()); + return new ResponseEntity<>(new GenericResponse<>(ex.getMessage()), HttpStatus.FORBIDDEN); } @ExceptionHandler(InternalServerErrorException.class) - public ResponseEntity handleInternalServerErrorException( - InternalServerErrorException ex, WebRequest request) { + public ResponseEntity handleInternalServerErrorException(InternalServerErrorException ex) { log.error("Internal Server Error", ex); - // Create a more specific error message based on the exception type or cause String errorMessage = "An unexpected error occurred."; + return new ResponseEntity<>( + new GenericResponse<>(errorMessage), HttpStatus.INTERNAL_SERVER_ERROR); + } - // Consider adding more details to the response for debugging - Map errorDetails = new HashMap<>(); - errorDetails.put("timestamp", LocalDateTime.now()); - errorDetails.put("message", errorMessage); - errorDetails.put("details", ex.getMessage()); // Include exception details for debugging - - return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); + @ExceptionHandler(EndorsementAlreadyExistsException.class) + public ResponseEntity handleEndorsementAlreadyExistsException( + EndorsementAlreadyExistsException ex) { + log.error("EndorsementAlreadyExistsException - 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/repositories/EndorsementRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java index 9b8d3c2f..1efe5417 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java @@ -3,9 +3,23 @@ import com.RDS.skilltree.models.Endorsement; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface EndorsementRepository extends JpaRepository { List findBySkillId(Integer skillId); List findByEndorseIdAndSkillId(String endorseId, Integer skillId); + + @Query(""" + SELECT (COUNT(*) > 0) AS exists + FROM Endorsement e + WHERE e.endorserId = :endorserId + AND e.endorseId = :endorseId + AND e.skill.id = :skillId + """) + boolean existsByEndorseIdAndEndorserIdAndSkillId( + @Param("endorseId") String endorseId, + @Param("endorserId") String endorserId, + @Param("skillId") Integer skillId); } 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 b5fd5a10..1c6d25c2 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 @@ -1,6 +1,7 @@ package com.RDS.skilltree.services; import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.exceptions.EndorsementAlreadyExistsException; import com.RDS.skilltree.exceptions.EndorsementNotFoundException; import com.RDS.skilltree.exceptions.SelfEndorsementNotAllowedException; import com.RDS.skilltree.exceptions.SkillNotFoundException; @@ -12,6 +13,7 @@ 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.viewmodels.CreateEndorsementViewModel; import com.RDS.skilltree.viewmodels.EndorsementViewModel; import com.RDS.skilltree.viewmodels.UpdateEndorsementViewModel; @@ -63,7 +65,6 @@ public List getAllEndorsementsBySkillId(Integer skillId) { } @Override - // TODO : add a check for when a endorsement is already created by a user for a particular skill. public EndorsementViewModel create(CreateEndorsementViewModel endorsementViewModel) { String message = endorsementViewModel.getMessage(); Integer skillId = endorsementViewModel.getSkillId(); @@ -75,19 +76,28 @@ public EndorsementViewModel create(CreateEndorsementViewModel endorsementViewMod String endorserId = jwtDetails.getRdsUserId(); if (Objects.equals(endorseId, endorserId)) { - log.info( + log.warn( "Self endorsement not allowed, endorseId: {}, endorserId: {}", endorseId, endorserId); - throw new SelfEndorsementNotAllowedException("Self endorsement not allowed"); + throw new SelfEndorsementNotAllowedException(ExceptionMessages.SELF_ENDORSEMENT_NOT_ALLOWED); } Optional skillDetails = skillRepository.findById(skillId); if (skillDetails.isEmpty()) { - log.info(String.format("Skill id: %s not found", skillId)); - throw new SkillNotFoundException("Skill does not exist"); + log.info("Skill id: {} not found", skillId); + throw new SkillNotFoundException(ExceptionMessages.SKILL_NOT_FOUND); + } + + if (endorsementRepository.existsByEndorseIdAndEndorserIdAndSkillId( + endorseId, endorserId, skillId)) { + log.info( + "Endorsement already exists for endorseId: {}, endorserId: {}, skillId: {}", + endorseId, + endorserId, + skillId); + throw new EndorsementAlreadyExistsException(ExceptionMessages.ENDORSEMENT_ALREADY_EXISTS); } - // Get endorse(person being endorsed) & endorser(person endorsing) details from RDS RdsGetUserDetailsResDto endorseDetails = rdsService.getUserDetails(endorseId); RdsGetUserDetailsResDto endorserDetails = rdsService.getUserDetails(endorserId); @@ -125,7 +135,7 @@ public EndorsementViewModel update(Integer endorsementId, UpdateEndorsementViewM if (exitingEndorsement.isEmpty()) { log.info(String.format("Endorsement with id: %s not found", endorsementId)); - throw new EndorsementNotFoundException("Endorsement not found"); + throw new EndorsementNotFoundException(ExceptionMessages.ENDORSEMENT_NOT_FOUND); } Endorsement endorsement = exitingEndorsement.get(); 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 new file mode 100644 index 00000000..da443a25 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/Constants.java @@ -0,0 +1,12 @@ +package com.RDS.skilltree.utils; + +public class Constants { + private Constants() {} + + public static final class ExceptionMessages { + public static final String SELF_ENDORSEMENT_NOT_ALLOWED = "Self endorsement not allowed"; + 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"; + } +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/GenericResponse.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/GenericResponse.java index 70d73df3..83a8a3ce 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/GenericResponse.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/GenericResponse.java @@ -14,4 +14,8 @@ public class GenericResponse { private T data; private String message; + + public GenericResponse(String message) { + this.message = message; + } } diff --git a/skill-tree/src/main/resources/application-production.properties b/skill-tree/src/main/resources/application-production.properties index 26d4250e..69f872f5 100644 --- a/skill-tree/src/main/resources/application-production.properties +++ b/skill-tree/src/main/resources/application-production.properties @@ -1 +1,2 @@ -cookieName=rds-session-v2 \ No newline at end of file +cookieName=rds-session-v2 +logging.level.root=ERROR \ No newline at end of file diff --git a/skill-tree/src/main/resources/application-staging.properties b/skill-tree/src/main/resources/application-staging.properties index abe06d79..598a2335 100644 --- a/skill-tree/src/main/resources/application-staging.properties +++ b/skill-tree/src/main/resources/application-staging.properties @@ -1 +1,2 @@ -cookieName=rds-session-v2-staging \ No newline at end of file +cookieName=rds-session-v2-staging +logging.level.root=WARN \ No newline at end of file diff --git a/skill-tree/src/main/resources/application-test.properties b/skill-tree/src/main/resources/application-test.properties index 104ec521..55ca50d8 100644 --- a/skill-tree/src/main/resources/application-test.properties +++ b/skill-tree/src/main/resources/application-test.properties @@ -2,3 +2,4 @@ cookieName=rds-session-v2-development test.db.mysql-image=mysql:8.1.0 spring.flyway.enabled=true spring.flyway.locations=classpath:db/migrations +logging.level.root=WARN diff --git a/skill-tree/src/main/resources/logback-test.xml b/skill-tree/src/main/resources/logback-test.xml deleted file mode 100644 index 3279a689..00000000 --- a/skill-tree/src/main/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateEndorsementIntegrationTest.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateEndorsementIntegrationTest.java new file mode 100644 index 00000000..0f253f49 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateEndorsementIntegrationTest.java @@ -0,0 +1,370 @@ +package com.RDS.skilltree.integration.skills; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static utils.TestDataHelper.createUserDetails; + +import com.RDS.skilltree.TestContainerManager; +import com.RDS.skilltree.dtos.CreateEndorsementRequestDto; +import com.RDS.skilltree.dtos.RdsGetUserDetailsResDto; +import com.RDS.skilltree.exceptions.EndorsementAlreadyExistsException; +import com.RDS.skilltree.exceptions.SelfEndorsementNotAllowedException; +import com.RDS.skilltree.exceptions.SkillNotFoundException; +import com.RDS.skilltree.models.Endorsement; +import com.RDS.skilltree.models.Skill; +import com.RDS.skilltree.models.UserSkills; +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.fasterxml.jackson.databind.ObjectMapper; +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.TestDataHelper; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import({TestContainerManager.class}) +public class CreateEndorsementIntegrationTest { + @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 = "Spring Boot"; + private final String ENDORSEMENT_MESSAGE = "Proficient in Spring Boot"; + + @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 String createUrl(Integer skillId) { + return String.format("/v1/skills/%d/endorsements", skillId); + } + + private MvcResult performPostRequest(String url, String requestBody) throws Exception { + return mockMvc + .perform( + MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andReturn(); + } + + private Skill createAndSaveSkill(String skillName) { + return skillRepository.save(TestDataHelper.createSkill(skillName, superUserId)); + } + + private CreateEndorsementRequestDto createEndorsementRequest(String endorseId, String message) { + CreateEndorsementRequestDto endorsementRequest = new CreateEndorsementRequestDto(); + endorsementRequest.setEndorseId(endorseId); + endorsementRequest.setMessage(message); + return endorsementRequest; + } + + private EndorsementViewModel createExpectedEndorsement( + Skill skill, String endorseId, String endorserId, String message) { + Endorsement endorsement = + TestDataHelper.createEndorsement(skill, endorseId, endorserId, message); + return TestDataHelper.createEndorsementViewModel(endorsement, rdsService); + } + + 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); + } + + @Test + @DisplayName("Happy flow for superuser - create endorsement for a skill") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void createEndorsement_asSuperUser_shouldCreateEndorsementSuccessfully() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = userId1; + String endorserId = superUserId; + String message = ENDORSEMENT_MESSAGE; + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(endorseId, message)); + + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(201); + assertThat(result.getResponse().getContentAsString()).isNotEmpty(); + + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + EndorsementViewModel expectedEndorsement = + createExpectedEndorsement(skill, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement, expectedEndorsement); + } + + @Test + @DisplayName("Happy flow for regular user - create endorsement for a skill") + @WithCustomMockUser( + username = userId2, + authorities = {"USER"}) + public void createEndorsement_asRegularUser_shouldCreateEndorsementSuccessfully() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = userId1; + String endorserId = userId2; + String message = ENDORSEMENT_MESSAGE; + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(endorseId, message)); + + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(201); + assertThat(result.getResponse().getContentAsString()).isNotEmpty(); + + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + EndorsementViewModel expectedEndorsement = + createExpectedEndorsement(skill, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement, expectedEndorsement); + } + + @Test + @DisplayName("Error case - self-endorsement not allowed") + @WithCustomMockUser( + username = userId1, + authorities = {"USER"}) + public void createEndorsement_selfEndorsement_shouldReturnError() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = userId1; // self-endorsement + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(endorseId, ENDORSEMENT_MESSAGE)); + + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(405); + + assertThat(result.getResolvedException()) + .isInstanceOf(SelfEndorsementNotAllowedException.class); + assertThat(requireNonNull(result.getResolvedException()).getMessage()) + .isEqualTo(ExceptionMessages.SELF_ENDORSEMENT_NOT_ALLOWED); + + assertThat(endorsementRepository.count()).isZero(); + } + + @Test + @DisplayName("Error case - non-existent skill ID") + @WithCustomMockUser( + username = userId2, + authorities = {"USER"}) + public void createEndorsement_nonExistentSkill_shouldReturnError() throws Exception { + Integer nonExistentSkillId = 9999; + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(userId1, ENDORSEMENT_MESSAGE)); + + MvcResult result = performPostRequest(createUrl(nonExistentSkillId), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(404); + + assertThat(result.getResolvedException()).isInstanceOf(SkillNotFoundException.class); + assertThat(requireNonNull(result.getResolvedException()).getMessage()) + .isEqualTo(ExceptionMessages.SKILL_NOT_FOUND); + + assertThat(endorsementRepository.count()).isZero(); + } + + @Test + @DisplayName("Edge case - first endorsement creates UserSkills entry") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void createEndorsement_firstEndorsement_shouldCreateUserSkillsEntry() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = userId1; + String endorserId = superUserId; + String message = ENDORSEMENT_MESSAGE; + + // no user-skills entry exists before the test + assertThat(userSkillRepository.findByUserIdAndSkillId(endorseId, skill.getId())).isEmpty(); + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(endorseId, message)); + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(201); + + // user-skills entry was created + assertThat(userSkillRepository.findByUserIdAndSkillId(endorseId, skill.getId())).isNotEmpty(); + + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + EndorsementViewModel expectedEndorsement = + createExpectedEndorsement(skill, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement, expectedEndorsement); + } + + @Test + @DisplayName("Edge case - endorsement with existing UserSkills entry") + @WithCustomMockUser( + username = superUserId, + authorities = {"SUPERUSER"}) + public void createEndorsement_withExistingUserSkills_shouldCreateEndorsement() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + String endorseId = userId1; + String endorserId = superUserId; + String message = ENDORSEMENT_MESSAGE; + + // user-skills entry before the test + UserSkills userSkill = new UserSkills(); + userSkill.setUserId(endorseId); + userSkill.setSkill(skill); + userSkillRepository.save(userSkill); + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(endorseId, message)); + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(201); + + EndorsementViewModel actualEndorsement = extractEndorsementFromResult(result); + EndorsementViewModel expectedEndorsement = + createExpectedEndorsement(skill, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement, expectedEndorsement); + + // no duplicate user-skills entry created + assertThat(userSkillRepository.findByUserIdAndSkillId(endorseId, skill.getId())).hasSize(1); + } + + @Test + @DisplayName("Error case - if endorsement already exists, do not create a duplicate") + @WithCustomMockUser( + username = userId2, + authorities = {"USER"}) + public void createEndorsement_withExistingEndorsement_shouldNotCreateDuplicateEndorsement() + throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(userId1, ENDORSEMENT_MESSAGE)); + + // create the first endorsement + performPostRequest(createUrl(skill.getId()), requestBody); + + // assert the first endorsement was created + assertThat(endorsementRepository.count()).isEqualTo(1); + + // try to create a duplicate endorsement + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + + assertThat(result.getResponse().getStatus()).isEqualTo(405); + assertThat(result.getResolvedException()).isInstanceOf(EndorsementAlreadyExistsException.class); + assertThat(requireNonNull(result.getResolvedException()).getMessage()) + .isEqualTo(ExceptionMessages.ENDORSEMENT_ALREADY_EXISTS); + + assertThat(endorsementRepository.count()).isEqualTo(1); + } + + @Test + @DisplayName( + "Edge case - if endorsement exists on a different skill, should create endorsement on new skill") + @WithCustomMockUser( + username = userId2, + authorities = {"USER"}) + public void + createEndorsement_withExistingEndorsementOnDiffSkill_shouldCreateEndorsementOnNewSkill() + throws Exception { + Skill skill1 = createAndSaveSkill(SKILL_NAME); + Skill skill2 = createAndSaveSkill("Python"); + + String endorseId = userId1; + String endorserId = userId2; + String message = ENDORSEMENT_MESSAGE; + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(userId1, ENDORSEMENT_MESSAGE)); + + // create the endorsement for skill1 + MvcResult result1 = performPostRequest(createUrl(skill1.getId()), requestBody); + + EndorsementViewModel actualEndorsement1 = extractEndorsementFromResult(result1); + EndorsementViewModel expectedEndorsement1 = + createExpectedEndorsement(skill1, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement1, expectedEndorsement1); + + assertThat(endorsementRepository.count()).isEqualTo(1); + + // create the endorsement for skill2 + MvcResult result2 = performPostRequest(createUrl(skill2.getId()), requestBody); + + assertThat(result2.getResponse().getStatus()).isEqualTo(201); + + EndorsementViewModel actualEndorsement2 = extractEndorsementFromResult(result2); + EndorsementViewModel expectedEndorsement2 = + createExpectedEndorsement(skill2, endorseId, endorserId, message); + + assertIsEqual(actualEndorsement2, expectedEndorsement2); + + // assert both endorsements were created + assertThat(endorsementRepository.count()).isEqualTo(2); + } + + @Test + @DisplayName("if user not authenticated, should return 401") + public void shouldReturn401_ifUnauthenticatedUser() throws Exception { + Skill skill = createAndSaveSkill(SKILL_NAME); + + String endorseId = userId1; + String message = ENDORSEMENT_MESSAGE; + + String requestBody = + objectMapper.writeValueAsString(createEndorsementRequest(userId1, ENDORSEMENT_MESSAGE)); + + MvcResult result = performPostRequest(createUrl(skill.getId()), requestBody); + assertThat(result.getResponse().getStatus()).isEqualTo(401); + } +} From 785af90c73dfbd59bec11b58eab3ffaa1cb05b10 Mon Sep 17 00:00:00 2001 From: Shyam Vishwakarma <144812100+Shyam-Vishwakarma@users.noreply.github.com> Date: Fri, 16 May 2025 00:45:16 +0530 Subject: [PATCH 10/10] fix: update formatting of queries (#204) --- .../repositories/EndorsementRepository.java | 11 +++---- .../repositories/UserSkillRepository.java | 32 ++++++++----------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java index 1efe5417..6ce0f5f9 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/EndorsementRepository.java @@ -11,13 +11,10 @@ public interface EndorsementRepository extends JpaRepository findByEndorseIdAndSkillId(String endorseId, Integer skillId); - @Query(""" - SELECT (COUNT(*) > 0) AS exists - FROM Endorsement e - WHERE e.endorserId = :endorserId - AND e.endorseId = :endorseId - AND e.skill.id = :skillId - """) + @Query( + "SELECT (COUNT(*) > 0) AS exists " + + "FROM Endorsement e " + + "WHERE e.endorserId = :endorserId AND e.endorseId = :endorseId AND e.skill.id = :skillId") boolean existsByEndorseIdAndEndorserIdAndSkillId( @Param("endorseId") String endorseId, @Param("endorserId") String endorserId, diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java index ee45d77d..bdc0c9e1 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java @@ -12,28 +12,22 @@ public interface UserSkillRepository extends JpaRepository List findByUserIdAndSkillId(String userId, Integer skillId); - @Query(""" - SELECT us FROM UserSkills us - JOIN Endorsement e ON us.userId = e.endorseId - WHERE e.endorserId = :endorserId - """) + @Query( + "SELECT us " + + "FROM UserSkills us JOIN Endorsement e ON us.userId = e.endorseId " + + "WHERE e.endorserId = :endorserId") List findUserSkillsByEndorserIdLegacy(@Param("endorserId") String endorserId); - @Query(""" - SELECT us FROM UserSkills us - JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id - WHERE e.endorserId = :endorserId - """) + @Query( + "SELECT us " + + "FROM UserSkills us JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id " + + "WHERE e.endorserId = :endorserId") List findUserSkillsByEndorserId(@Param("endorserId") String endorserId); - @Query(""" - SELECT us FROM UserSkills us - JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id - WHERE e.endorserId = :endorserId AND us.status = :status - """) + @Query( + "SELECT us " + + "FROM UserSkills us JOIN Endorsement e ON us.userId = e.endorseId AND us.skill.id = e.skill.id " + + "WHERE e.endorserId = :endorserId AND us.status = :status") List findByStatusAndEndorserId( - @Param("status") UserSkillStatusEnum status, - @Param("endorserId") String endorserId - ); - + @Param("status") UserSkillStatusEnum status, @Param("endorserId") String endorserId); }