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 ac132714..d53dee5a 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 + @@ -214,7 +219,7 @@ ${skip-ut} ${skip-tests} - **/unit/*.java + **/unit/**/*.java @@ -226,7 +231,7 @@ ${skip-tests} - **/integration/*.java + **/integration/**/*.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..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") @@ -57,7 +59,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/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..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 @@ -3,9 +3,20 @@ 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/repositories/UserSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/UserSkillRepository.java index 12868976..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 @@ -13,8 +13,21 @@ 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 " + "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/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/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/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 new file mode 100644 index 00000000..55ca50d8 --- /dev/null +++ b/skill-tree/src/main/resources/application-test.properties @@ -0,0 +1,5 @@ +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/TestContainerManager.java b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java new file mode 100644 index 00000000..99883b13 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/TestContainerManager.java @@ -0,0 +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; + +@TestConfiguration +public class TestContainerManager { + @Value("${test.db.mysql-image}") + private String MYSQL_IMAGE_NAME; + + @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/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); + } +} 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 new file mode 100644 index 00000000..de7963c5 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/CreateSkillIntegrationTest.java @@ -0,0 +1,160 @@ +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; +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.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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import(TestContainerManager.class) +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)) + .andExpect(MockMvcResultMatchers.status().is(201)) + .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)) + .andExpect(MockMvcResultMatchers.status().is(409)); + } + + @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().is(400)); + } + + @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().is(500)); + } + + @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().is(403)); + } +} 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 new file mode 100644 index 00000000..5f13355b --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillRequestIntegrationTest.java @@ -0,0 +1,359 @@ +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; +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.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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.CustomResultMatchers; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import(TestContainerManager.class) +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)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .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)) + .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.hasUser("user-id", " ")) + .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( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void getAllRequests_ByStatus_ShouldReturnFilteredRequests() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=APPROVED") + .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.hasUser("user-id", " ")) + .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( + username = "user-id", + authorities = {"USER"}) + public void noSkillRequestsEndorsedByUser_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform(MockMvcRequestBuilders.get(route).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 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( + username = "user-id", + authorities = {"USER"}) + public void noMatchingRequestsByStatus_ShouldReturnEmptyLists() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get(route + "?status=REJECTED") + .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 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( + 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)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .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)) + .andExpect(MockMvcResultMatchers.status().is(401)); + } +} 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 new file mode 100644 index 00000000..2c9bfa30 --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/GetAllSkillsIntegrationTest.java @@ -0,0 +1,112 @@ +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.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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import(TestContainerManager.class) +public class GetAllSkillsIntegrationTest { + + @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"; + + @BeforeEach + public void setUp() { + 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)); + + // 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 + @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(route).accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Java")) + .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Springboot")); + } + + @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(route).accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(MockMvcResultMatchers.jsonPath("$").isEmpty()); + } + + @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(route).cookie(authCookie).accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(401)); + } +} 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 new file mode 100644 index 00000000..8e406c0e --- /dev/null +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/skills/SkillRequestActionIntegrationTest.java @@ -0,0 +1,210 @@ +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; +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.*; +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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import utils.WithCustomMockUser; + +@AutoConfigureMockMvc +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@Import(TestContainerManager.class) +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 { + 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().is(200)) + .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 { + 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().is(200)) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("rejected")); + + // 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"}) + public void approveSkillRequest_NonExistentSkillId_ShouldFail() throws Exception { + 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().is(404)); + } + + @Test + @DisplayName("Error case - Request with non-existent user ID") + @WithCustomMockUser( + username = "super-user-id", + authorities = {"SUPERUSER"}) + public void approveSkillRequest_NonExistentUserId_ShouldFail() throws Exception { + 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().is(404)); + } + + @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().is(400)); + } + + @Test + @DisplayName("Authorization test - Non-super user cannot access endpoint") + @WithCustomMockUser( + username = "normal-user", + authorities = {"USER"}) + public void approveSkillRequest_NonSuperUser_ShouldFail() throws Exception { + 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().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"); 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..2c354a94 --- /dev/null +++ b/skill-tree/src/test/java/utils/CustomResultMatchers.java @@ -0,0 +1,119 @@ +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); + }; + } + + 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)) { + 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/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(); + } +} 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 {}; +}