diff --git a/assessment-api/open-api-specification.yml b/assessment-api/open-api-specification.yml index 0eef846..4b295d7 100644 --- a/assessment-api/open-api-specification.yml +++ b/assessment-api/open-api-specification.yml @@ -137,6 +137,32 @@ paths: description: 'Not found' '500': description: 'Internal server error' + /assessments/{assessment-id}/checkpoint: + delete: + tags: + - assessments + summary: 'delete checkpoint from assessment' + operationId: 'deleteAssessmentCheckpoint' + parameters: + - name: 'assessment-id' + in: 'path' + required: true + schema: + type: 'integer' + format: 'int64' + example: '1234567890' + - $ref: '#/components/parameters/requiredUserLoginId' + responses: + '204': + description: 'Successful deletion' + '400': + description: 'Bad request' + '401': + description: 'Unauthorized' + '404': + description: 'Not found' + '500': + description: 'Internal server error' components: parameters: @@ -192,6 +218,8 @@ components: default: [ ] items: $ref: '#/components/schemas/assessmentEntityTypeDetail' + checkpoint: + $ref: '#/components/schemas/assessmentCheckpointDetail' audit_detail: $ref: '#/components/schemas/auditDetail' patchAssessmentDetail: @@ -218,6 +246,13 @@ components: format: 'date' last_saved_by: type: 'string' + assessmentCheckpointDetail: + type: 'object' + properties: + username: + type: 'string' + interviewData: + type: 'string' assessmentEntityTypeDetail: type: object properties: diff --git a/assessment-service/src/integrationTest/resources/sql/assessment_tables_create_schema.sql b/assessment-service/src/integrationTest/resources/sql/assessment_tables_create_schema.sql index 6fab6ea..64667a8 100644 --- a/assessment-service/src/integrationTest/resources/sql/assessment_tables_create_schema.sql +++ b/assessment-service/src/integrationTest/resources/sql/assessment_tables_create_schema.sql @@ -58,4 +58,11 @@ CREATE TABLE XXCCMS_OPA_RELSHIPTARGET ( TARGET_ENTITY_ID VARCHAR2(255 CHAR) NOT NULL, FK_OPA_RELATIONSHIP NUMBER(19, 0), PRIMARY KEY (ID) +); + +CREATE TABLE XXCCMS_OPA_CHECKPOINT ( + RESUME_ID NUMBER(19,0) NOT NULL, + USER_NAME VARCHAR2(255 CHAR), + INTERVIEW_DATA BLOB, + CONSTRAINT PK_OPA_CHECKPOINT PRIMARY KEY (RESUME_ID) ); \ No newline at end of file diff --git a/assessment-service/src/integrationTest/resources/sql/assessment_tables_drop_schema.sql b/assessment-service/src/integrationTest/resources/sql/assessment_tables_drop_schema.sql index 4b86bdd..8fb5372 100644 --- a/assessment-service/src/integrationTest/resources/sql/assessment_tables_drop_schema.sql +++ b/assessment-service/src/integrationTest/resources/sql/assessment_tables_drop_schema.sql @@ -3,4 +3,5 @@ DROP TABLE XXCCMS_OPA_RELATIONSHIP; DROP TABLE XXCCMS_OPA_ATTRIBUTE; DROP TABLE XXCCMS_OPA_ENTITY; DROP TABLE XXCCMS_OPA_LISTENTITY; +DROP TABLE XXCCMS_OPA_CHECKPOINT; DROP TABLE XXCCMS_OPA_SESSION; \ No newline at end of file diff --git a/assessment-service/src/integrationTest/resources/sql/assessments_insert.sql b/assessment-service/src/integrationTest/resources/sql/assessments_insert.sql index 558b45c..146383a 100644 --- a/assessment-service/src/integrationTest/resources/sql/assessments_insert.sql +++ b/assessment-service/src/integrationTest/resources/sql/assessments_insert.sql @@ -19,4 +19,9 @@ VALUES (30, 28, 'name1', 1); -- Insert into XXCCMS_OPA_RELSHIPTARGET INSERT INTO XXCCMS_OPA_RELSHIPTARGET (ID, TARGET_ENTITY_ID) -VALUES (31, 'entityId1'); \ No newline at end of file +VALUES (31, 'entityId1'); + +INSERT INTO XXCCMS_OPA_CHECKPOINT (RESUME_ID, USER_NAME, INTERVIEW_DATA) +VALUES (1, 'john.doe', hextoraw('48656c6c6f20776f726c64')); + + diff --git a/assessment-service/src/integrationTest/resources/sql/delete_data.sql b/assessment-service/src/integrationTest/resources/sql/delete_data.sql index e8929fa..d236f92 100644 --- a/assessment-service/src/integrationTest/resources/sql/delete_data.sql +++ b/assessment-service/src/integrationTest/resources/sql/delete_data.sql @@ -3,4 +3,5 @@ DELETE FROM XXCCMS_OPA_RELATIONSHIP; DELETE FROM XXCCMS_OPA_ATTRIBUTE; DELETE FROM XXCCMS_OPA_ENTITY; DELETE FROM XXCCMS_OPA_LISTENTITY; -DELETE FROM XXCCMS_OPA_SESSION; \ No newline at end of file +DELETE FROM XXCCMS_OPA_CHECKPOINT; +DELETE FROM XXCCMS_OPA_SESSION; diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentController.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentController.java index b45812a..f4bec85 100644 --- a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentController.java +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentController.java @@ -57,6 +57,16 @@ public ResponseEntity getAssessments( return ResponseEntity.ok(assessmentService.getAssessments(criteria, name)); } + @Override + public ResponseEntity deleteAssessmentCheckpoint( + final Long assessmentId, + final String caabUserLoginId) { + + assessmentService.deleteCheckpoint(assessmentId); + + return ResponseEntity.noContent().build(); + } + /** * Deletes assessments based on given criteria and a list of names. * diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaCheckpoint.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaCheckpoint.java new file mode 100644 index 0000000..c09f23a --- /dev/null +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaCheckpoint.java @@ -0,0 +1,41 @@ +package uk.gov.laa.ccms.caab.assessment.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Represents an Oracle Intelligence Advisor Checkpoint. + */ +@Entity +@Table(name = "XXCCMS_OPA_CHECKPOINT") +@Data +public class OpaCheckpoint { + + @Id + @Column(name = "RESUME_ID") + private long resumeId; + + @Column(name = "USER_NAME") + private String username; + + @Column(name = "INTERVIEW_DATA") + @Lob + private byte[] interviewData; + + @OneToOne(cascade = { + CascadeType.PERSIST, + CascadeType.MERGE + }) + @MapsId + @JoinColumn(name = "RESUME_ID") + private OpaSession opaSession; + +} diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaSession.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaSession.java index 52eaabf..6a26fff 100644 --- a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaSession.java +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/entity/OpaSession.java @@ -8,6 +8,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import java.util.List; @@ -76,11 +77,13 @@ public class OpaSession { ) private List opaListEntities; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "opaSession") + private OpaCheckpoint checkpoint; + /** * audit trail info. */ @Embedded private AuditTrail auditTrail = new AuditTrail(); - } diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapper.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapper.java index 579cb64..26decd3 100644 --- a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapper.java +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapper.java @@ -30,11 +30,13 @@ public interface AssessmentMapper { @Mapping(target = "targetId", source = "caseReferenceNumber") @Mapping(target = "auditTrail", ignore = true) @Mapping(target = "opaListEntities", ignore = true) + @Mapping(target = "checkpoint", ignore = true) OpaSession toOpaSession(AssessmentDetail assessmentDetail); @InheritInverseConfiguration(name = "toOpaSession") @Mapping(target = "auditDetail", source = "auditTrail") @Mapping(target = "entityTypes", source = "opaListEntities") + @Mapping(target = "checkpoint", source = "checkpoint") AssessmentDetail toAssessmentDetail(OpaSession opaSession); @Mapping(target = "entities", source = "opaEntities") @@ -55,6 +57,7 @@ public interface AssessmentMapper { @Mapping(target = "auditTrail", ignore = true) @Mapping(target = "opaListEntities", ignore = true) @Mapping(target = "id", ignore = true) + @Mapping(target = "checkpoint", ignore = true) @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) void mapIntoOpaSession( @MappingTarget OpaSession opaSession, diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/CommonMapper.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/CommonMapper.java index 6dedf2c..bfa4970 100644 --- a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/CommonMapper.java +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/mapper/CommonMapper.java @@ -1,5 +1,6 @@ package uk.gov.laa.ccms.caab.assessment.mapper; +import java.util.Base64; import org.mapstruct.Mapper; /** @@ -17,5 +18,14 @@ public interface CommonMapper { default Boolean toBoolean(Boolean flag) { return flag != null ? flag : Boolean.FALSE; } + + + default String toBase64String(byte[] bytes) { + return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null; + } + + default byte[] toByteArrayFromBase64EncodedString(String base64EncodedString) { + return Base64.getDecoder().decode(base64EncodedString); + } } diff --git a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentService.java b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentService.java index fa6ffea..dc8a2cb 100644 --- a/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentService.java +++ b/assessment-service/src/main/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentService.java @@ -1,6 +1,5 @@ package uk.gov.laa.ccms.caab.assessment.service; -import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.transaction.Transactional; import java.util.ArrayList; @@ -78,6 +77,32 @@ public void deleteAssessments( opaSessionRepository.findAll(buildQuerySpecification(Example.of(example), names))); } + /** + * Deletes a checkpoint from an assessment. + * + * @param assessmentId the ID of the assessment to delete the checkpoint from + * @throws ApplicationException if the checkpoint with the specified ID + * does not exist. + */ + public void deleteCheckpoint( + final Long assessmentId) { + + opaSessionRepository.findById(assessmentId) + .ifPresentOrElse( + assessment -> { + assessment.getCheckpoint().setOpaSession(null); + assessment.setCheckpoint(null); + opaSessionRepository.save(assessment); + }, () -> { + throw new ApplicationException( + String.format("Assessment checkpoint with id: %s not found", assessmentId), + HttpStatus.NOT_FOUND); + } + ); + } + + + /** * Updates an assessment's details in the database. * @@ -106,7 +131,7 @@ public void updateAssessment( * @param names a list of names to include in the query; may be null or empty. * @return the Specification object that constructs the predicate for querying. */ - private Specification buildQuerySpecification( + protected Specification buildQuerySpecification( final Example assessment, final List names) { return (root, query, builder) -> { diff --git a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentControllerTest.java b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentControllerTest.java index e9ce97c..c47d2f1 100644 --- a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentControllerTest.java +++ b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/controller/AssessmentControllerTest.java @@ -4,9 +4,11 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -20,6 +22,7 @@ import uk.gov.laa.ccms.caab.assessment.exception.ApplicationException; import uk.gov.laa.ccms.caab.assessment.model.AssessmentDetail; import uk.gov.laa.ccms.caab.assessment.model.AssessmentDetails; +import uk.gov.laa.ccms.caab.assessment.model.PatchAssessmentDetail; import uk.gov.laa.ccms.caab.assessment.service.AssessmentService; @WebMvcTest(AssessmentController.class) @@ -138,4 +141,15 @@ public void deleteAssessments_returnsNoContentWhenNoMatchingAssessments() throws verify(assessmentService).deleteAssessments(criteria, names); } + @Test + public void deleteAssessmentCheckpoint_returnsNoContent_whenCheckpointExists() throws Exception { + Long assessmentId = 1L; + + this.mockMvc.perform(delete("/assessments/{assessment-id}/checkpoint", assessmentId) + .header("caab-User-Login-Id", "TestUser")) + .andExpect(status().isNoContent()); + + verify(assessmentService).deleteCheckpoint(assessmentId); + } + } \ No newline at end of file diff --git a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapperTest.java b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapperTest.java index 42d33e5..db72e0f 100644 --- a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapperTest.java +++ b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/mapper/AssessmentMapperTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Arrays; @@ -17,12 +19,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.laa.ccms.caab.assessment.entity.OpaAttribute; +import uk.gov.laa.ccms.caab.assessment.entity.OpaCheckpoint; import uk.gov.laa.ccms.caab.assessment.entity.OpaEntity; import uk.gov.laa.ccms.caab.assessment.entity.OpaListEntity; import uk.gov.laa.ccms.caab.assessment.entity.OpaRelationship; import uk.gov.laa.ccms.caab.assessment.entity.OpaRelationshipTarget; import uk.gov.laa.ccms.caab.assessment.entity.OpaSession; import uk.gov.laa.ccms.caab.assessment.model.AssessmentAttributeDetail; +import uk.gov.laa.ccms.caab.assessment.model.AssessmentCheckpointDetail; import uk.gov.laa.ccms.caab.assessment.model.AssessmentDetail; import uk.gov.laa.ccms.caab.assessment.model.AssessmentDetails; import uk.gov.laa.ccms.caab.assessment.model.AssessmentEntityDetail; @@ -357,6 +361,25 @@ void mapIntoOpaSession_WithPartialDetail_MapsNonNullFields() { assertNull(opaSession.getStatus()); } + @Test + void opaCheckpointToAssessmentCheckpointDetail_returnsNull_whenOpaCheckpointIsNull() { + AssessmentCheckpointDetail result = assessmentMapper.opaCheckpointToAssessmentCheckpointDetail(null); + assertNull(result); + } + + @Test + void opaCheckpointToAssessmentCheckpointDetail_returnsAssessmentCheckpointDetail_whenOpaCheckpointIsNotNull() { + OpaCheckpoint opaCheckpoint = mock(OpaCheckpoint.class); + when(opaCheckpoint.getUsername()).thenReturn("testUser"); + when(opaCheckpoint.getInterviewData()).thenReturn(new byte[0]); + + AssessmentCheckpointDetail result = assessmentMapper.opaCheckpointToAssessmentCheckpointDetail(opaCheckpoint); + + assertNotNull(result); + assertEquals("testUser", result.getUsername()); + assertEquals("", result.getInterviewData()); + } + } \ No newline at end of file diff --git a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentServiceTest.java b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentServiceTest.java index d2b37a0..c56e3a1 100644 --- a/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentServiceTest.java +++ b/assessment-service/src/test/java/uk/gov/laa/ccms/caab/assessment/service/AssessmentServiceTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -19,6 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpStatus; +import uk.gov.laa.ccms.caab.assessment.entity.OpaCheckpoint; import uk.gov.laa.ccms.caab.assessment.entity.OpaSession; import uk.gov.laa.ccms.caab.assessment.exception.ApplicationException; import uk.gov.laa.ccms.caab.assessment.mapper.AssessmentMapper; @@ -27,6 +29,7 @@ import uk.gov.laa.ccms.caab.assessment.model.PatchAssessmentDetail; import uk.gov.laa.ccms.caab.assessment.repository.OpaSessionRepository; +@SuppressWarnings({"unchecked"}) @ExtendWith(MockitoExtension.class) class AssessmentServiceTest { @@ -138,4 +141,71 @@ void testUpdateAssessmentNotFound() { verify(opaSessionRepository, never()).save(any(OpaSession.class)); } + @Test + void deleteAssessments_deletesAssessmentsMatchingCriteriaAndNames() { + AssessmentDetail criteria = new AssessmentDetail() + .providerId("providerId") + .caseReferenceNumber("caseReferenceNumber") + .status("status"); + List names = List.of("name1", "name2"); + + OpaSession session = new OpaSession(); + when(assessmentMapper.toOpaSession(criteria)).thenReturn(session); + when(opaSessionRepository.findAll(any(Specification.class))).thenReturn(List.of(session)); + + assessmentService.deleteAssessments(criteria, names); + + verify(assessmentMapper).toOpaSession(criteria); + verify(opaSessionRepository).findAll(any(Specification.class)); + verify(opaSessionRepository).deleteAll(List.of(session)); + } + + @Test + void deleteAssessments_doesNothingWhenNoMatchingAssessments() { + AssessmentDetail criteria = new AssessmentDetail() + .providerId("nonExistentProviderId") + .caseReferenceNumber("nonExistentCaseReferenceNumber") + .status("nonExistentStatus"); + List names = List.of("nonExistentName"); + + OpaSession session = new OpaSession(); + when(assessmentMapper.toOpaSession(criteria)).thenReturn(session); + when(opaSessionRepository.findAll(any(Specification.class))).thenReturn(List.of()); + + assessmentService.deleteAssessments(criteria, names); + + verify(assessmentMapper).toOpaSession(criteria); + verify(opaSessionRepository).findAll(any(Specification.class)); + } + + @Test + void deleteCheckpoint_deletesCheckpointWhenExists() { + Long assessmentId = 1L; + OpaSession session = new OpaSession(); + session.setCheckpoint(new OpaCheckpoint()); + + when(opaSessionRepository.findById(assessmentId)).thenReturn(Optional.of(session)); + + assessmentService.deleteCheckpoint(assessmentId); + + verify(opaSessionRepository).findById(assessmentId); + verify(opaSessionRepository).save(session); + assertNull(session.getCheckpoint()); + } + + @Test + void deleteCheckpoint_throwsExceptionWhenCheckpointDoesNotExist() { + Long assessmentId = 1L; + + when(opaSessionRepository.findById(assessmentId)).thenReturn(Optional.empty()); + + ApplicationException thrown = assertThrows(ApplicationException.class, () -> { + assessmentService.deleteCheckpoint(assessmentId); + }); + + assertEquals(HttpStatus.NOT_FOUND, thrown.getHttpStatus()); + assertTrue(thrown.getMessage().contains("Assessment checkpoint with id: " + assessmentId + " not found")); + verify(opaSessionRepository).findById(assessmentId); + } + } \ No newline at end of file