diff --git a/.github/workflows/deploy-to-ec2.yml b/.github/workflows/deploy-to-ec2.yml new file mode 100644 index 00000000..a0ec511f --- /dev/null +++ b/.github/workflows/deploy-to-ec2.yml @@ -0,0 +1,51 @@ +name: Deploy to EC2 + +on: + push: + branches: + - main + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up AWS CLI + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR Public + id: login-ecr + run: aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.AWS_ECR_URI }} + + - name: Build Docker image + run: docker build -t skill-tree-backend -f skill-tree/Dockerfile . + - name: Tag Docker image + run: docker tag skill-tree-backend:latest ${{ secrets.AWS_ECR_URI }}/skill-tree-backend:latest + + - name: Push Docker image to ECR Public + run: docker push ${{ secrets.AWS_ECR_URI }}/skill-tree-backend:latest + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Deploy to EC2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_EC2_HOST }} + username: ${{ secrets.AWS_EC2_USERNAME }} + key: ${{ secrets.AWS_EC2_SSH_PRIVATE_KEY }} + - name: Pull Image from docker hub + run: docker pull ${{ secrets.AWS_ECR_URI }}/skill-tree-backend:latest + - name: Delete old container + run: docker rm -f skill-tree-backend + - name: Run docker container + run: docker run -d -p 8080:8080 --name skill-tree-backend -e RDS_PUBLIC_KEY=${{secrets.RDS_PUBLIC_KEY}} -e API_V1_PREFIX=/api/v1 ${{ secrets.AWS_ECR_URI }}/skill-tree-backend + diff --git a/.gitignore b/.gitignore index 56f3c0fb..661c12b8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,4 @@ replay_pid* *.env -target/ \ No newline at end of file +target/ diff --git a/api-contracts.yml b/api-contracts.yml index c30cd1b6..cf87436a 100644 --- a/api-contracts.yml +++ b/api-contracts.yml @@ -227,24 +227,25 @@ paths: - /endorsements/{endorsementId}: + /endorsements/{id}?status={EndorsementStatus}: patch: tags: - endorsements parameters: - - name: endorsementId + - name: id schema: type: string + format: UUID in: path description: EndorsementId which has to be modified required: true - requestBody: - required: true - content: - application/json: - schema: - type: object - $ref: '#/components/schemas/endorsementUpdate' + - name: status + schema: + type: string, + format: EndorsementStatus, + in: query + description: New endorsement status which has to be updated + required: true summary: Update endorsement status given endorsementId description: Update endorsement status given endorsement id, **this can be only used by Super User for now** operationId: updateEndorsementStatusGivenId @@ -256,8 +257,7 @@ paths: application/json: schema: type: object - $ref: '#/components/schemas/endorsementResponse' - + $ref: '#/components/schemas/UpdateEndorsementResponse' '400': description: Invalid endorsement Id value @@ -267,6 +267,30 @@ paths: type: object $ref: '#/components/schemas/ApiResponseFailureInvalidParameter' + '400': + description: Invalid endorsement status + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureInvalidParameter' + + '409': + description: Updating the status of an endorsement that has been accepted/rejected + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureEndorsementUpdateResponse' + + '403': + description: Unauthorized access, user is not a super user + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureUnauthorized' + '503': description: Service Unavailable content: @@ -580,4 +604,37 @@ components: ApiResponseForSkillName: type: array items: - $ref: '#/components/schemas/skillUsersResponse' \ No newline at end of file + $ref: '#/components/schemas/skillUsersResponse' + + UpdateEndorsementResponse: + type: object + properties: + code: + type: integer + format: int32 + example: 200 + message: + type: string + example: Successfully updated endorsement status + + ApiResponseFailureUnauthorized: + type: object + properties: + code: + type: integer + format: int32 + example: 403 + message: + type: string + example: Unauthorized, Access is only available to super users + + ApiResponseFailureEndorsementUpdateResponse: + type: object + properties: + code: + type: integer + format: int32 + example: 409 + message: + type: string + example: Endorsement is already updated, Cannot modify status \ No newline at end of file diff --git a/skill-tree/Dockerfile b/skill-tree/Dockerfile new file mode 100644 index 00000000..251f9ab4 --- /dev/null +++ b/skill-tree/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:17-jdk as build +WORKDIR /app +COPY skill-tree . +RUN ./mvnw --version +RUN ./mvnw clean install -DskipTests + +FROM openjdk:17-jdk +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/skill-tree/docker-compose.yml b/skill-tree/docker-compose.yml index e7fc4601..63bcab43 100644 --- a/skill-tree/docker-compose.yml +++ b/skill-tree/docker-compose.yml @@ -2,8 +2,6 @@ version: "3.7" services: db: image: mysql - # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password - # (this is just an example, not intended to be a production configuration) command: --default-authentication-plugin=mysql_native_password ports: - 3306:3306 @@ -13,9 +11,33 @@ services: MYSQL_PASSWORD: testpassword MYSQL_ROOT_PASSWORD: password volumes: - - ./skilltree-database-script.sql:/docker-entrypoint-initdb.d/skilltree-database-script.sql + - mysql-data:/var/lib/mysql adminer: image: adminer ports: - - 9000:8080 \ No newline at end of file + - 9000:8080 + + spring-boot-app: + build: + context: . + dockerfile: Dockerfile + depends_on: + - db + ports: + - 8080:8080 + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/skilltree + SPRING_DATASOURCE_USERNAME: testuser + SPRING_DATASOURCE_PASSWORD: testpassword + SPRING_JPA_HIBERNATE_DDL_AUTO: update + MYSQL_HOST: db + DB_NAME: skilltree + MYSQL_DB_USERNAME: testuser + MYSQL_DB_PASSWORD: testpassword + DB_DDL_POLICY: update + API_V1_PREFIX: /api/v1 + SPRING_DATASOURCE_VERSION: 8.1.0 + +volumes: + mysql-data: diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java index 06e054f9..7b154118 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java @@ -79,4 +79,10 @@ public ResponseEntity> postEndorsement( new GenericResponse(null, "Failed to create endorsement"), HttpStatus.BAD_REQUEST); } + + @PatchMapping(value = "/{id}") + public ResponseEntity> updateEndorsementStatus( + @PathVariable(value = "id") UUID id, @RequestParam String status) { + return ResponseEntity.ok().body(endorsementService.updateEndorsementStatus(id, status)); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDRO.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDRO.java index 5e5ade22..d0910431 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDRO.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDRO.java @@ -13,7 +13,7 @@ @Builder public class EndorsementDRO { @NotNull(message = "user id cannot be null") - private UUID userId; + private UUID endorseeId; @NotNull(message = "skill id cannot be null") private UUID skillId; diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDTO.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDTO.java index 6d571d8d..3047e63f 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDTO.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementDTO.java @@ -2,7 +2,6 @@ import com.RDS.skilltree.EndorsementList.EndorsementListModel; import com.RDS.skilltree.Skill.SkillDTO; -import com.RDS.skilltree.User.UserDTO; import com.RDS.skilltree.utils.TrackedProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.List; @@ -14,7 +13,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class EndorsementDTO extends TrackedProperties { private UUID id; - private UserDTO user; + private UUID endorseeId; private SkillDTO skill; private EndorsementStatus status; private List endorsersList; @@ -23,7 +22,7 @@ public static EndorsementDTO toDto(EndorsementModel endorsementModel) { EndorsementDTO endorsementDTO = EndorsementDTO.builder() .id(endorsementModel.getId()) - .user(UserDTO.toDTO(endorsementModel.getUser())) + .endorseeId(endorsementModel.getEndorseeId()) .skill(SkillDTO.toDto(endorsementModel.getSkill())) .status(endorsementModel.getStatus()) .endorsersList(endorsementModel.getEndorsersList()) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementModel.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementModel.java index d25f4916..68a3faec 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementModel.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementModel.java @@ -2,7 +2,6 @@ import com.RDS.skilltree.EndorsementList.EndorsementListModel; import com.RDS.skilltree.Skill.SkillModel; -import com.RDS.skilltree.User.UserModel; import com.RDS.skilltree.utils.TrackedProperties; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; @@ -23,9 +22,8 @@ public class EndorsementModel extends TrackedProperties { @Column(name = "id", columnDefinition = "BINARY(16)") private UUID id; - @ManyToOne(targetEntity = UserModel.class, cascade = CascadeType.ALL) - @JoinColumn(name = "user_id", referencedColumnName = "id") - private UserModel user; + @Column(name = "endorsee_id") + private UUID endorseeId; @ManyToOne(targetEntity = SkillModel.class, cascade = CascadeType.ALL) @JoinColumn(name = "skill_id", referencedColumnName = "id") diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementRepository.java index 28d1ac56..6da326dd 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementRepository.java @@ -7,7 +7,7 @@ @Repository public interface EndorsementRepository extends JpaRepository { - List findByUserId(UUID userId); + List findByEndorseeId(UUID userId); List findBySkillId(UUID skillId); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java index e599ceaa..a7254a48 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java @@ -1,5 +1,6 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Common.Response.GenericResponse; import java.io.IOException; import java.util.UUID; import org.springframework.data.domain.Page; @@ -15,4 +16,6 @@ Page getEndorsementsFromDummyData( PageRequest pageRequest, String skillID, String userID) throws IOException; EndorsementModel createEndorsement(EndorsementDRO endorsementDRO); + + GenericResponse updateEndorsementStatus(UUID id, String status); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java index 740fa8f3..fce1ed63 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java @@ -1,10 +1,13 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Common.Response.GenericResponse; +import com.RDS.skilltree.Exceptions.EntityAlreadyExistsException; +import com.RDS.skilltree.Exceptions.InvalidParameterException; import com.RDS.skilltree.Exceptions.NoEntityException; import com.RDS.skilltree.Skill.SkillModel; import com.RDS.skilltree.Skill.SkillRepository; import com.RDS.skilltree.User.UserModel; -import com.RDS.skilltree.User.UserRepository; +import com.RDS.skilltree.User.UserRole; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; @@ -21,13 +24,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class EndorsementServiceImpl implements EndorsementService { private final EndorsementRepository endorsementRepository; - private final UserRepository userRepository; private final SkillRepository skillRepository; private final ObjectMapper objectMapper; @@ -113,19 +117,50 @@ private Page createPagedEndorsements( @Override public EndorsementModel createEndorsement(EndorsementDRO endorsementDRO) { - UUID userId = endorsementDRO.getUserId(); + UUID userId = endorsementDRO.getEndorseeId(); UUID skillId = endorsementDRO.getSkillId(); - Optional userOptional = userRepository.findById(userId); + Optional skillOptional = skillRepository.findById(skillId); - if (userOptional.isPresent() && skillOptional.isPresent()) { + if (skillOptional.isPresent()) { EndorsementModel endorsementModel = - EndorsementModel.builder().user(userOptional.get()).skill(skillOptional.get()).build(); + EndorsementModel.builder().endorseeId(userId).skill(skillOptional.get()).build(); return endorsementRepository.save(endorsementModel); } else { - if (userOptional.isEmpty()) - throw new NoEntityException("User with id:" + userId + " not found"); + throw new NoEntityException("Skill with id:" + skillId + " not found"); } } + + @Override + public GenericResponse updateEndorsementStatus(UUID id, String status) { + UserModel user = + (UserModel) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (!user.getRole().equals(UserRole.SUPERUSER)) { + throw new AccessDeniedException("Unauthorized, Access is only available to super users"); + } + + EndorsementStatus endorsementStatus = EndorsementStatus.fromString(status); + if (endorsementStatus.equals(EndorsementStatus.PENDING)) { + throw new InvalidParameterException("endorsement status", status); + } + Optional optionalEndorsementModel = endorsementRepository.findById(id); + if (optionalEndorsementModel.isPresent()) { + if (optionalEndorsementModel.get().getStatus() != EndorsementStatus.PENDING) { + throw new EntityAlreadyExistsException( + "Endorsement is already updated. Cannot modify status"); + } + EndorsementModel updatedEndorsementModel = + EndorsementModel.builder() + .id(optionalEndorsementModel.get().getId()) + .endorseeId(optionalEndorsementModel.get().getEndorseeId()) + .skill(optionalEndorsementModel.get().getSkill()) + .endorsersList(optionalEndorsementModel.get().getEndorsersList()) + .status(EndorsementStatus.valueOf(status)) + .build(); + endorsementRepository.save(updatedEndorsementModel); + return new GenericResponse<>(null, "Successfully updated endorsement status"); + } + throw new NoEntityException("No endorsement with id " + id + " was found"); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java index 06407ca2..9300b618 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java @@ -1,7 +1,25 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Exceptions.InvalidParameterException; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + public enum EndorsementStatus { APPROVED, PENDING, - REJECTED + REJECTED; + + private static final Map endorsementStatusMap = + Stream.of(values()) + .collect(Collectors.toMap(EndorsementStatus::toString, Function.identity())); + + public static EndorsementStatus fromString(final String name) { + EndorsementStatus endorsementStatus = endorsementStatusMap.get(name); + if (endorsementStatus == null) { + throw new InvalidParameterException("endorsement status", name); + } + return endorsementStatus; + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListModel.java b/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListModel.java index 501de8aa..c734925d 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListModel.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListModel.java @@ -1,7 +1,6 @@ package com.RDS.skilltree.EndorsementList; import com.RDS.skilltree.Endorsement.EndorsementModel; -import com.RDS.skilltree.User.UserModel; import com.RDS.skilltree.utils.TrackedProperties; import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; @@ -26,9 +25,8 @@ public class EndorsementListModel extends TrackedProperties { @JsonBackReference private EndorsementModel endorsement; - @JoinColumn(name = "user_id", referencedColumnName = "id") - @OneToOne(targetEntity = UserModel.class, cascade = CascadeType.ALL) - private UserModel endorser; + @Column(name = "endorser_id") + private UUID endorserId; @Column(name = "description") private String description; @@ -41,9 +39,9 @@ public class EndorsementListModel extends TrackedProperties { private EndorsementType type; public EndorsementListModel( - EndorsementModel endorsement, UserModel endorser, String description, EndorsementType type) { + EndorsementModel endorsement, UUID endorserId, String description, EndorsementType type) { this.endorsement = endorsement; - this.endorser = endorser; + this.endorserId = endorserId; this.description = description; this.type = type; this.deleted = false; diff --git a/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListService.java b/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListService.java index 0cd294c5..c26308b2 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/EndorsementList/EndorsementListService.java @@ -3,26 +3,21 @@ import com.RDS.skilltree.Endorsement.EndorsementModel; import com.RDS.skilltree.Endorsement.EndorsementRepository; import com.RDS.skilltree.Exceptions.NoEntityException; -import com.RDS.skilltree.User.UserModel; -import com.RDS.skilltree.User.UserRepository; import java.util.Optional; import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class EndorsementListService { - @Autowired private final EndorsementListRepository endorsementListRepository; + + private final EndorsementListRepository endorsementListRepository; private final EndorsementRepository endorsementRepository; - private final UserRepository userRepository; public EndorsementListService( EndorsementListRepository endorsementListRepository, - EndorsementRepository endorsementRepository, - UserRepository userRepository) { + EndorsementRepository endorsementRepository) { this.endorsementListRepository = endorsementListRepository; this.endorsementRepository = endorsementRepository; - this.userRepository = userRepository; } public EndorsementListModel createEndorsementListEntry(EndorsementListDRO endorsementListDRO) { @@ -30,11 +25,11 @@ public EndorsementListModel createEndorsementListEntry(EndorsementListDRO endors UUID endorserId = endorsementListDRO.getEndorserId(); UUID endorsementId = endorsementListDRO.getEndorsementId(); - Optional endorserOptional = userRepository.findById(endorserId); + Optional endorsementOptional = endorsementRepository.findById(endorsementId); - if (endorserOptional.isPresent() && endorsementOptional.isPresent()) { + if (endorsementOptional.isPresent()) { - endorsementListEntry.setEndorser(endorserOptional.get()); + endorsementListEntry.setEndorserId(endorserId); endorsementListEntry.setEndorsement(endorsementOptional.get()); endorsementListEntry.setDescription(endorsementListDRO.getDescription()); endorsementListEntry.setType(endorsementListDRO.getType()); @@ -42,8 +37,6 @@ public EndorsementListModel createEndorsementListEntry(EndorsementListDRO endors return endorsementListEntry; } else { - if (endorserOptional.isEmpty()) - throw new NoEntityException("User with id:" + endorserId + " not found"); throw new NoEntityException("Endorsement with id:" + endorsementId + " not found"); } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Skill/SkillsServiceImpl.java b/skill-tree/src/main/java/com/RDS/skilltree/Skill/SkillsServiceImpl.java index b66b06d7..61943723 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Skill/SkillsServiceImpl.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Skill/SkillsServiceImpl.java @@ -1,6 +1,5 @@ package com.RDS.skilltree.Skill; -import com.RDS.skilltree.User.UserModel; import com.RDS.skilltree.User.UserRepository; import java.time.Instant; import java.util.Optional; @@ -42,9 +41,7 @@ public SkillDTO createSkill(SkillDRO skillDRO) { SkillModel newSkill = SkillDRO.toModel(skillDRO); newSkill.setCreatedAt(Instant.now()); newSkill.setUpdatedAt(Instant.now()); - UserModel user = userRepository.findById(skillDRO.getCreatedBy()).get(); - newSkill.setUpdatedBy(user); - newSkill.setCreatedBy(user); + try { skillRepository.save(newSkill); } catch (DataIntegrityViolationException ex) { diff --git a/skill-tree/src/main/java/com/RDS/skilltree/User/UserDRO.java b/skill-tree/src/main/java/com/RDS/skilltree/User/UserDRO.java index 47fc8495..c0141963 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/User/UserDRO.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/User/UserDRO.java @@ -59,7 +59,6 @@ public static UserModel compareAndUpdateModel(UserModel user, UserDRO userDRO) { user.setRole(user.getRole()); } user.setUpdatedAt(Instant.now()); - user.setUpdatedBy(user); return user; } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java new file mode 100644 index 00000000..ac810b76 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java @@ -0,0 +1,14 @@ +package com.RDS.skilltree.utils; + +import java.util.regex.Pattern; + +public class CommonUtils { + private static final Pattern UUID_REGEX = + Pattern.compile( + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + public static boolean isValidUUID(String uuidString) { + + return UUID_REGEX.matcher(uuidString).matches(); + } +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/TrackedProperties.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/TrackedProperties.java index 18025b1b..23601457 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/TrackedProperties.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/TrackedProperties.java @@ -1,23 +1,20 @@ package com.RDS.skilltree.utils; -import com.RDS.skilltree.User.UserModel; import jakarta.persistence.Column; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.MappedSuperclass; import java.time.Instant; +import java.util.UUID; import lombok.Data; @Data @MappedSuperclass public abstract class TrackedProperties { - @ManyToOne - @JoinColumn(name = "created_by") - private UserModel createdBy; - @ManyToOne - @JoinColumn(name = "updated_by") - private UserModel updatedBy; + @Column(name = "created_by") + private UUID createdBy; + + @Column(name = "updated_by") + private UUID updatedBy; @Column(name = "created_at") private Instant createdAt; diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java index d5a4aa19..cccc1d5b 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java @@ -3,7 +3,6 @@ import com.RDS.skilltree.Exceptions.InvalidParameterException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -13,9 +12,6 @@ public class UUIDValidationInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(UUIDValidationInterceptor.class); - private static final Pattern UUID_REGEX = - Pattern.compile( - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); @Override public boolean preHandle( @@ -23,19 +19,14 @@ public boolean preHandle( String skillID = request.getParameter("skillID"); String userID = request.getParameter("userID"); - if (skillID != null && !skillID.isEmpty() && !isValidUUID(skillID)) { + if (skillID != null && !skillID.isEmpty() && !CommonUtils.isValidUUID(skillID)) { throw new InvalidParameterException("skillID", "Invalid UUID format"); } - if (userID != null && !userID.isEmpty() && !isValidUUID(userID)) { + if (userID != null && !userID.isEmpty() && !CommonUtils.isValidUUID(userID)) { throw new InvalidParameterException("userID", "Invalid UUID format"); } return true; } - - private boolean isValidUUID(String uuidString) { - - return UUID_REGEX.matcher(uuidString).matches(); - } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/integration/EndorsementsIntegrationTests.java b/skill-tree/src/test/java/com/RDS/skilltree/integration/EndorsementsIntegrationTests.java index 6dd98f11..a4b5cb1b 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/integration/EndorsementsIntegrationTests.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/integration/EndorsementsIntegrationTests.java @@ -67,6 +67,18 @@ private void cleanUp() { userRepository.deleteAll(); } + private UUID createEndorsementModel(Boolean isStatusPending) { + EndorsementStatus endorsementStatus; + if (isStatusPending) { + endorsementStatus = EndorsementStatus.PENDING; + } else { + endorsementStatus = EndorsementStatus.APPROVED; + } + EndorsementModel endorsementModel = + EndorsementModel.builder().status(endorsementStatus).build(); + return endorsementRepository.save(endorsementModel).getId(); + } + @Test @Disabled @DisplayName("Fetch all the endorsements") @@ -83,7 +95,7 @@ public void testAPIReturnsAllEndorsements() { .body("data[0].status", anyOf(equalTo("APPROVED"), equalTo("REJECTED"), equalTo("PENDING"))) .body("data[0].endorsementType", anyOf(equalTo("POSITIVE"), equalTo("NEGATIVE"))) .body("data[0].endorsersList", hasSize(1)) - .body("data[0].endorserList[0].endorserId", equalTo("user-2")) + .body("data[0].endorserList[0].endorseeId", equalTo("user-2")) .body("data[0].endorserList[0].description", isA(String.class)) .body( "data[0].endorserList[0].userType", @@ -110,7 +122,7 @@ public void testAPIReturnsEndorsementsGivenStatus() { .body("data[0].status", equalTo("PENDING")) .body("data[0].endorsementType", anyOf(equalTo("POSITIVE"), equalTo("NEGATIVE"))) .body("data[0].endorsersList", hasSize(1)) - .body("data[0].endorserList[0].endorserId", equalTo("user-2")) + .body("data[0].endorserList[0].endorseeId", equalTo("user-2")) .body("data[0].endorserList[0].description", isA(String.class)) .body( "data[0].endorserList[0].userType", @@ -155,11 +167,11 @@ public void testAPIReturns400_OnInvalidParameterPassed() { @Test @DisplayName("Return 201 on endorsements creation") public void testAPIReturns201_OnEndorsementCreation() { - UUID userId = user.getId(); + UUID endorseeId = user.getId(); UUID skillId = skill.getId(); EndorsementDRO endorsementDRO = new EndorsementDRO(); - endorsementDRO.setUserId(userId); + endorsementDRO.setEndorseeId(endorseeId); endorsementDRO.setSkillId(skillId); Response response = given() @@ -172,7 +184,7 @@ public void testAPIReturns201_OnEndorsementCreation() { .then() .statusCode(201) .contentType("application/json") - .body("data.user.firstName", equalTo("John")) + .body("data.endorseeId", equalTo(endorseeId.toString())) .body("data.skill.name", equalTo("Java")); } @@ -203,10 +215,10 @@ public void testAPIReturns400_OnEndorsementCreationUserIdNull() { @Test @DisplayName("Return 400 on endorsements skillid null") public void testAPIReturns400_OnEndorsementCreationSkillIdNull() { - UUID userId = user.getId(); + UUID endorseeId = user.getId(); EndorsementDRO endorsementDRO = new EndorsementDRO(); - endorsementDRO.setUserId(userId); + endorsementDRO.setEndorseeId(endorseeId); Response response = given() @@ -244,7 +256,7 @@ public void testAPIReturns200_OnEndorsementGivenId() { .body("data.status", anyOf(equalTo("APPROVED"), equalTo("PENDING"), equalTo("REJECTED"))) .body("data.endorsementType", anyOf(equalTo("POSITIVE"), equalTo("NEGATIVE"))) .body("data.endorsersList", hasSize(1)) - .body("data.endorsersList[0].endorserId", equalTo("user-2")) + .body("data.endorsersList[0].endorseeId", equalTo("user-2")) .body("data.endorsersList[0].description", isA(String.class)) .body( "data.endorsersList[0].userType", @@ -479,4 +491,99 @@ public void itShouldReturn401OnEndorsementSearchWithoutCookie() { equalTo( "The access token provided is expired, revoked, malformed, or invalid for other reasons.")); } + + @Test + @DisplayName( + "Return 200, when request is made using super user cookie and status is APPROVED/REJECTED") + public void + itShouldReturn200OnUpdateEndorsementStatusWithSuperUserCookieAndAcceptOrRejectEndorsementStatus() { + UUID endorsementId = createEndorsementModel(true); + Response response = + given() + .cookies(RestAPIHelper.getSuperUserCookie()) + .queryParam("status", EndorsementStatus.APPROVED.name()) + .patch("/v1/endorsements/{id}", endorsementId); + + response + .then() + .statusCode(200) + .body("data", equalTo(null)) + .body("message", equalTo("Successfully updated endorsement status")); + } + + @Test + @DisplayName( + "Return 403, when request is made without using super user cookie and status is APPROVED/REJECTED") + public void + itShouldReturn403OnUpdateEndorsementStatusWithOutSuperUserCookieAndAcceptOrRejectEndorsementStatus() { + UUID endorsementId = createEndorsementModel(true); + Response response = + given() + .cookies(RestAPIHelper.getUserCookie()) + .queryParam("status", EndorsementStatus.APPROVED.name()) + .patch("/v1/endorsements/{id}", endorsementId); + + response + .then() + .statusCode(403) + .body("data", equalTo(null)) + .body("message", equalTo("Unauthorized, Access is only available to super users")); + } + + @Test + @DisplayName( + "Return 400, when request is made with using super user cookie and status is invalid") + public void + itShouldReturn400OnUpdateEndorsementStatusWithSuperUserCookieAndEndorsementStatusIsInvalid() { + UUID endorsementId = createEndorsementModel(true); + Response response = + given() + .cookies(RestAPIHelper.getSuperUserCookie()) + .queryParam("status", "invalid-status") + .patch("/v1/endorsements/{id}", endorsementId); + + response + .then() + .statusCode(400) + .body("data", equalTo(null)) + .body("message", equalTo("Invalid parameter endorsement status: invalid-status")); + } + + @Test + @DisplayName( + "Return 400, when request is made with using super user cookie and status is PENDING") + public void + itShouldReturn400OnUpdateEndorsementStatusWithSuperUserCookieAndEndorsementStatusIsPending() { + UUID endorsementId = createEndorsementModel(true); + Response response = + given() + .cookies(RestAPIHelper.getSuperUserCookie()) + .queryParam("status", EndorsementStatus.PENDING.name()) + .patch("/v1/endorsements/{id}", endorsementId); + + response + .then() + .statusCode(400) + .body("data", equalTo(null)) + .body("message", equalTo("Invalid parameter endorsement status: PENDING")); + } + + @Test + @DisplayName( + "Return 409, when request is made with using super user cookie and endorsement is already updated") + public void + itShouldReturn409OnUpdateEndorsementStatusWithSuperUserCookieAndEndorsementAlreadyUpdated() { + UUID endorsementId = createEndorsementModel(false); + Response response = + given() + .cookies(RestAPIHelper.getSuperUserCookie()) + .queryParam("status", EndorsementStatus.APPROVED.name()) + .patch("/v1/endorsements/{id}", endorsementId); + + response + .then() + .statusCode(409) + .body("data", equalTo(null)) + .body("message", equalTo("Endorsement is already updated. Cannot modify status")); + } } diff --git a/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementListServiceTest.java b/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementListServiceTest.java index bc124e6c..4a7e0ad4 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementListServiceTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementListServiceTest.java @@ -61,34 +61,12 @@ void testCreateEndorsementListEntry() { // Assertions assertNotNull(result); - assertEquals(endorserId, result.getEndorser().getId()); + assertEquals(endorserId, result.getEndorserId()); assertEquals(endorsementId, result.getEndorsement().getId()); assertEquals("Test Description", result.getDescription()); assertEquals(EndorsementType.POSITIVE, result.getType()); } - @Test - void testCreateEndorsementListEntryWithInvalidUser() { - UUID endorserId = UUID.randomUUID(); - UUID endorsementId = UUID.randomUUID(); - EndorsementListDRO endorsementListDRO = new EndorsementListDRO(); - endorsementListDRO.setEndorserId(endorserId); - endorsementListDRO.setEndorsementId(endorsementId); - - // Mock the repository behavior for an invalid user - when(userRepository.findById(endorserId)).thenReturn(Optional.empty()); - - // Assert that a NoEntityException is thrown - NoEntityException exception = - assertThrows( - NoEntityException.class, - () -> endorsementListService.createEndorsementListEntry(endorsementListDRO)); - assertEquals("User with id:" + endorserId + " not found", exception.getMessage()); - - // Verify that save method is not called - verify(endorsementListRepository, never()).save(any(EndorsementListModel.class)); - } - @Test void testCreateEndorsementListEntryWithInvalidEndorsement() { UUID endorserId = UUID.randomUUID(); diff --git a/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementServiceTest.java b/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementServiceTest.java index ef420899..2ee4dcef 100644 --- a/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementServiceTest.java +++ b/skill-tree/src/test/java/com/RDS/skilltree/unit/EndorsementServiceTest.java @@ -3,17 +3,16 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import com.RDS.skilltree.Endorsement.EndorsementDRO; -import com.RDS.skilltree.Endorsement.EndorsementDTO; -import com.RDS.skilltree.Endorsement.EndorsementModel; -import com.RDS.skilltree.Endorsement.EndorsementModelFromJSON; -import com.RDS.skilltree.Endorsement.EndorsementRepository; -import com.RDS.skilltree.Endorsement.EndorsementServiceImpl; +import com.RDS.skilltree.Common.Response.GenericResponse; +import com.RDS.skilltree.Endorsement.*; +import com.RDS.skilltree.Exceptions.EntityAlreadyExistsException; +import com.RDS.skilltree.Exceptions.InvalidParameterException; import com.RDS.skilltree.Exceptions.NoEntityException; import com.RDS.skilltree.Skill.SkillModel; import com.RDS.skilltree.Skill.SkillRepository; import com.RDS.skilltree.User.UserModel; import com.RDS.skilltree.User.UserRepository; +import com.RDS.skilltree.User.UserRole; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; @@ -22,6 +21,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.util.*; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -34,6 +34,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @@ -48,26 +51,47 @@ public class EndorsementServiceTest { @InjectMocks @Autowired private EndorsementServiceImpl endorsementService; + @Mock private Authentication auth; + @BeforeEach public void setUp() { ReflectionTestUtils.setField( endorsementService, "dummyEndorsementDataPath", "dummy-data/endorsements.json"); } + @AfterEach + public void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private void setupUpdateEndorsementTests(Boolean useSuperUserRole) { + UserModel userModel = new UserModel(); + if (useSuperUserRole) { + userModel.setRole(UserRole.SUPERUSER); + } else { + userModel.setRole(UserRole.USER); + } + when(auth.getPrincipal()).thenReturn(userModel); + SecurityContextHolder.getContext().setAuthentication(auth); + } + @Test public void itShouldGetEndorsementsById() { UUID endorsementId = UUID.randomUUID(); - UUID userId = UUID.randomUUID(); + UUID endorserId = UUID.randomUUID(); UUID skillId = UUID.randomUUID(); - UserModel userModel = UserModel.builder().id(userId).build(); SkillModel skillModel = SkillModel.builder().id(skillId).build(); EndorsementModel endorsementModel = - EndorsementModel.builder().id(endorsementId).user(userModel).skill(skillModel).build(); + EndorsementModel.builder() + .id(endorsementId) + .endorseeId(endorserId) + .skill(skillModel) + .build(); endorsementModel.setCreatedAt(Instant.now()); endorsementModel.setUpdatedAt(Instant.now()); - endorsementModel.setCreatedBy(userModel); - endorsementModel.setUpdatedBy(userModel); + endorsementModel.setCreatedBy(endorsementId); + endorsementModel.setUpdatedBy(endorsementId); when(endorsementRepository.findById(endorsementId)).thenReturn(Optional.of(endorsementModel)); @@ -510,24 +534,26 @@ public void itShouldHandleEndorsementNotFound() { @Test void testCreateEndorsement() { // Mock data - UUID userId = UUID.randomUUID(); + UUID endorserId = UUID.randomUUID(); UUID skillId = UUID.randomUUID(); UUID endorsementId = UUID.randomUUID(); EndorsementDRO endorsementDRO = new EndorsementDRO(); - endorsementDRO.setUserId(userId); + endorsementDRO.setEndorseeId(endorserId); endorsementDRO.setSkillId(skillId); - UserModel mockUser = UserModel.builder().id(userId).build(); SkillModel mockSkill = SkillModel.builder().id(skillId).build(); EndorsementModel mockEndorsement = - EndorsementModel.builder().id(endorsementId).user(mockUser).skill(mockSkill).build(); + EndorsementModel.builder() + .id(endorsementId) + .endorseeId(endorserId) + .skill(mockSkill) + .build(); mockEndorsement.setCreatedAt(Instant.now()); mockEndorsement.setUpdatedAt(Instant.now()); - mockEndorsement.setCreatedBy(mockUser); - mockEndorsement.setUpdatedBy(mockUser); + mockEndorsement.setCreatedBy(endorserId); + mockEndorsement.setUpdatedBy(endorserId); // Mock the repository behavior - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); when(skillRepository.findById(skillId)).thenReturn(Optional.of(mockSkill)); when(endorsementRepository.save(any(EndorsementModel.class))).thenReturn(mockEndorsement); @@ -539,53 +565,182 @@ void testCreateEndorsement() { // Assertions assertNotNull(result); - assertEquals(userId, result.getUser().getId()); + assertEquals(endorserId, result.getEndorseeId()); assertEquals(skillId, result.getSkill().getId()); } @Test - void testCreateEndorsementWithInvalidUser() { - UUID userId = UUID.randomUUID(); + void testCreateEndorsementWithInvalidSkill() { + UUID endorserId = UUID.randomUUID(); UUID skillId = UUID.randomUUID(); EndorsementDRO endorsementDRO = new EndorsementDRO(); - endorsementDRO.setUserId(userId); + endorsementDRO.setEndorseeId(endorserId); endorsementDRO.setSkillId(skillId); - // Mock the repository behavior for an invalid user - when(userRepository.findById(userId)).thenReturn(Optional.empty()); + // Mock the repository behavior for an invalid skill + when(skillRepository.findById(skillId)).thenReturn(Optional.empty()); // Assert that a NoEntityException is thrown NoEntityException exception = assertThrows( NoEntityException.class, () -> endorsementService.createEndorsement(endorsementDRO)); - assertEquals("User with id:" + userId + " not found", exception.getMessage()); + assertEquals("Skill with id:" + skillId + " not found", exception.getMessage()); // Verify that save method is not called verify(endorsementRepository, never()).save(any(EndorsementModel.class)); } @Test - void testCreateEndorsementWithInvalidSkill() { - UUID userId = UUID.randomUUID(); + @DisplayName( + "Return unauthorized access, given user is not a super user to update endorsement status") + public void itShouldReturnUnauthorizedGivenUserIsNotSuperUser() { + setupUpdateEndorsementTests(false); + + UUID endorsementId = UUID.randomUUID(); + String status = EndorsementStatus.APPROVED.name(); + + AccessDeniedException exception = + assertThrows( + AccessDeniedException.class, + () -> endorsementService.updateEndorsementStatus(endorsementId, status)); + assertEquals("Unauthorized, Access is only available to super users", exception.getMessage()); + verify(endorsementRepository, never()).save(any(EndorsementModel.class)); + } + + @Test + @DisplayName("Return invalid status given status is pending") + public void itShouldReturnInvalidStatusGivenEndorsementStatusIsPending() { + setupUpdateEndorsementTests(true); + + UUID endorsementId = UUID.randomUUID(); + String status = EndorsementStatus.PENDING.name(); + + InvalidParameterException exception = + assertThrows( + InvalidParameterException.class, + () -> endorsementService.updateEndorsementStatus(endorsementId, status)); + assertEquals("Invalid parameter endorsement status: " + status, exception.getMessage()); + verify(endorsementRepository, never()).save(any(EndorsementModel.class)); + } + + @Test + @DisplayName("Return invalid status given status is invalid") + public void itShouldReturnInvalidStatusGivenInvalidEndorsementStatus() { + setupUpdateEndorsementTests(true); + + UUID endorsementId = UUID.randomUUID(); + String status = "invalid-status"; + + InvalidParameterException exception = + assertThrows( + InvalidParameterException.class, + () -> endorsementService.updateEndorsementStatus(endorsementId, status)); + assertEquals("Invalid parameter endorsement status: " + status, exception.getMessage()); + verify(endorsementRepository, never()).save(any(EndorsementModel.class)); + } + + @Test + @DisplayName("Return cannot modify status given status is already updated") + public void itShouldThrowEntityAlreadyExistsExceptionGivenEndorsementIsUpdated() { + setupUpdateEndorsementTests(true); + + UUID endorseeId = UUID.randomUUID(); UUID skillId = UUID.randomUUID(); - EndorsementDRO endorsementDRO = new EndorsementDRO(); - endorsementDRO.setUserId(userId); - endorsementDRO.setSkillId(skillId); + UUID endorsementId = UUID.randomUUID(); + + SkillModel mockSkill = SkillModel.builder().id(skillId).build(); + EndorsementModel mockEndorsement = + EndorsementModel.builder() + .id(endorsementId) + .status(EndorsementStatus.APPROVED) + .endorseeId(endorseeId) + .skill(mockSkill) + .build(); + mockEndorsement.setCreatedAt(Instant.now()); + mockEndorsement.setUpdatedAt(Instant.now()); + mockEndorsement.setCreatedBy(endorseeId); + mockEndorsement.setUpdatedBy(endorseeId); - UserModel mockUser = new UserModel(); - mockUser.setId(userId); + when(endorsementRepository.findById(endorsementId)).thenReturn(Optional.of(mockEndorsement)); - // Mock the repository behavior for an invalid skill - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(skillRepository.findById(skillId)).thenReturn(Optional.empty()); + EntityAlreadyExistsException exception = + assertThrows( + EntityAlreadyExistsException.class, + () -> + endorsementService.updateEndorsementStatus( + endorsementId, EndorsementStatus.APPROVED.name())); + assertEquals("Endorsement is already updated. Cannot modify status", exception.getMessage()); + verify(endorsementRepository, never()).save(any(EndorsementModel.class)); + } + + @Test + @DisplayName("Return endorsement not found given an unknown endorsement id") + public void itShouldReturnEndorsementNotFoundGivenUnknownEndorsementId() { + setupUpdateEndorsementTests(true); + + UUID nonExistentEndorsementId = UUID.randomUUID(); + String status = EndorsementStatus.APPROVED.name(); + + when(endorsementRepository.findById(nonExistentEndorsementId)).thenReturn(Optional.empty()); - // Assert that a NoEntityException is thrown NoEntityException exception = assertThrows( - NoEntityException.class, () -> endorsementService.createEndorsement(endorsementDRO)); - assertEquals("Skill with id:" + skillId + " not found", exception.getMessage()); - - // Verify that save method is not called + NoEntityException.class, + () -> endorsementService.updateEndorsementStatus(nonExistentEndorsementId, status)); + assertEquals( + "No endorsement with id " + nonExistentEndorsementId + " was found", + exception.getMessage()); verify(endorsementRepository, never()).save(any(EndorsementModel.class)); } + + @Test + @DisplayName( + "Update endorsement status given a valid endorsement id and status is approved or rejected") + public void itShouldUpdateEndorsementStatusGivenEndorsementIdAndStatusApprovedOrRejected() { + setupUpdateEndorsementTests(true); + + UUID endorseeId = UUID.randomUUID(); + UUID skillId = UUID.randomUUID(); + UUID endorsementId = UUID.randomUUID(); + EndorsementStatus status = EndorsementStatus.APPROVED; + + SkillModel mockSkill = SkillModel.builder().id(skillId).build(); + EndorsementModel mockEndorsement = + EndorsementModel.builder() + .id(endorsementId) + .status(EndorsementStatus.PENDING) + .endorseeId(endorseeId) + .skill(mockSkill) + .build(); + mockEndorsement.setCreatedAt(Instant.now()); + mockEndorsement.setUpdatedAt(Instant.now()); + mockEndorsement.setCreatedBy(endorseeId); + mockEndorsement.setUpdatedBy(endorseeId); + + when(endorsementRepository.findById(endorsementId)).thenReturn(Optional.of(mockEndorsement)); + + GenericResponse result = + endorsementService.updateEndorsementStatus(endorsementId, status.name()); + assertEquals("Successfully updated endorsement status", result.getMessage()); + + verify(endorsementRepository, times(1)).save(any(EndorsementModel.class)); + + EndorsementModel updatedMockEndorsement = + EndorsementModel.builder() + .id(endorsementId) + .endorseeId(endorseeId) + .skill(mockSkill) + .status(EndorsementStatus.APPROVED) + .build(); + mockEndorsement.setCreatedAt(Instant.now()); + mockEndorsement.setUpdatedAt(Instant.now()); + mockEndorsement.setCreatedBy(endorseeId); + mockEndorsement.setUpdatedBy(endorseeId); + + when(endorsementRepository.findById(endorsementId)) + .thenReturn(Optional.of(updatedMockEndorsement)); + Optional updatedEndorsement = endorsementRepository.findById(endorsementId); + assertTrue(updatedEndorsement.isPresent()); + assertEquals(EndorsementStatus.APPROVED, updatedEndorsement.get().getStatus()); + } }