diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFT.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFT.java index cad0052c94..17d27da357 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFT.java @@ -104,9 +104,10 @@ void editRequestFromCsvForbidden(TestingSupportRoles role) { @Test @DisplayName("Should not create an edit with a csv that has unsafe data in fields") - void editRequestWithUnsafeDataCsv() throws JsonProcessingException { + void editRequestWithUnsafeDataCsv() { CreateRecordingResponse recordingDetails = createRecording(); - RecordingDTO recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true).as(RecordingDTO.class); + RecordingDTO recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true) + .as(RecordingDTO.class); when(azureFinalStorageService.getMp4FileName(recordingDetails.recordingId().toString())) .thenReturn(recordingDTO.getFilename()); @@ -183,7 +184,7 @@ void reencodeOnlyEditRequestSuccess() throws JsonProcessingException { TestingSupportRoles.SUPER_USER ); - assertResponseCode(putResponse, 201); + assertResponseCode(putResponse, 204); EditRequestDTO getResponse = doGetRequest(EDIT_ENDPOINT + "/" + editRequestId, TestingSupportRoles.SUPER_USER) .as(EditRequestDTO.class); @@ -223,7 +224,7 @@ void editRequestWithNotificationsDisabledSuccess() throws JsonProcessingExceptio TestingSupportRoles.SUPER_USER ); - assertResponseCode(putResponse, 201); + assertResponseCode(putResponse, 204); EditRequestDTO getResponse = doGetRequest(EDIT_ENDPOINT + "/" + editRequestId, TestingSupportRoles.SUPER_USER) .as(EditRequestDTO.class); diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java new file mode 100644 index 0000000000..35c8470d95 --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -0,0 +1,250 @@ +package uk.gov.hmcts.reform.preapi.controllers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.response.Response; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import uk.gov.hmcts.reform.preapi.controllers.params.TestingSupportRoles; +import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; +import uk.gov.hmcts.reform.preapi.entities.Booking; +import uk.gov.hmcts.reform.preapi.entities.CaptureSession; +import uk.gov.hmcts.reform.preapi.entities.Case; +import uk.gov.hmcts.reform.preapi.entities.Court; +import uk.gov.hmcts.reform.preapi.entities.Recording; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; +import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase; + +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static java.lang.String.format; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +// I split this out to capture the first half of the editing process, which doesn't yet have any tests +class EditControllerFullyAutomatedFT extends FunctionalTestBase { + private static final String EDIT_ENDPOINT = "/edits"; + + @MockitoBean + private AzureFinalStorageService azureFinalStorageService; + + private UUID recordingId; + private RecordingDTO recordingDTO; + private Recording recording; + + @MockitoBean + private CaptureSession captureSession; + + @MockitoBean + private Booking booking; + + @MockitoBean + private Case legalCase; + + @MockitoBean + private Court mockCourt; + + + @BeforeEach + void setUp() { + CreateRecordingResponse recordingDetails = createRecording(); + recordingId = recordingDetails.recordingId(); + recording = new Recording(); + recording.setId(recordingId); + recording.setCaptureSession(captureSession); + recording.setDuration(Duration.ofMinutes(30)); + recording.setVersion(1); + recording.setCreatedAt(Timestamp.valueOf(LocalDateTime.now())); + recording.setFilename("Test_filename.mp4"); + + when(captureSession.getId()).thenReturn(recordingDetails.captureSessionId()); + when(captureSession.getBooking()).thenReturn(booking); + + when(booking.getId()).thenReturn(recordingDetails.bookingId()); + when(booking.getCaseId()).thenReturn(legalCase); + + when(legalCase.getCourt()).thenReturn(mockCourt); + when(mockCourt.getGroupEmail()).thenReturn("mock@email.com"); + + recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true) + .as(RecordingDTO.class); + + when(azureFinalStorageService.getMp4FileName(recordingDetails.recordingId().toString())) + .thenReturn(recordingDTO.getFilename()); + when(azureFinalStorageService.getRecordingDuration(recordingDetails.recordingId())) + .thenReturn(recordingDTO.getDuration()); + } + + @Test + @DisplayName("Should create a DRAFT edit request, update it and submit it. Should be read-only after submission.") + void editRequestSuccess() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = new CreateEditRequestDTO(); + UUID createEditRequestId = UUID.randomUUID(); + createEditRequestDTO.setId(createEditRequestId); + createEditRequestDTO.setStatus(EditRequestStatus.DRAFT); + createEditRequestDTO.setSourceRecordingId(recordingId); + + // Create as DRAFT + Response createdAsDraft = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); + assertResponseCode(createdAsDraft, 200); + Assertions.assertThat(createdAsDraft.jsonPath().getString("id")) + .isEqualTo(createEditRequestId.toString()); + Assertions.assertThat(createdAsDraft.jsonPath().getString("status")) + .isEqualTo(EditRequestStatus.DRAFT.name()); + Assertions.assertThat(createdAsDraft.jsonPath().getString("source_recording.id")) + .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); + Assertions.assertThat(createdAsDraft.jsonPath().getList("edit_instruction.requestedInstructions")) + .isEmpty(); + + // Update as DRAFT + List editInstructions = List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:02") + .endOfCut("00:00:03") + .build()); + createEditRequestDTO.setEditInstructions(editInstructions); + + Response updatedAsDraft = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); + assertResponseCode(updatedAsDraft, 200); + Assertions.assertThat(updatedAsDraft.jsonPath().getString("id")) + .isEqualTo(createEditRequestId.toString()); + Assertions.assertThat(updatedAsDraft.jsonPath().getString("status")) + .isEqualTo(EditRequestStatus.DRAFT.name()); + Assertions.assertThat(updatedAsDraft.jsonPath().getString("source_recording.id")) + .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); + + Assertions.assertThat(updatedAsDraft.jsonPath().getList("edit_instruction.requestedInstructions")) + .size().isEqualTo(editInstructions.size()); + Assertions.assertThat(updatedAsDraft.jsonPath() + .getInt("edit_instruction.requestedInstructions[0].start")) + .isEqualTo(2); + Assertions.assertThat(updatedAsDraft.jsonPath() + .getInt("edit_instruction.requestedInstructions[0].end")) + .isEqualTo(3); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + createEditRequestDTO.setJointlyAgreed(true); + Response submitted = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); + assertResponseCode(submitted, 200); + Assertions.assertThat(submitted.jsonPath().getString("id")) + .isEqualTo(createEditRequestId.toString()); + Assertions.assertThat(submitted.jsonPath().getString("status")) + .isEqualTo(EditRequestStatus.SUBMITTED.name()); + + // Attempt to update edit instructions after submission should fail + List updatedEditInstructions = List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:06") + .endOfCut("00:00:07") + .build()); + createEditRequestDTO.setEditInstructions(updatedEditInstructions); + + Response resubmittedWithChangedInstructions = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestId, + OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(resubmittedWithChangedInstructions, 400); + Assertions.assertThat(resubmittedWithChangedInstructions.jsonPath().getString("message")) + .isEqualTo(format( + "Cannot alter edit request instructions after submission: " + + "edit request %s has status %s", + createEditRequestDTO.getId(), createEditRequestDTO.getStatus().toString() + )); + } + + @Test + @DisplayName("Should record an audit trail when edit request is submitted") + void editRequestSubmissionAuditLog() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response firstResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + + assertResponseCode(firstResponse, 204); + + // TODO: Finish test here when https://tools.hmcts.net/jira/browse/S28-3556 is done + // Response auditResponse = doGetRequest(AUDIT_ENDPOINT...) + } + + @Test + @DisplayName("Should not create an edit request with unsafe data in rejection reason fields") + void editRequestWithUnsafeDataRejectionReason() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.REJECTED); + createEditRequestDTO.setRejectionReason("this & is unsafe"); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(putResponse, 400); + + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); + } + + @Test + @DisplayName("Should not create an edit request with unsafe data in approved by fields") + void editRequestWithUnsafeDataApprovedBy() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); + + // Approved + createEditRequestDTO.setStatus(EditRequestStatus.APPROVED); + createEditRequestDTO.setRejectionReason(null); + createEditRequestDTO.setApprovedBy("this & is unsafe"); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(putResponse, 400); + + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); + } + + @Test + @DisplayName("Should not create an edit request with unsafe data in reason fields") + void editRequestWithUnsafeDataReason() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); + + // Approved + createEditRequestDTO.setStatus(EditRequestStatus.APPROVED); + List editInstructions = createEditRequestDTO.getEditInstructions(); + editInstructions.forEach(editInstruction -> { + editInstruction.setReason("this & is unsafe"); + }); + createEditRequestDTO.setEditInstructions(editInstructions); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(putResponse, 400); + + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); + } + +} diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/RecordingControllerFT.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/RecordingControllerFT.java index 81b123fec6..eb8b5e75bc 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/RecordingControllerFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/RecordingControllerFT.java @@ -28,7 +28,7 @@ class RecordingControllerFT extends FunctionalTestBase { @DisplayName("Scenario: Restore recording") @Test void undeleteRecording() { - var recordingDetails = createRecording(); + CreateRecordingResponse recordingDetails = createRecording(); assertRecordingExists(recordingDetails.recordingId(), true); var deleteResponse = diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/email/GovNotifyFT.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/email/GovNotifyFT.java index 15c48cfbca..4439837b5e 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/email/GovNotifyFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/email/GovNotifyFT.java @@ -1,24 +1,31 @@ package uk.gov.hmcts.reform.preapi.email; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; import uk.gov.hmcts.reform.preapi.email.govnotify.GovNotify; import uk.gov.hmcts.reform.preapi.entities.Booking; import uk.gov.hmcts.reform.preapi.entities.CaptureSession; import uk.gov.hmcts.reform.preapi.entities.Case; import uk.gov.hmcts.reform.preapi.entities.Court; import uk.gov.hmcts.reform.preapi.entities.EditRequest; -import uk.gov.hmcts.reform.preapi.entities.Participant; import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.User; -import uk.gov.hmcts.reform.preapi.enums.ParticipantType; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; +import uk.gov.hmcts.reform.preapi.utils.JsonUtils; import java.sql.Timestamp; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @SpringBootTest class GovNotifyFT { @@ -32,6 +39,9 @@ class GovNotifyFT { @Autowired GovNotify client; + @MockitoBean + EditRequest editRequest; + private User createUser() { var user = new User(); user.setFirstName(USER_FIRST_NAME); @@ -43,38 +53,35 @@ private User createUser() { private Case createCase() { var court = new Court(); court.setName(COURT_NAME); + court.setGroupEmail(FROM_EMAIL_ADDRESS); + var forCase = new Case(); forCase.setCourt(court); forCase.setReference(CASE_REFERENCE); return forCase; } - private Participant createParticipant(ParticipantType type) { - var participant = new Participant(); - participant.setFirstName("First"); - participant.setLastName("Last"); - participant.setParticipantType(type); - return participant; - } + @BeforeEach + public void setUp() { + Case testCase = createCase(); + Booking booking = mock(Booking.class); + when(booking.getCaseId()).thenReturn(testCase); + when(booking.getWitnessName()).thenReturn(USER_FIRST_NAME); + when(booking.getDefendantName()).thenReturn(USER_LAST_NAME); + + CaptureSession captureSession = mock(CaptureSession.class); + when(captureSession.getBooking()).thenReturn(booking); + + Recording recording = mock(Recording.class); + when(recording.getCaptureSession()).thenReturn(captureSession); + + EditCutInstructionDTO instruction1 = new EditCutInstructionDTO(0, 30, "first reason"); + EditInstructions defaultEditInstructions = new EditInstructions(List.of(instruction1), new ArrayList<>()); + when(editRequest.getEditInstruction()).thenReturn(JsonUtils.toJson(defaultEditInstructions)); - private EditRequest createEditRequest() { - var aCase = createCase(); - var booking = new Booking(); - booking.setCaseId(aCase); - booking.setParticipants(Set.of( - createParticipant(ParticipantType.WITNESS), - createParticipant(ParticipantType.DEFENDANT))); - var captureSession = new CaptureSession(); - captureSession.setBooking(booking); - var recording = new Recording(); - recording.setCaptureSession(captureSession); - var request = new EditRequest(); - request.setSourceRecording(recording); - request.setEditInstruction( - "{\"requestedInstructions\":" - + "[{\"start_of_cut\":\"00:00:00\",\"end_of_cut\":\"00:00:30\",\"reason\":\"\",\"start\":0,\"end\":0}]," - + "\"ffmpegInstructions\":[]}"); - return request; + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(editRequest.getSourceRecording()).thenReturn(recording); } private void compareBody(String expected, EmailResponse emailResponse) { @@ -276,10 +283,11 @@ void verifyEmail() { @DisplayName("Should send editing jointly agreed email") @SuppressWarnings("LineLength") void editingJointlyAgreed() { - var user = createUser(); - var forEditRequest = createEditRequest(); + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + EmailResponse response = client.sendEmailAboutEditingRequest(editRequest).orElseThrow(); - var response = client.editingJointlyAgreed(user.getEmail(), forEditRequest); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( "[Do Not Reply] Pre-recorded Evidence: Edit request for case reference 123456", @@ -291,29 +299,30 @@ void editingJointlyAgreed() { Court: Court Name Case reference: 123456 - Witness name: First - Defendant name(s): First Last + Witness name: John + Defendant name(s): Doe Edit 1:\s Start time: 00:00:00 End time: 00:00:30 - Time Removed: 00:00:00 - Reason:\s + Time Removed: 00:00:30 + Reason: first reason Edits have been jointly agreed: Yes - PRE Portal link: [http://localhost:8080](http://localhost:8080)""", response); + PRE Portal link: [http://localhost:8080](http://localhost:8080)""", response + ); } @Test @DisplayName("Should send editing not jointly agreed email") @SuppressWarnings("LineLength") void editingNotJointlyAgreed() { - var user = createUser(); - var forEditRequest = createEditRequest(); + when(editRequest.getJointlyAgreed()).thenReturn(false); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - var response = client.editingNotJointlyAgreed(user.getEmail(), forEditRequest); + EmailResponse response = client.sendEmailAboutEditingRequest(editRequest).orElseThrow(); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( "[Do Not Reply] Pre-recorded Evidence: Edit request for case reference 123456 (NOT JOINTLY AGREED)", @@ -325,14 +334,14 @@ void editingNotJointlyAgreed() { Court: Court Name Case reference: 123456 - Witness name: First - Defendant name(s): First Last + Witness name: John + Defendant name(s): Doe Edit 1:\s Start time: 00:00:00 End time: 00:00:30 - Time Removed: 00:00:00 - Reason:\s + Time Removed: 00:00:30 + Reason: first reason Edits have been jointly agreed: No @@ -344,12 +353,11 @@ Defendant name(s): First Last @DisplayName("Should send editing rejection email") @SuppressWarnings("LineLength") void editingRejectionEmail() { - var user = createUser(); - var forEditRequest = createEditRequest(); - forEditRequest.setRejectionReason("REJECTION REASON"); - forEditRequest.setJointlyAgreed(true); + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getRejectionReason()).thenReturn("REJECTION REASON"); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.REJECTED); - var response = client.editingRejected(user.getEmail(), forEditRequest); + var response = client.sendEmailAboutEditingRequest(editRequest).orElseThrow(); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( "[Do Not Reply] Pre-recorded Evidence: Edit request REJECTION for case reference 123456", @@ -363,14 +371,14 @@ void editingRejectionEmail() { Court: Court Name Case reference: 123456 - Witness name: First - Defendant name(s): First Last + Witness name: John + Defendant name(s): Doe Edit 1:\s Start time: 00:00:00 End time: 00:00:30 - Time Removed: 00:00:00 - Reason:\s + Time Removed: 00:00:30 + Reason: first reason Edits have been jointly agreed: Yes diff --git a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/util/FunctionalTestBase.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/util/FunctionalTestBase.java index 7ee7ff6cff..a555b11cc6 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/util/FunctionalTestBase.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/util/FunctionalTestBase.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.response.Response; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.context.SpringBootTest; @@ -17,11 +18,14 @@ import uk.gov.hmcts.reform.preapi.dto.CreateCaptureSessionDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCaseDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCourtDTO; +import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.CreateInviteDTO; import uk.gov.hmcts.reform.preapi.dto.CreateParticipantDTO; import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateShareBookingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateUserDTO; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.ParticipantDTO; import uk.gov.hmcts.reform.preapi.enums.CaseState; import uk.gov.hmcts.reform.preapi.enums.CourtType; @@ -63,6 +67,8 @@ public class FunctionalTestBase { protected static final String LOCATION_HEADER = "Location"; protected static final String LEGACY_REPORTS_ENDPOINT = "/reports"; protected static final String REPORTS_ENDPOINT = "/reports-v2"; + protected static final String TRIGGER_TASK_ENDPOINT = "/testing-support/trigger-task"; + protected static final String EDIT_ENDPOINT = "/edits"; protected static final Map MULTIPART_HEADERS = Map.of("Content-Type", MediaType.MULTIPART_FORM_DATA_VALUE); @@ -355,6 +361,15 @@ protected Response putCaptureSession(CreateCaptureSessionDTO dto) throws JsonPro ); } + protected EditRequestDTO getEditRequest(UUID id) { + Response response = doGetRequest( + EDIT_ENDPOINT + "/" + id, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(response, 200); + return response.body().as(EditRequestDTO.class); + } + protected CreateParticipantDTO convertDtoToCreateDto(ParticipantDTO dto) { var create = new CreateParticipantDTO(); create.setId(dto.getId()); @@ -437,6 +452,36 @@ protected CreateCaptureSessionDTO createCaptureSession() { return dto; } + protected CreateEditRequestDTO createEditRequestDTO(UUID recordingId) { + CreateEditRequestDTO createEditRequestDTO = new CreateEditRequestDTO(); + UUID editRequestId = UUID.randomUUID(); + createEditRequestDTO.setId(editRequestId); + createEditRequestDTO.setSourceRecordingId(recordingId); + List editInstructions = List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:00") + .endOfCut("00:00:01") + .build()); + createEditRequestDTO.setEditInstructions(editInstructions); + createEditRequestDTO.setJointlyAgreed(true); + return createEditRequestDTO; + } + + protected @NotNull Response upsertEditRequestAndGetResponse(UUID createEditRequestId, + CreateEditRequestDTO createEditRequestDTO) + throws JsonProcessingException { + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestId, + OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(putResponse, 204); + + return doGetRequest( + EDIT_ENDPOINT + "/" + createEditRequestId, + TestingSupportRoles.SUPER_USER + ); + } + protected CreateRecordingDTO createRecording(UUID captureSessionId) { var dto = new CreateRecordingDTO(); dto.setId(UUID.randomUUID()); diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java b/src/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java index 52899b98e3..ec35bc3380 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java @@ -8,19 +8,22 @@ import org.springframework.transaction.annotation.Transactional; import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; import uk.gov.hmcts.reform.preapi.entities.CaptureSession; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import uk.gov.hmcts.reform.preapi.enums.RecordingOrigin; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; import uk.gov.hmcts.reform.preapi.util.HelperFactory; import uk.gov.hmcts.reform.preapi.utils.IntegrationTestBase; import java.sql.Timestamp; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -148,19 +151,21 @@ public void deleteEditRequest() { @Test @Transactional - public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { + public void upsertDraftWithEmptyInstructionsShouldDeleteEditRequestInstructions() { var recording = HelperFactory.createRecording(captureSession, null, 1, "filename", null); + recording.setDuration(Duration.ofHours(1)); entityManager.persist(recording); when(azureFinalStorageService.getRecordingDuration(recording.getId())).thenReturn(recording.getDuration()); when(azureFinalStorageService.getMp4FileName(recording.getId().toString())).thenReturn("filename"); UUID editRequestId = UUID.randomUUID(); + String editInstructions = "{\"ffmpegInstructions\":[{\"start\":0,\"end\":60},{\"start\":120,\"end\":180}]}"; var editRequest = HelperFactory.createEditRequest( editRequestId, recording, - "{\"ffmpegInstructions\":[{\"start\":0,\"end\":60},{\"start\":120,\"end\":180}]}", - EditRequestStatus.PENDING, + editInstructions, + EditRequestStatus.DRAFT, user, null, null, @@ -173,19 +178,33 @@ public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { SearchEditRequests paramsForExistingEditRequest = new SearchEditRequests(); paramsForExistingEditRequest.setSourceRecordingId(recording.getId()); - var requests1 = editRequestService.findAll(paramsForExistingEditRequest, Pageable.unpaged()).toList(); + List requests1 = editRequestService.findAll(paramsForExistingEditRequest, + Pageable.unpaged()).toList(); assertThat(requests1).hasSize(1); assertThat(requests1.getFirst().getId()).isEqualTo(editRequest.getId()); + List ffmpegInstructions = requests1.getFirst() + .getEditInstruction().getFfmpegInstructions(); + assertThat(ffmpegInstructions.getFirst().getStart()) + .isEqualTo(0); + assertThat(ffmpegInstructions.getFirst().getEnd()) + .isEqualTo(60); + assertThat(ffmpegInstructions.get(1).getStart()) + .isEqualTo(120); + assertThat(ffmpegInstructions.get(1).getEnd()) + .isEqualTo(180); CreateEditRequestDTO upsertRequest = new CreateEditRequestDTO(); upsertRequest.setSourceRecordingId(recording.getId()); upsertRequest.setId(editRequestId); + upsertRequest.setStatus(EditRequestStatus.DRAFT); upsertRequest.setEditInstructions(new ArrayList<>()); // Intentionally empty - UpsertResult upsertResult = editRequestService.upsert(upsertRequest); - assertThat(upsertResult).isEqualTo(UpsertResult.UPDATED); + editRequestService.upsert(upsertRequest); - var requests2 = editRequestService.findAll(paramsForExistingEditRequest, Pageable.unpaged()).toList(); - assertThat(requests2).isEmpty(); + List requests2 = editRequestService.findAll(paramsForExistingEditRequest, + Pageable.unpaged()).toList(); + assertThat(requests2).hasSize(1); + assertThat(requests2.getFirst().getId()).isEqualTo(editRequest.getId()); + assertThat(requests2.getFirst().getEditInstruction().getFfmpegInstructions()).isEmpty(); } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/controllers/EditController.java b/src/main/java/uk/gov/hmcts/reform/preapi/controllers/EditController.java index 84291ef6fa..64055e459f 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/controllers/EditController.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/controllers/EditController.java @@ -13,7 +13,6 @@ import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpEntity; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,6 +31,7 @@ import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.PathPayloadMismatchException; import uk.gov.hmcts.reform.preapi.exception.RequestedPageOutOfRangeException; @@ -122,7 +122,8 @@ public ResponseEntity upsertEditRequest( throw new PathPayloadMismatchException("editRequestId", "createEditRequestDTO.id"); } - return getUpsertResponse(editRequestService.upsert(createEditRequestDTO), id); + editRequestService.upsert(createEditRequestDTO); + return getUpsertResponse(UpsertResult.UPDATED, id); } @DeleteMapping("/{id}") @@ -136,7 +137,7 @@ public ResponseEntity delete( } editRequestService.delete(deleteEditRequestDTO); - return new ResponseEntity<>(HttpStatus.OK); + return getUpsertResponse(UpsertResult.UPDATED, id); } @PreAuthorize("hasAnyRole('ROLE_SUPER_USER')") diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/controllers/TestingSupportController.java b/src/main/java/uk/gov/hmcts/reform/preapi/controllers/TestingSupportController.java index 3e6f63cdfc..b840197bae 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/controllers/TestingSupportController.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/controllers/TestingSupportController.java @@ -600,6 +600,7 @@ private Court createTestCourt() { Court court = new Court(); court.setId(UUID.randomUUID()); court.setName("Foo Court"); + court.setGroupEmail("foo_court@email.com"); court.setCourtType(CourtType.CROWN); court.setLocationCode(UUID.randomUUID().toString().replace("-", "").substring(0, 20)); courtRepository.save(court); diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/IEmailService.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/IEmailService.java index 5b4cfec8c8..d846b9d384 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/IEmailService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/IEmailService.java @@ -5,6 +5,7 @@ import uk.gov.hmcts.reform.preapi.entities.User; import java.sql.Timestamp; +import java.util.Optional; public interface IEmailService { EmailResponse recordingReady(User to, Case forCase); @@ -21,9 +22,5 @@ public interface IEmailService { EmailResponse emailVerification(String email, String firstName, String lastName, String verificationCode); - EmailResponse editingJointlyAgreed(String to, EditRequest editRequest); - - EmailResponse editingNotJointlyAgreed(String to, EditRequest editRequest); - - EmailResponse editingRejected(String to, EditRequest editRequest); + Optional sendEmailAboutEditingRequest(EditRequest editRequest); } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotify.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotify.java index 2d1169e194..925cd0386e 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotify.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotify.java @@ -4,21 +4,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; import uk.gov.hmcts.reform.preapi.email.EmailResponse; import uk.gov.hmcts.reform.preapi.email.IEmailService; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.BaseTemplate; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CaseClosed; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CaseClosureCancelled; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CasePendingClosure; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditingJointlyAgreed; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditingNotJointlyAgreed; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditingRejection; +import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditEmailParameters; +import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditRequestEmailTemplate; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EmailVerification; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.PortalInvite; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.RecordingEdited; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.RecordingReady; -import uk.gov.hmcts.reform.preapi.entities.Booking; import uk.gov.hmcts.reform.preapi.entities.Case; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.User; @@ -28,16 +25,10 @@ import uk.gov.service.notify.SendEmailResponse; import java.sql.Timestamp; -import java.time.Duration; -import java.util.List; -import java.util.StringJoiner; - -import static java.lang.String.format; -import static uk.gov.hmcts.reform.preapi.media.edit.EditInstructions.fromJson; +import java.util.Optional; @Slf4j @Service -@SuppressWarnings("PMD.CouplingBetweenObjects") public class GovNotify implements IEmailService { private final NotificationClient client; private final String portalUrl; @@ -93,11 +84,13 @@ public EmailResponse recordingEdited(User to, Case forCase) { @Override public EmailResponse portalInvite(User to) { - PortalInvite template = new PortalInvite(to.getEmail(), to.getFirstName(), to.getLastName(), portalUrl, - portalUrl + "/assets/files/user-guide.pdf", - portalUrl + "/assets/files/process-guide.pdf", - portalUrl + "/assets/files/faqs.pdf", - portalUrl + "/assets/files/pre-editing-request-form.xlsx"); + PortalInvite template = new PortalInvite( + to.getEmail(), to.getFirstName(), to.getLastName(), portalUrl, + portalUrl + "/assets/files/user-guide.pdf", + portalUrl + "/assets/files/process-guide.pdf", + portalUrl + "/assets/files/faqs.pdf", + portalUrl + "/assets/files/pre-editing-request-form.xlsx" + ); try { log.info("Portal invite email sent to {}", to.getEmail()); return EmailResponse.fromGovNotifyResponse(sendEmail(template)); @@ -109,8 +102,10 @@ public EmailResponse portalInvite(User to) { @Override public EmailResponse casePendingClosure(User to, Case forCase, Timestamp date) { - CasePendingClosure template = new CasePendingClosure(to.getEmail(), to.getFirstName(), to.getLastName(), - forCase.getReference(), date); + CasePendingClosure template = new CasePendingClosure( + to.getEmail(), to.getFirstName(), to.getLastName(), + forCase.getReference(), date + ); try { log.info("Case pending closure email sent to {}", to.getEmail()); return EmailResponse.fromGovNotifyResponse(sendEmail(template)); @@ -139,8 +134,10 @@ public EmailResponse caseClosed(User to, Case forCase) { @Override public EmailResponse caseClosureCancelled(User to, Case forCase) { - CaseClosureCancelled template = new CaseClosureCancelled(to.getEmail(), to.getFirstName(), to.getLastName(), - forCase.getReference()); + CaseClosureCancelled template = new CaseClosureCancelled( + to.getEmail(), to.getFirstName(), to.getLastName(), + forCase.getReference() + ); try { log.info("Case closure cancelled email sent to {}", to.getEmail()); return EmailResponse.fromGovNotifyResponse(sendEmail(template)); @@ -163,118 +160,34 @@ public EmailResponse emailVerification(String email, String firstName, String la } @Override - public EmailResponse editingJointlyAgreed(String to, EditRequest editRequest) { - Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); - List requestInstructions = fromJson(editRequest.getEditInstruction()) - .getRequestedInstructions(); - - String witnessName = booking.getWitnessName(); - String defendant = booking.getDefendantName(); - - EditingJointlyAgreed template = new EditingJointlyAgreed( - to, - booking.getCaseId().getReference(), - requestInstructions.size(), - booking.getCaseId().getCourt().getName(), - witnessName, - defendant, - generateEditSummary(requestInstructions), - portalUrl - ); + public Optional sendEmailAboutEditingRequest(EditRequest editRequest) { + EditEmailParameters editEmailParameters; try { - log.info("Edit request jointly agreed email sent to {}", to); - return EmailResponse.fromGovNotifyResponse(sendEmail(template)); - } catch (NotificationClientException e) { - log.error("Failed to send edit request jointly agreed email to {}", to, e); - throw new EmailFailedToSendException(to, e); + editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + } catch (Exception e) { + log.error("Failed to create email parameters for edit request submission: {}", e.getMessage()); + return Optional.empty(); } - } - - @Override - public EmailResponse editingNotJointlyAgreed(String to, EditRequest editRequest) { - Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); - List requestInstructions = fromJson(editRequest.getEditInstruction()) - .getRequestedInstructions(); - String witnessName = booking.getWitnessName(); + EditRequestEmailTemplate template = new EditRequestEmailTemplate(editEmailParameters); - String defendant = booking.getDefendantName(); - - EditingNotJointlyAgreed template = new EditingNotJointlyAgreed( - to, - booking.getCaseId().getReference(), - requestInstructions.size(), - booking.getCaseId().getCourt().getName(), - witnessName, - defendant, - generateEditSummary(requestInstructions), - portalUrl - ); + String to = editEmailParameters.getToEmailAddress(); try { - log.info("Edit request not jointly agreed email sent to {}", to); - return EmailResponse.fromGovNotifyResponse(sendEmail(template)); + log.info("Edit request {} email sent to {}", template.getEditingEmailType(), to); + return Optional.of(EmailResponse.fromGovNotifyResponse(sendEmail(template))); } catch (NotificationClientException e) { - log.error("Failed to send edit request not jointly agreed email to {}", to, e); + log.error("Failed to send edit request {} email to {}", template.getEditingEmailType(), to, e); throw new EmailFailedToSendException(to, e); } - } - - @Override - public EmailResponse editingRejected(String to, EditRequest editRequest) { - Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); - List requestInstructions = fromJson(editRequest.getEditInstruction()) - .getRequestedInstructions(); - String witnessName = booking.getWitnessName(); - String defendant = booking.getDefendantName(); - EditingRejection template = new EditingRejection( - to, - booking.getCaseId().getReference(), - editRequest.getRejectionReason(), - booking.getCaseId().getCourt().getName(), - witnessName, - defendant, - generateEditSummary(requestInstructions), - editRequest.getJointlyAgreed(), - portalUrl - ); - - try { - log.info("Edit request rejection email sent to {}", to); - return EmailResponse.fromGovNotifyResponse(sendEmail(template)); - } catch (NotificationClientException e) { - log.error("Failed to send edit request rejection email to {}", to, e); - throw new EmailFailedToSendException(to, e); - } } private SendEmailResponse sendEmail(BaseTemplate email) throws NotificationClientException { return client.sendEmail(email.getTemplateId(), email.getTo(), email.getVariables(), email.getReference()); } - private String generateEditSummary(List editInstructions) { - StringJoiner summary = new StringJoiner(""); - for (int i = 0; i < editInstructions.size(); i++) { - EditCutInstructionDTO instruction = editInstructions.get(i); - summary.add(format("Edit %s: %n", i + 1)) - .add(format("Start time: %s%n", instruction.getStartOfCut())) - .add(format("End time: %s%n", instruction.getEndOfCut())) - .add(format("Time Removed: %s%n", calculateTimeRemoved(instruction))) - .add(format("Reason: %s%n%n", instruction.getReason())); - } - - return summary.toString(); - } - - private String calculateTimeRemoved(EditCutInstructionDTO instruction) { - long difference = instruction.getEnd() - instruction.getStart(); - Duration duration = Duration.ofSeconds(difference); - - return format("%02d:%02d:%02d", duration.toHours(), duration.toMinutesPart(), duration.toSecondsPart()); - } - // If the users alternative email ends with .cjsm.net then use that as the preferred email, else fall back // to the email field. private String getUsersPreferredEmail(User user) { diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParameters.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParameters.java new file mode 100644 index 0000000000..f79cd31007 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParameters.java @@ -0,0 +1,154 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.entities.Booking; +import uk.gov.hmcts.reform.preapi.entities.Court; +import uk.gov.hmcts.reform.preapi.entities.EditRequest; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.exception.BadRequestException; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import static java.lang.String.format; + +@Slf4j +@Getter +public class EditEmailParameters { + + private final Boolean jointlyAgreed; + private final EditRequestStatus editRequestStatus; + private final String toEmailAddress; // court group email + private final Map emailParameters; + + public EditEmailParameters(EditRequest editRequest, String portalUrl) { + if (portalUrl == null) { + throw new BadRequestException("Portal URL is missing"); + } + + validateEditRequestIsOkayForNotification(editRequest); + + Booking booking = validateAndRetrieveBooking(editRequest); + + List requestInstructions = validateAndRetrieveRequestInstructions(editRequest); + String summary = generateEditSummary(requestInstructions); + + this.jointlyAgreed = editRequest.getJointlyAgreed(); + String jointlyAgreedText = editRequest.getJointlyAgreed() ? "Yes" : "No"; + + this.emailParameters = Map.of( + "edit_summary", summary, + "rejection_reason", editRequest.getRejectionReason() == null ? "" : editRequest.getRejectionReason(), + "jointly_agreed", jointlyAgreedText, + "case_reference", booking.getCaseId().getReference(), + "court_name", booking.getCaseId().getCourt().getName(), + "witness_name", booking.getWitnessName(), + "defendant_names", booking.getDefendantName(), + "portal_link", portalUrl, + "edit_count", requestInstructions.size() + ); + this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); + this.editRequestStatus = editRequest.getStatus(); + } + + private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { + if (editRequest == null) { + throw new NotFoundException("No edit request found when trying to send notification"); + } + + if (editRequest.getSourceRecording() == null) { + throw new NotFoundException("No recording found when trying to send edit notification, edit ID: " + + editRequest.getId()); + } + + if (editRequest.getSourceRecording().getCaptureSession() == null) { + throw new NotFoundException("No capture session found when trying to send edit notification, edit ID: " + + editRequest.getId()); + } + } + + private Booking validateAndRetrieveBooking(EditRequest editRequest) { + Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); + if (booking == null) { + throw new NotFoundException("No booking found when trying to send edit notification, edit ID: " + + editRequest.getId()); + } + + if (booking.getCaseId() == null) { + throw new NotFoundException("No case found when trying to send edit notification, edit ID: " + + editRequest.getId()); + } + + Court court = booking.getCaseId().getCourt(); + if (court == null) { + throw new NotFoundException("No court found when trying to send edit notification, edit ID: " + + editRequest.getId()); + } + + String courtEmailAddress = court.getGroupEmail(); + + if (courtEmailAddress.isBlank()) { + throw new BadRequestException( + format( + "Court %s does not have a group email for sending edit request submission email" + + " for edit ID: %s: ", + court.getName(), editRequest.getId() + )); + } + return booking; + } + + private static List validateAndRetrieveRequestInstructions(EditRequest editRequest) { + if (editRequest.getEditInstruction() == null) { + throw new BadRequestException("No instructions found when trying to send edit notification for edit ID: " + + editRequest.getId()); + } + + EditInstructions instructions = EditInstructions.tryFromJson(editRequest.getEditInstruction()); + if (instructions == null) { + throw new BadRequestException("Could not parse instructions for edit request : " + + editRequest.getId()); + } + + if (!instructions.shouldSendNotifications()) { + throw new BadRequestException("Instructions say not to notify for edit request : " + + editRequest.getId()); + } + + List requestInstructions = instructions.getRequestedInstructions(); + + if (requestInstructions == null || requestInstructions.isEmpty()) { + throw new BadRequestException("No instructions found when trying to send edit notification for edit ID: " + + editRequest.getId()); + } + return requestInstructions; + } + + private String generateEditSummary(List editInstructions) { + StringJoiner summary = new StringJoiner(""); + for (int i = 0; i < editInstructions.size(); i++) { + EditCutInstructionDTO instruction = editInstructions.get(i); + summary.add(format("Edit %s: %n", i + 1)) + .add(format("Start time: %s%n", instruction.getStartOfCut())) + .add(format("End time: %s%n", instruction.getEndOfCut())) + .add(format("Time Removed: %s%n", calculateTimeRemoved(instruction))) + .add(format("Reason: %s%n%n", instruction.getReason())); + } + + return summary.toString(); + } + + private String calculateTimeRemoved(EditCutInstructionDTO instruction) { + long difference = instruction.getEnd() - instruction.getStart(); + Duration duration = Duration.ofSeconds(difference); + + return format("%02d:%02d:%02d", duration.toHours(), duration.toMinutesPart(), duration.toSecondsPart()); + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java new file mode 100644 index 0000000000..3f52c96db1 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java @@ -0,0 +1,49 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import lombok.Getter; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; + +import static java.lang.String.format; + +public class EditRequestEmailTemplate extends BaseTemplate { + + @Getter + private final EditingEmailType editingEmailType; + + public EditRequestEmailTemplate(EditEmailParameters editEmailParameters) { + super( + editEmailParameters.getToEmailAddress(), + editEmailParameters.getEmailParameters() + ); + + this.editingEmailType = calculateEditingEmailType(editEmailParameters); + } + + @Override + public String getTemplateId() { + return switch (editingEmailType) { + case REJECTED -> "aa2a836f-b6f0-46dc-91e0-1698822c5137"; + case NOT_JOINTLY_AGREED -> "fb11d2a9-086d-4f27-9208-a3ddfe696919"; + case JOINTLY_AGREED -> "018ad5d2-c7ba-42a8-ad50-6baaaecf210c"; + }; + } + + private EditingEmailType calculateEditingEmailType(EditEmailParameters editEmailParameters) { + if (editEmailParameters.getEditRequestStatus() == EditRequestStatus.REJECTED) { + return EditingEmailType.REJECTED; + } + + if (editEmailParameters.getEditRequestStatus() == EditRequestStatus.SUBMITTED) { + if (Boolean.TRUE.equals(editEmailParameters.getJointlyAgreed())) { + return EditingEmailType.JOINTLY_AGREED; + } else { + return EditingEmailType.NOT_JOINTLY_AGREED; + } + } + throw new IllegalArgumentException(format( + "Could not work out which type of edit email to send: edit status %s, jointly agreed %b", + editEmailParameters.getEditRequestStatus().name(), + editEmailParameters.getJointlyAgreed() + )); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingEmailType.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingEmailType.java new file mode 100644 index 0000000000..69bc488ad6 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingEmailType.java @@ -0,0 +1,7 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +public enum EditingEmailType { + JOINTLY_AGREED, + NOT_JOINTLY_AGREED, + REJECTED +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingJointlyAgreed.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingJointlyAgreed.java deleted file mode 100644 index 82bd3ce6e5..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingJointlyAgreed.java +++ /dev/null @@ -1,33 +0,0 @@ -package uk.gov.hmcts.reform.preapi.email.govnotify.templates; - -import java.util.Map; - -public class EditingJointlyAgreed extends BaseTemplate { - - public EditingJointlyAgreed(String to, - String caseReference, - int editCount, - String courtName, - String witnessName, - String defendantNames, - String editSummary, - String portalUrl) { - super( - to, - Map.of( - "case_reference", caseReference, - "edit_count", editCount, - "court_name", courtName, - "witness_name", witnessName, - "defendant_names", defendantNames, - "edit_summary", editSummary, - "portal_link", portalUrl - ) - ); - } - - @Override - public String getTemplateId() { - return "018ad5d2-c7ba-42a8-ad50-6baaaecf210c"; - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingNotJointlyAgreed.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingNotJointlyAgreed.java deleted file mode 100644 index fe0c6f8c36..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingNotJointlyAgreed.java +++ /dev/null @@ -1,32 +0,0 @@ -package uk.gov.hmcts.reform.preapi.email.govnotify.templates; - -import java.util.Map; - -public class EditingNotJointlyAgreed extends BaseTemplate { - public EditingNotJointlyAgreed(String to, - String caseReference, - int editCount, - String courtName, - String witnessName, - String defendantNames, - String editSummary, - String portalUrl) { - super( - to, - Map.of( - "case_reference", caseReference, - "edit_count", editCount, - "court_name", courtName, - "witness_name", witnessName, - "defendant_names", defendantNames, - "edit_summary", editSummary, - "portal_link", portalUrl - ) - ); - } - - @Override - public String getTemplateId() { - return "fb11d2a9-086d-4f27-9208-a3ddfe696919"; - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingRejection.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingRejection.java deleted file mode 100644 index 4529d4ba7d..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingRejection.java +++ /dev/null @@ -1,34 +0,0 @@ -package uk.gov.hmcts.reform.preapi.email.govnotify.templates; - -import java.util.Map; - -public class EditingRejection extends BaseTemplate { - public EditingRejection(String to, - String caseReference, - String rejectionReason, - String courtName, - String witnessName, - String defendantNames, - String editSummary, - boolean jointlyAgreed, - String portalUrl) { - super( - to, - Map.of( - "case_reference", caseReference, - "rejection_reason", rejectionReason, - "court_name", courtName, - "witness_name", witnessName, - "defendant_names", defendantNames, - "edit_summary", editSummary, - "jointly_agreed", jointlyAgreed ? "Yes" : "No", - "portal_link", portalUrl - ) - ); - } - - @Override - public String getTemplateId() { - return "aa2a836f-b6f0-46dc-91e0-1698822c5137"; - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/EditNotificationService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditNotificationService.java index c00b211ec2..e2b50f03ba 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/EditNotificationService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditNotificationService.java @@ -5,11 +5,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import uk.gov.hmcts.reform.preapi.email.EmailServiceFactory; -import uk.gov.hmcts.reform.preapi.email.IEmailService; import uk.gov.hmcts.reform.preapi.entities.Booking; -import uk.gov.hmcts.reform.preapi.entities.Court; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.ShareBooking; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; @Service @Slf4j @@ -29,44 +28,29 @@ public void sendNotifications(Booking booking) { .forEach(u -> emailServiceFactory.getEnabledEmailService().recordingEdited(u, booking.getCaseId())); } - @Transactional - public void onEditRequestSubmitted(EditRequest request) { - Court court = request.getSourceRecording().getCaptureSession().getBooking().getCaseId().getCourt(); - if (court.getGroupEmail() == null) { - log.error("Court {} does not have a group email for sending edit request submission email for request: {}", - court.getId(), request.getId()); + public void editRequestStatusWasUpdated(EditRequest editRequest) { + if (editRequest == null) { + log.error("Tried to send email notification about a null edit request"); return; } - String groupEmail = court.getGroupEmail(); + if (!isNotifiable(editRequest.getStatus())) { + // For other statuses e.g. completed edits, notification is sent by RecordingListener instead + log.info("No notification needed for edit request status {}", editRequest.getStatus().name()); + return; + } try { - IEmailService enabledEmailService = emailServiceFactory.getEnabledEmailService(); - - if (Boolean.TRUE.equals(request.getJointlyAgreed())) { - enabledEmailService.editingJointlyAgreed(groupEmail, request); - } else { - enabledEmailService.editingNotJointlyAgreed(groupEmail, request); - } + emailServiceFactory.getEnabledEmailService().sendEmailAboutEditingRequest(editRequest); } catch (Exception e) { log.error("Error sending email on edit request submission: {}", e.getMessage()); } } - @Transactional - public void onEditRequestRejected(EditRequest request) { - Court court = request.getSourceRecording().getCaptureSession().getBooking().getCaseId().getCourt(); - if (court.getGroupEmail() == null) { - log.error("Court {} does not have a group email for sending edit request rejection email for request: {}", - court.getId(), request.getId()); - return; - } - - try { - emailServiceFactory.getEnabledEmailService().editingRejected(court.getGroupEmail(), request); - } catch (Exception e) { - log.error("Error sending email on edit request rejection: {}", e.getMessage()); - } + // An email should be sent to counsel when status becomes... + // This can be overridden by e.g. EditInstructions.shouldSendNotifications + public static boolean isNotifiable(EditRequestStatus status) { + return status == EditRequestStatus.REJECTED || status == EditRequestStatus.SUBMITTED; } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java index 62e5f1a854..5ea38951eb 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java @@ -8,7 +8,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.util.Pair; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -22,7 +21,6 @@ import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; @@ -47,7 +45,6 @@ public class EditRequestService { private final EditRequestCrudService editRequestCrudService; private final RecordingRepository recordingRepository; private final RecordingService recordingService; - private final EditNotificationService editNotificationService; private final boolean hideReencodedRecordings; private static final String ROLE_SUPER_USER = "ROLE_SUPER_USER"; @@ -56,13 +53,11 @@ public class EditRequestService { public EditRequestService(final EditRequestCrudService editRequestCrudService, final RecordingRepository recordingRepository, final RecordingService recordingService, - final EditNotificationService editNotificationService, @Value("${feature-flags.hide-reencoded-recordings:true}") final boolean hideReencodedRecordings) { this.editRequestCrudService = editRequestCrudService; this.recordingRepository = recordingRepository; this.recordingService = recordingService; - this.editNotificationService = editNotificationService; this.hideReencodedRecordings = hideReencodedRecordings; } @@ -111,15 +106,13 @@ public void delete(CreateEditRequestDTO dto) { @Transactional @PreAuthorize("@authorisationService.hasUpsertAccess(authentication, #dto)") - public UpsertResult upsert(CreateEditRequestDTO dto) { + public void upsert(CreateEditRequestDTO dto) { Recording sourceRecording = getSourceRecording(dto.getSourceRecordingId()); UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); - Pair result = editRequestCrudService.upsert(dto, sourceRecording, user); - notifyOnUpdatedRequest(dto, result); - return result.getFirst(); + editRequestCrudService.upsert(dto, sourceRecording, user); } @Transactional @@ -166,19 +159,6 @@ private Recording getSourceRecording(UUID sourceRecordingId) { return sourceRecording; } - private void notifyOnUpdatedRequest(CreateEditRequestDTO dto, Pair upserted) { - if (!upserted.getFirst().equals(UpsertResult.UPDATED)) { - return; - } - - if (dto.getStatus() == EditRequestStatus.SUBMITTED) { - editNotificationService.onEditRequestSubmitted(upserted.getSecond()); - return; - } - - editNotificationService.onEditRequestRejected(upserted.getSecond()); - } - private boolean canViewReencodedRecordings() { UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); return canViewReencodedRecordings(auth); diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java index d2cf70e871..eaa37f1f09 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java @@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; @@ -15,29 +14,35 @@ import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; -import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.services.EditNotificationService; import java.sql.Timestamp; import java.time.Instant; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import static uk.gov.hmcts.reform.preapi.utils.JsonUtils.toJson; + @Slf4j @Service public class EditRequestCrudService { private final EditRequestRepository editRequestRepository; private final IEditingService editingService; + private final EditNotificationService editNotificationService; @Autowired public EditRequestCrudService(final EditRequestRepository editRequestRepository, - final IEditingService editingService) { + final IEditingService editingService, + final EditNotificationService editNotificationService) { this.editRequestRepository = editRequestRepository; this.editingService = editingService; + this.editNotificationService = editNotificationService; } public Optional findByIdIfExists(UUID id) { @@ -52,7 +57,8 @@ public EditRequestDTO findById(UUID id) { public EditRequestDTO findById(UUID id, boolean includeReencodedRecordings) { return editRequestRepository .findByIdNotLocked(id) - .map(editRequest -> new EditRequestDTO(editRequest, true, includeReencodedRecordings)) + .map(editRequest -> + new EditRequestDTO(editRequest, true, includeReencodedRecordings)) .orElseThrow(() -> new NotFoundException("Edit Request: " + id)); } @@ -91,6 +97,9 @@ public EditRequest updateEditRequestStatus(UUID id, EditRequestStatus status) { } } editRequestRepository.save(request); + + editNotificationService.editRequestStatusWasUpdated(request); + return request; } @@ -107,31 +116,56 @@ public void delete(CreateEditRequestDTO dto) { } @Transactional - public @NotNull Pair upsert(CreateEditRequestDTO dto, - Recording sourceRecording, User user) { + public @NotNull EditRequest upsert(CreateEditRequestDTO dto, Recording sourceRecording, User user) { EditRequestValidator.validateEditMode(dto); Optional existingEditRequest = findByIdIfExists(dto.getId()); boolean isUpdate = existingEditRequest.isPresent(); - UpsertResult emptyInstructionResult = handleEmptyInstructions(dto, existingEditRequest, isUpdate); - if (emptyInstructionResult != null) { - return Pair.of(emptyInstructionResult, existingEditRequest.orElse(new EditRequest())); + + if (isUpdate) { + EditRequestValidator.extraValidationForExistingEditRequest(existingEditRequest.get(), dto); } - EditRequest request = editingService.prepareEditRequestToCreateOrUpdate( - dto, sourceRecording, - existingEditRequest.orElse(new EditRequest()) - ); + EditRequest updatedEditRequest = getEditRequestToUpdate(dto, sourceRecording, existingEditRequest); if (!isUpdate) { - request.setCreatedBy(user); - request.setCreatedAt(Timestamp.from(Instant.now())); + updatedEditRequest.setCreatedBy(user); + updatedEditRequest.setCreatedAt(Timestamp.from(Instant.now())); } - editRequestRepository.save(request); - if (isUpdate) { - return Pair.of(UpsertResult.UPDATED, request); + editRequestRepository.saveAndFlush(updatedEditRequest); + + boolean editStatusWasUpdated = !isUpdate || !existingEditRequest.get().getStatus().equals(dto.getStatus()); + if (editStatusWasUpdated) { + editNotificationService.editRequestStatusWasUpdated(updatedEditRequest); + } + + return updatedEditRequest; + } + + private EditRequest getEditRequestToUpdate(final CreateEditRequestDTO dto, + final Recording sourceRecording, + final Optional existingEditRequest) { + + EditRequest request = existingEditRequest.orElse(new EditRequest()); + if (EditRequestValidator.editInstructionsAreEmpty(dto) && dto.getStatus() == EditRequestStatus.DRAFT) { + String newEditInstruction = toJson(new EditInstructions( + List.of(), List.of(), false, + dto.getSendNotifications() + )); + + request.updateEditRequestFromDto(dto, sourceRecording, newEditInstruction); + return request; + } + + if (dto.isForceReencode()) { + String newEditInstruction = toJson(new EditInstructions( + List.of(), List.of(), true, dto.getSendNotifications())); + request.updateEditRequestFromDto(dto, sourceRecording, newEditInstruction); + return request; } - return Pair.of(UpsertResult.CREATED, request); + + return editingService.mergeOldAndNewEditInstructions(dto, sourceRecording, + existingEditRequest.orElse(new EditRequest())); } @Transactional @@ -142,26 +176,5 @@ public Set findRecordingIdsWithForceReencodeRequests(Set sourceRecor return editRequestRepository.findSourceRecordingIdsWithForceReencodeRequests(sourceRecordingIds); } - - private UpsertResult handleEmptyInstructions(CreateEditRequestDTO dto, - Optional existingEditRequest, - boolean isUpdate) { - if (dto.isForceReencode() || dto.getEditInstructions() != null && !dto.getEditInstructions().isEmpty()) { - return null; - } - - if (!isUpdate) { - throw new BadRequestException("Invalid Instruction: Cannot create an edit request with empty" - + " instructions"); - } - - log.info( - "Deleting edit request {} for source recording {} as edit instructions are empty", - existingEditRequest.orElseThrow().getId(), dto.getSourceRecordingId() - ); - delete(dto); - return UpsertResult.UPDATED; - } - } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestValidator.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestValidator.java index fb56d48a14..2023d689fc 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestValidator.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestValidator.java @@ -13,9 +13,11 @@ import java.util.List; +import static java.lang.String.format; import static uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO.formatTime; @Slf4j +@SuppressWarnings("PMD.GodClass") public final class EditRequestValidator { static final Integer SINGLETON_LIST_SIZE = 1; @@ -31,10 +33,16 @@ static void ensureEditRequestHasSourceRecording(EditRequest request) { static void validateEditMode(CreateEditRequestDTO dto) { log.debug("Validating edit request {}", dto); - if (dto.isForceReencode() && dto.getEditInstructions() != null && !dto.getEditInstructions().isEmpty()) { + if (dto.isForceReencode() && !editInstructionsAreEmpty(dto)) { throw new BadRequestException( "Invalid Instruction: Cannot request cuts and force reencode on the same edit request"); } + + if (editInstructionsAreEmpty(dto) + && !(dto.isForceReencode() || dto.getStatus().equals(EditRequestStatus.DRAFT))) { + throw new IllegalArgumentException("Invalid edit request: instructions may only be empty for DRAFT" + + " edit requests or forced re-encodes"); + } } static void ensureStatusIsPendingBeforeProcessingEdit(EditRequestDTO request) { @@ -76,7 +84,7 @@ static void checkForNonEmptyInstructionsOrForceReencode(EditInstructions instruc } static void checkThatEditsDoNotCutAnEntireRecording(final List instructions, - final long recordingDuration) { + final long recordingDuration) { if (instructions.size() == SINGLETON_LIST_SIZE) { EditCutInstructionDTO firstInstruction = instructions.getFirst(); if (firstInstruction.getStart() == 0 && firstInstruction.getEnd() == recordingDuration) { @@ -106,7 +114,7 @@ static void checkThatEditsDoNotOverlap(final List instruc } static void validateEditInstruction(final EditCutInstructionDTO instruction, - final long recordingDuration) { + final long recordingDuration) { if (instruction.getStart() == instruction.getEnd()) { throw new BadRequestException( "Invalid instruction: Instruction with 0 second duration invalid: Start(" @@ -133,4 +141,22 @@ static void validateEditInstruction(final EditCutInstructionDTO instruction, + ")"); } } + + public static void extraValidationForExistingEditRequest(EditRequest existingEditRequest, + CreateEditRequestDTO dto) { + if (existingEditRequest.getStatus() != EditRequestStatus.DRAFT) { + EditInstructions existing = EditInstructions.fromJson(existingEditRequest.getEditInstruction()); + if (!dto.getEditInstructions().equals(existing)) { + throw new BadRequestException(format( + "Cannot alter edit request instructions after submission: edit request %s has status %s", + existingEditRequest.getId(), + existingEditRequest.getStatus().toString() + )); + } + } + } + + public static boolean editInstructionsAreEmpty(CreateEditRequestDTO dto) { + return dto.getEditInstructions() == null || dto.getEditInstructions().isEmpty(); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java index 116f671cbb..e50bc7ad1a 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java @@ -210,16 +210,9 @@ protected void generateConcatListFile(final Set segmentFiles, final Stri } @Override - public @NotNull EditRequest prepareEditRequestToCreateOrUpdate(final CreateEditRequestDTO dto, - final Recording sourceRecording, - final EditRequest request) { - if (dto.isForceReencode()) { - String newEditInstruction = toJson(new EditInstructions( - List.of(), List.of(), true, - shouldSendNotifications(dto))); - request.updateEditRequestFromDto(dto, sourceRecording, newEditInstruction); - return request; - } + public @NotNull EditRequest mergeOldAndNewEditInstructions(final CreateEditRequestDTO dto, + final Recording sourceRecording, + final EditRequest request) { boolean isOriginalRecordingEdit = sourceRecording.getParentRecording() == null; @@ -254,7 +247,7 @@ protected void generateConcatListFile(final Set segmentFiles, final Stri String newEditInstruction = toJson(new EditInstructions( requestedEdits, editInstructions, false, - shouldSendNotifications(dto) + dto.getSendNotifications() )); request.updateEditRequestFromDto(dto, recordingForUpdatedEdit, newEditInstruction); @@ -262,10 +255,6 @@ protected void generateConcatListFile(final Set segmentFiles, final Stri return request; } - private boolean shouldSendNotifications(CreateEditRequestDTO dto) { - return !Boolean.FALSE.equals(dto.getSendNotifications()); - } - // Should be private but leaving as protected to avoid rewriting tests protected List invertInstructions(final List instructions, final Recording recording) { diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/IEditingService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/IEditingService.java index faefd04e64..92c2089512 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/IEditingService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/IEditingService.java @@ -9,7 +9,7 @@ public interface IEditingService { void performEdit(UUID newRecordingId, EditRequest request); - EditRequest prepareEditRequestToCreateOrUpdate(CreateEditRequestDTO createEditRequestDTO, - Recording sourceRecording, - EditRequest request); + EditRequest mergeOldAndNewEditInstructions(CreateEditRequestDTO createEditRequestDTO, + Recording sourceRecording, + EditRequest request); } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java new file mode 100644 index 0000000000..79ca207b24 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java @@ -0,0 +1,22 @@ +package uk.gov.hmcts.reform.preapi.utils; + +import static java.lang.String.format; + +public final class StringTools { + private StringTools() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static String formatTimeAsString(Integer time) { + if (time < 0) { + throw new IllegalArgumentException("Time in seconds cannot be negative: " + time); + } + + Integer hours = time / 3600; + Integer minutes = time % 3600 / 60; + Integer seconds = time % 60; + + return format("%02d:%02d:%02d", hours, minutes, seconds); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java index de299c8300..087cca3b56 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.reform.preapi.controller; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,6 +14,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.multipart.MultipartFile; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.com.fasterxml.jackson.databind.PropertyNamingStrategy; @@ -93,9 +95,9 @@ static void beforeAll() { @Test @DisplayName("Should return 200 when successfully created edit request from csv") void createEditFromCsvSuccess() throws Exception { - var dto = new EditRequestDTO(); + EditRequestDTO dto = new EditRequestDTO(); dto.setId(UUID.randomUUID()); - var sourceId = UUID.randomUUID(); + UUID sourceId = UUID.randomUUID(); when(editRequestService.upsert(sourceId, validFile)).thenReturn(dto); @@ -112,14 +114,14 @@ void createEditFromCsvSuccess() throws Exception { @Test @DisplayName("Should return 415 when attempting to create edit request with file that is not a csv") void createEditFromCsvNotCsv() throws Exception { - var notACsv = new MockMultipartFile( + MockMultipartFile notACsv = new MockMultipartFile( "file", "test.txt", MediaType.TEXT_PLAIN.toString(), "file content".getBytes() ); - var sourceId = UUID.randomUUID(); + UUID sourceId = UUID.randomUUID(); mockMvc.perform(multipart(TEST_URL + "/edits/from-csv/" + sourceId) .file(notACsv) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -133,14 +135,14 @@ void createEditFromCsvNotCsv() throws Exception { @Test @DisplayName("Should return 415 when attempting to create edit request with file that is not a csv (has null type)") void createEditFromCsvNotCsvIsNullType() throws Exception { - var notACsv = new MockMultipartFile( + MockMultipartFile notACsv = new MockMultipartFile( "file", "test.txt", null, "file content".getBytes() ); - var sourceId = UUID.randomUUID(); + UUID sourceId = UUID.randomUUID(); mockMvc.perform(multipart(TEST_URL + "/edits/from-csv/" + sourceId) .file(notACsv) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -154,9 +156,10 @@ void createEditFromCsvNotCsvIsNullType() throws Exception { @Test @DisplayName("Should return 400 when attempting to create edit request with empty file") void createdEditFromCsvEmpty() throws Exception { - var emptyCsv = new MockMultipartFile("file", "test.txt", "text/csv", new byte[0]); + MockMultipartFile emptyCsv = new MockMultipartFile("file", "test.txt", + "text/csv", new byte[0]); - var sourceId = UUID.randomUUID(); + UUID sourceId = UUID.randomUUID(); mockMvc.perform(multipart(TEST_URL + "/edits/from-csv/" + sourceId) .file(emptyCsv) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -170,7 +173,7 @@ void createdEditFromCsvEmpty() throws Exception { @Test @DisplayName("Should return 200 and edit request dto when edit request exists") void getByIdSuccess() throws Exception { - var dto = new EditRequestDTO(); + EditRequestDTO dto = new EditRequestDTO(); dto.setId(UUID.randomUUID()); when(editRequestService.findById(dto.getId())).thenReturn(dto); @@ -184,7 +187,7 @@ void getByIdSuccess() throws Exception { @Test @DisplayName("Should return 404 when edit request does not exist") void getByIdNotFound() throws Exception { - var id = UUID.randomUUID(); + UUID id = UUID.randomUUID(); doThrow(new NotFoundException("Edit Request: " + id)).when(editRequestService).findById(id); @@ -249,47 +252,20 @@ void getEditsOutOfBounds() throws Exception { } @Test - @DisplayName("Should return 201 when successfully created edit request") - void upsertEditRequestCreated() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); - dto.setStatus(EditRequestStatus.DRAFT); - - when(editRequestService.upsert(any(CreateEditRequestDTO.class))).thenReturn(UpsertResult.CREATED); - - mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) - .with(csrf()) - .content(OBJECT_MAPPER.writeValueAsString(dto)) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", TEST_URL + "/edits/" + dto.getId())); - - verify(editRequestService, times(1)).upsert(any(CreateEditRequestDTO.class)); - } - - @Test - @DisplayName("Should return 201 when successfully created reencode edit request") + @DisplayName("Should return 204 when successfully created reencode edit request") void upsertReencodeEditRequestCreated() throws Exception { - var dto = new CreateEditRequestDTO(); + CreateEditRequestDTO dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); dto.setForceReencode(true); dto.setStatus(EditRequestStatus.DRAFT); - when(editRequestService.upsert(any(CreateEditRequestDTO.class))).thenReturn(UpsertResult.CREATED); - mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) .with(csrf()) .content(OBJECT_MAPPER.writeValueAsString(dto)) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) + .andExpect(status().isNoContent()) .andExpect(header().string("Location", TEST_URL + "/edits/" + dto.getId())); verify(editRequestService, times(1)).upsert(any(CreateEditRequestDTO.class)); @@ -298,13 +274,7 @@ void upsertReencodeEditRequestCreated() throws Exception { @Test @DisplayName("Should pass send notifications false through to service") void upsertEditRequestWithNotificationsDisabled() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setSendNotifications(false); dto.setStatus(EditRequestStatus.DRAFT); @@ -319,7 +289,7 @@ void upsertEditRequestWithNotificationsDisabled() throws Exception { .content(OBJECT_MAPPER.writeValueAsString(dto)) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isCreated()) + .andExpect(status().isNoContent()) .andExpect(header().string("Location", TEST_URL + "/edits/" + dto.getId())); verify(editRequestService, times(1)).upsert(any(CreateEditRequestDTO.class)); @@ -328,7 +298,7 @@ void upsertEditRequestWithNotificationsDisabled() throws Exception { @Test @DisplayName("Should return 400 when cut instructions and force reencode are both provided") void upsertEditRequestWithCutsAndForceReencodeBadRequest() throws Exception { - var dto = new CreateEditRequestDTO(); + CreateEditRequestDTO dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); dto.setForceReencode(true); @@ -351,23 +321,15 @@ void upsertEditRequestWithCutsAndForceReencodeBadRequest() throws Exception { @Test @DisplayName("Should return 200 when successfully deleted edit request") void upsertEditRequestDeleted() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.DRAFT); - when(editRequestService.upsert(any(CreateEditRequestDTO.class))).thenReturn(UpsertResult.CREATED); - mockMvc.perform(delete(TEST_URL + "/edits/" + dto.getId()) .with(csrf()) .content(OBJECT_MAPPER.writeValueAsString(dto)) .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); verify(editRequestService, times(1)).delete(any(CreateEditRequestDTO.class)); } @@ -375,15 +337,9 @@ void upsertEditRequestDeleted() throws Exception { @Test @DisplayName("Delete should return 400 when path and payload IDs do not match") void deleteEditRequestPathPayloadMismatch() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.DRAFT); - var differentId = UUID.randomUUID(); + UUID differentId = UUID.randomUUID(); mockMvc.perform(delete(TEST_URL + "/edits/" + differentId) .with(csrf()) @@ -395,21 +351,12 @@ void deleteEditRequestPathPayloadMismatch() throws Exception { .value("Path editRequestId does not match payload property deleteEditRequestDTO.id")); } - @Test @DisplayName("Should return 204 when successfully updated edit request") void upsertEditRequestUpdated() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.DRAFT); - when(editRequestService.upsert(any(CreateEditRequestDTO.class))).thenReturn(UpsertResult.UPDATED); - mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) .with(csrf()) .content(OBJECT_MAPPER.writeValueAsString(dto)) @@ -424,15 +371,9 @@ void upsertEditRequestUpdated() throws Exception { @Test @DisplayName("Should return 400 when path and payload IDs do not match") void upsertEditRequestPathPayloadMismatch() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.DRAFT); - var differentId = UUID.randomUUID(); + UUID differentId = UUID.randomUUID(); mockMvc.perform(put(TEST_URL + "/edits/" + differentId) .with(csrf()) @@ -462,7 +403,7 @@ void validateStartOfCutIsNull() throws Exception { } """, anyIdWillDo, anyIdWillDo); - var result = mockMvc.perform(put(TEST_URL + "/edits/" + anyIdWillDo) + MvcResult result = mockMvc.perform(put(TEST_URL + "/edits/" + anyIdWillDo) .with(csrf()) .content(editRequestJson) .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -477,7 +418,7 @@ void validateStartOfCutIsNull() throws Exception { @Test @DisplayName("Should return 400 when sourceRecordingId is null") void validateSourceRecordingIdIsNull() throws Exception { - var dto = new CreateEditRequestDTO(); + CreateEditRequestDTO dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() .startOfCut("00:00:00") @@ -497,13 +438,7 @@ void validateSourceRecordingIdIsNull() throws Exception { @Test @DisplayName("Should return 400 when status is null") void validateStatusIsNull() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) .with(csrf()) @@ -517,13 +452,7 @@ void validateStatusIsNull() throws Exception { @Test @DisplayName("Should return 400 when status is REJECTED and rejectionReason is null") void validateRejectedStatusWithoutRejectionReason() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.REJECTED); mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) @@ -538,13 +467,7 @@ void validateRejectedStatusWithoutRejectionReason() throws Exception { @Test @DisplayName("Should return 400 when status is SUBMITTED and jointlyAgreed is null") void validateSubmittedStatusWithoutJointlyAgreed() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.SUBMITTED); mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) @@ -559,13 +482,7 @@ void validateSubmittedStatusWithoutJointlyAgreed() throws Exception { @Test @DisplayName("Should return 400 when status is APPROVED and approvedAt is null") void validateApprovedStatusWithoutApprovedAt() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.APPROVED); dto.setApprovedBy("Someone"); @@ -581,13 +498,7 @@ void validateApprovedStatusWithoutApprovedAt() throws Exception { @Test @DisplayName("Should return 400 when status is APPROVED and approvedBy is null") void validateApprovedStatusWithoutApprovedBy() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.APPROVED); dto.setApprovedAt(Timestamp.from(Instant.now())); @@ -600,4 +511,15 @@ void validateApprovedStatusWithoutApprovedBy() throws Exception { .andExpect(jsonPath("$.approvedBy").value("must have approved by when status is APPROVED")); } + private static @NotNull CreateEditRequestDTO createBogStandardEditRequest() { + CreateEditRequestDTO dto = new CreateEditRequestDTO(); + dto.setId(UUID.randomUUID()); + dto.setSourceRecordingId(UUID.randomUUID()); + dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:00") + .endOfCut("00:00:01") + .build())); + return dto; + } + } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java similarity index 51% rename from src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java rename to src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index 37321e79a9..4a70149c5a 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -1,11 +1,18 @@ -package uk.gov.hmcts.reform.preapi.email; +package uk.gov.hmcts.reform.preapi.email.govnotify; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import uk.gov.hmcts.reform.preapi.email.govnotify.GovNotify; +import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.email.EmailResponse; +import uk.gov.hmcts.reform.preapi.email.EmailServiceFactory; +import uk.gov.hmcts.reform.preapi.email.IEmailService; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CaseClosed; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CaseClosureCancelled; import uk.gov.hmcts.reform.preapi.email.govnotify.templates.CasePendingClosure; @@ -17,24 +24,30 @@ import uk.gov.hmcts.reform.preapi.entities.Case; import uk.gov.hmcts.reform.preapi.entities.Court; import uk.gov.hmcts.reform.preapi.entities.EditRequest; -import uk.gov.hmcts.reform.preapi.entities.Participant; import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.ParticipantType; import uk.gov.hmcts.reform.preapi.exception.EmailFailedToSendException; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; +import uk.gov.hmcts.reform.preapi.utils.JsonUtils; import uk.gov.service.notify.NotificationClient; import uk.gov.service.notify.NotificationClientException; import uk.gov.service.notify.SendEmailResponse; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; -import java.util.Set; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @SpringBootTest(classes = GovNotify.class) @@ -64,6 +77,44 @@ public class GovNotifyTest { } }"""; + + private GovNotify underTest; + + private EditRequest editRequest; + + private static final User sampleUser = getSampleUser(); + private static final Case sampleCase = getSampleCase(); + + @BeforeEach + void setUp() throws NotificationClientException { + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) + .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); + + underTest = new GovNotify("http://localhost:8080", mockGovNotifyClient); + + Booking booking = mock(Booking.class); + when(booking.getCaseId()).thenReturn(sampleCase); + when(booking.getWitnessName()).thenReturn("Witness Name"); + when(booking.getDefendantName()).thenReturn("Defendant Name"); + + CaptureSession captureSession = mock(CaptureSession.class); + when(captureSession.getBooking()).thenReturn(booking); + + Recording recording = mock(Recording.class); + when(recording.getCaptureSession()).thenReturn(captureSession); + + CreateEditRequestDTO dto = new CreateEditRequestDTO(); + dto.setId(UUID.randomUUID()); + dto.setJointlyAgreed(true); + dto.setStatus(EditRequestStatus.REJECTED); + + EditCutInstructionDTO instruction1 = new EditCutInstructionDTO(0, 30, "first reason"); + EditInstructions editInstructions = new EditInstructions(List.of(instruction1), new ArrayList<>()); + + editRequest = new EditRequest(); + editRequest.updateEditRequestFromDto(dto, recording, JsonUtils.toJson(editInstructions)); + } + @DisplayName("Should create CaseClosed template") @Test void shouldCreateCaseClosedTemplate() { @@ -95,11 +146,13 @@ void shouldCreateRecordingReadyTemplate() { @DisplayName("Should create CasePendingClosure template") @Test void shouldCreateCasePendingClosureTemplate() { - var template = new CasePendingClosure("to", - "firstName", - "lastName", - "caseRef", - Timestamp.valueOf("2025-01-01 00:00:00.0")); + var template = new CasePendingClosure( + "to", + "firstName", + "lastName", + "caseRef", + Timestamp.valueOf("2025-01-01 00:00:00.0") + ); assertThat(template.getTemplateId()).isEqualTo("5322ba5c-f4c4-4d1b-807c-16f56f0d8d0c"); } @@ -107,14 +160,14 @@ void shouldCreateCasePendingClosureTemplate() { @Test void shouldCreatePortalInviteTemplate() { var template = new PortalInvite( - "to", - "firstName", - "lastName", - "portalUrl", - "guideLink", - "processGuideLink", - "faqsLink", - "editingRequestForm" + "to", + "firstName", + "lastName", + "portalUrl", + "guideLink", + "processGuideLink", + "faqsLink", + "editingRequestForm" ); assertThat(template.getTemplateId()).isEqualTo("e04adfb8-58e0-44be-ab42-bd6d896ccfb7"); } @@ -133,73 +186,28 @@ void shouldCreateEmailResponseFromGovNotifyResponse() { @Test void shouldCreateEmailServiceFactory() { var govNotify = new GovNotify("GovNotify", mockGovNotifyClient); - var emailServiceFactory = new EmailServiceFactory("GovNotify", true, List.of(govNotify)); + List emailServiceList = List.of(govNotify); + var emailServiceFactory = new EmailServiceFactory("GovNotify", true, + emailServiceList); assertThat(emailServiceFactory).isNotNull(); assertThat(emailServiceFactory.getEnabledEmailService()).isEqualTo(govNotify); assertThat(emailServiceFactory.isEnabled()).isTrue(); assertThat(emailServiceFactory.getEnabledEmailService("GovNotify")).isEqualTo(govNotify); - assertThrows(IllegalArgumentException.class, () -> emailServiceFactory.getEnabledEmailService("nonexistent")); + assertThrows(IllegalArgumentException.class, + () -> emailServiceFactory.getEnabledEmailService("nonexistent")); - assertThrows(IllegalArgumentException.class, () -> - new EmailServiceFactory("nonexistent", true, List.of(govNotify)) + assertThrows( + IllegalArgumentException.class, () -> + new EmailServiceFactory("nonexistent", true, emailServiceList) ); } - private User getUser() { - var user = new User(); - user.setFirstName("John"); - user.setLastName("Doe"); - user.setEmail("johndoe@example.com"); - return user; - } - - private Case getCase() { - var forCase = new Case(); - forCase.setReference("123456"); - var court = new Court(); - court.setName("Court Name"); - court.setGroupEmail("group-email@example.com"); - forCase.setCourt(court); - return forCase; - } - - private Participant getParticipant(ParticipantType t) { - var participant = new Participant(); - participant.setFirstName("John"); - participant.setLastName("Doe"); - participant.setParticipantType(t); - return participant; - } - - private EditRequest getEditRequest() { - var aCase = getCase(); - var booking = new Booking(); - booking.setCaseId(aCase); - booking.setParticipants(Set.of( - getParticipant(ParticipantType.WITNESS), - getParticipant(ParticipantType.DEFENDANT))); - var captureSession = new CaptureSession(); - captureSession.setBooking(booking); - var recording = new Recording(); - recording.setCaptureSession(captureSession); - var request = new EditRequest(); - request.setSourceRecording(recording); - request.setEditInstruction( - "{\"requestedInstructions\":" - + "[{\"start_of_cut\":\"00:00:00\",\"end_of_cut\":\"00:00:30\",\"reason\":\"\",\"start\":0,\"end\":0}]," - + "\"ffmpegInstructions\":[]}"); - return request; - } @DisplayName(("Should send recording ready email")) @Test - void shouldSendRecordingReadyEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.recordingReady(getUser(), getCase()); + void shouldSendRecordingReadyEmail() { + var response = underTest.recordingReady(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -208,12 +216,8 @@ void shouldSendRecordingReadyEmail() throws NotificationClientException { @DisplayName(("Should send recording edited email")) @Test - void shouldSendRecordingEditedEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.recordingEdited(getUser(), getCase()); + void shouldSendRecordingEditedEmail() { + var response = underTest.recordingEdited(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -222,12 +226,8 @@ void shouldSendRecordingEditedEmail() throws NotificationClientException { @DisplayName(("Should send portal invite email")) @Test - void shouldSendPortalInviteEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.portalInvite(getUser()); + void shouldSendPortalInviteEmail() { + var response = underTest.portalInvite(sampleUser); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -236,12 +236,11 @@ void shouldSendPortalInviteEmail() throws NotificationClientException { @DisplayName(("Should send case pending closure email")) @Test - void shouldSendCasePendingClosureEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.casePendingClosure(getUser(), getCase(), Timestamp.valueOf("2025-01-01 00:00:00.0")); + void shouldSendCasePendingClosureEmail() { + var response = underTest.casePendingClosure( + sampleUser, sampleCase, + Timestamp.valueOf("2025-01-01 00:00:00.0") + ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -250,12 +249,8 @@ void shouldSendCasePendingClosureEmail() throws NotificationClientException { @DisplayName(("Should send case closed email")) @Test - void shouldSendCaseClosedEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.caseClosed(getUser(), getCase()); + void shouldSendCaseClosedEmail() { + var response = underTest.caseClosed(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -264,163 +259,165 @@ void shouldSendCaseClosedEmail() throws NotificationClientException { @DisplayName(("Should send case closure cancelled email")) @Test - void shouldSendCaseClosureCancelledEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var response = govNotify.caseClosureCancelled(getUser(), getCase()); + void shouldSendCaseClosureCancelledEmail() { + var response = underTest.caseClosureCancelled(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); } + @Captor + private ArgumentCaptor> variablesCaptor; + @Test - @DisplayName("Should send editing rejection email") + @DisplayName("Should send editing email") void sendEditRejectionEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var request = getEditRequest(); - request.setStatus(EditRequestStatus.REJECTED); - request.setRejectionReason("REJECTION REASON"); - request.setJointlyAgreed(true); - - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingRejected("group-email@example.com", request); + EmailResponse response = underTest.sendEmailAboutEditingRequest(editRequest) + .orElseThrow(() -> new EmailFailedToSendException("Something went wrong")); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); - } - @Test - @DisplayName("Should send jointly agreed submission email") - void sendJointlyAgreedEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); + ArgumentCaptor emailAddressCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor templateCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(String.class); - var request = getEditRequest(); - request.setStatus(EditRequestStatus.SUBMITTED); - request.setJointlyAgreed(true); - - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingJointlyAgreed("group-email@example.com", request); + verify(mockGovNotifyClient, times(1)).sendEmail( + templateCaptor.capture(), + emailAddressCaptor.capture(), + variablesCaptor.capture(), + referenceCaptor.capture() + ); - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); + // Gov Notify template ID + assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); + assertThat(emailAddressCaptor.getValue()).isEqualTo(sampleCase.getCourt().getGroupEmail()); + Map variables = variablesCaptor.getValue(); + assertThat(variables).containsAllEntriesOf( + Map.of("case_reference", sampleCase.getReference(), + "court_name", sampleCase.getCourt().getName(), + "defendant_names", "Defendant Name", + "edit_count", 1, + "jointly_agreed", "Yes", + "rejection_reason", "", + "portal_link", "http://localhost:8080")); } + @DisplayName(("Should fail to send recording ready email")) @Test - @DisplayName("Should send not jointly agreed submission email") - void sendNotJointlyAgreedEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); + void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { - var request = getEditRequest(); - request.setStatus(EditRequestStatus.SUBMITTED); - request.setJointlyAgreed(false); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) + .thenThrow(mock(NotificationClientException.class)); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingNotJointlyAgreed("group-email@example.com", request); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.recordingReady(sampleUser, sampleCase) + ).getMessage(); - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } - @DisplayName(("Should fail to send recording ready email")) + @DisplayName(("Should fail to send recording edited email")) @Test - void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.recordingReady(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.recordingEdited(sampleUser, sampleCase) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } - @DisplayName(("Should fail to send recording edited email")) + @DisplayName(("Should absorb error thrown by email parameters creation")) @Test - void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + void shouldAbsorbErrorThrownByEmailParameters() { + editRequest.setSourceRecording(null); // will cause exception - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.recordingEdited(getUser(), getCase())).getMessage(); + Optional emailResponse = underTest.sendEmailAboutEditingRequest(editRequest); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(emailResponse).isEmpty(); + verifyNoInteractions(mockGovNotifyClient); } @DisplayName(("Should fail to send portal invite email")) @Test void shouldFailToSendPortalInviteEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.portalInvite(getUser())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.portalInvite(sampleUser) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case pending closure email")) @Test void shouldFailToSendCasePendingClosureEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.casePendingClosure(getUser(), - getCase(), - Timestamp.valueOf("2025-01-01 00:00:00.0"))) - .getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.casePendingClosure( + sampleUser, + sampleCase, + Timestamp.valueOf("2025-01-01 00:00:00.0") + ) + ) + .getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case closed email")) @Test void shouldFailToSendCaseClosedEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.caseClosed(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.caseClosed(sampleUser, sampleCase) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case closure cancelled email")) @Test void shouldFailToSendCaseClosureCancelledEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.caseClosureCancelled(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.caseClosureCancelled(sampleUser, sampleCase) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should send email verification email")) @Test - void shouldSendEmailVerificationEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var user = getUser(); - var response = govNotify.emailVerification(user.getEmail(), user.getFirstName(), user.getLastName(), "123456"); + void shouldSendEmailVerificationEmail() { + var response = underTest.emailVerification( + sampleUser.getEmail(), sampleUser.getFirstName(), + sampleUser.getLastName(), "123456" + ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -430,82 +427,35 @@ void shouldSendEmailVerificationEmail() throws NotificationClientException { @DisplayName(("Should fail to send email verification email")) @Test void shouldFailToSendEmailVerificationEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); - var user = getUser(); var message = assertThrows( - EmailFailedToSendException.class, - () -> govNotify.emailVerification( - user.getEmail(), - user.getFirstName(), - user.getLastName(), - "123456" - ) + EmailFailedToSendException.class, + () -> underTest.emailVerification( + sampleUser.getEmail(), + sampleUser.getFirstName(), + sampleUser.getLastName(), + "123456" + ) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @Test - @DisplayName("Should fail to send editing rejection email") - void shouldFailToSendEditingRejectionEmail() throws NotificationClientException { + @DisplayName("Should fail to send editing email") + void shouldFailToSendEditingEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); - - var request = getEditRequest(); - request.setStatus(EditRequestStatus.REJECTED); - request.setRejectionReason("REJECTION REASON"); - request.setJointlyAgreed(true); - - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.editingRejected(getCase().getCourt().getGroupEmail(), request)) - .getMessage(); - - assertThat(message).isEqualTo("Failed to send email to: " + getCase().getCourt().getGroupEmail()); - } - - @Test - @DisplayName("Should fail to send jointly agreed email") - void shouldFailToSendEditingJointlyAgreedEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); - - var request = getEditRequest(); - request.setStatus(EditRequestStatus.SUBMITTED); - request.setJointlyAgreed(true); + .thenThrow(mock(NotificationClientException.class)); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.editingJointlyAgreed(getCase().getCourt().getGroupEmail(), request)) - .getMessage(); - - assertThat(message).isEqualTo("Failed to send email to: " + getCase().getCourt().getGroupEmail()); - } - - @Test - @DisplayName("Should fail to send not jointly agreed email") - void shouldFailToSendEditingNotJointlyAgreedEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); - - var request = getEditRequest(); - request.setStatus(EditRequestStatus.SUBMITTED); - request.setJointlyAgreed(false); - - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); var message = assertThrows( - EmailFailedToSendException.class, - () -> govNotify.editingNotJointlyAgreed( - getCase().getCourt().getGroupEmail(), - request - ) - ) - .getMessage(); + EmailFailedToSendException.class, + () -> underTest.sendEmailAboutEditingRequest(editRequest) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getCase().getCourt().getGroupEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + + sampleCase.getCourt().getGroupEmail()); } @DisplayName("Should prefer alternative email when it ends with .cjsm.net") @@ -517,8 +467,8 @@ void shouldPreferAlternativeEmailWithCjsmNet() throws Exception { var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example.com.cjsm.net"); } @@ -532,8 +482,8 @@ void shouldPreferPrimaryEmailWithCjsmNet() throws Exception { var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example.com.cjsm.net"); } @@ -547,8 +497,8 @@ void shouldPreferAlternativeEmailWhenBothHaveCjsmNet() throws Exception { var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example2.com.cjsm.net"); } @@ -562,8 +512,8 @@ void shouldFallbackToPrimaryEmailWhenNoCjsmNet() throws Exception { var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example.com"); } @@ -577,8 +527,8 @@ void shouldFallbackToPrimaryEmailWhenAlternativeIsNull() throws Exception { var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example.com"); } @@ -592,9 +542,28 @@ void shouldPreferPrimaryEmailWhenAlternativeIsNullAndPrimaryHasCjsmNet() throws var method = GovNotify.class.getDeclaredMethod("getUsersPreferredEmail", User.class); method.setAccessible(true); - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var result = (String) method.invoke(govNotify, user); + + var result = (String) method.invoke(underTest, user); assertThat(result).isEqualTo("user@example.com.cjsm.net"); } + + private static User getSampleUser() { + var user = new User(); + user.setFirstName("John"); + user.setLastName("Doe"); + user.setEmail("johndoe@example.com"); + return user; + } + + private static Case getSampleCase() { + var forCase = new Case(); + forCase.setReference("123456"); + var court = new Court(); + court.setName("Court Name"); + court.setGroupEmail("group-email@example.com"); + forCase.setCourt(court); + return forCase; + } + } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParametersTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParametersTest.java new file mode 100644 index 0000000000..1b588e334e --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParametersTest.java @@ -0,0 +1,264 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.entities.Booking; +import uk.gov.hmcts.reform.preapi.entities.CaptureSession; +import uk.gov.hmcts.reform.preapi.entities.Case; +import uk.gov.hmcts.reform.preapi.entities.Court; +import uk.gov.hmcts.reform.preapi.entities.EditRequest; +import uk.gov.hmcts.reform.preapi.entities.Recording; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.exception.BadRequestException; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; +import uk.gov.hmcts.reform.preapi.utils.JsonUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class EditEmailParametersTest { + + private Recording mockRecording; + private CaptureSession mockCaptureSession; + private Booking mockBooking; + private Court mockCourt; + private Case mockCase; + + private final UUID editRequestId = UUID.randomUUID(); + private EditRequest editRequest; + private List instructionsList; + private CreateEditRequestDTO dto; + + private final String portalUrl = "http://localhost:8080"; + + @BeforeEach + void setUp() { + mockCourt = mock(Court.class); + when(mockCourt.getName()).thenReturn("Court Name"); + when(mockCourt.getGroupEmail()).thenReturn("Group Email"); + + mockCase = mock(Case.class); + when(mockCase.getCourt()).thenReturn(mockCourt); + when(mockCase.getReference()).thenReturn("Test Case"); + + mockBooking = mock(Booking.class); + when(mockBooking.getCaseId()).thenReturn(mockCase); + when(mockBooking.getWitnessName()).thenReturn("Witness Name"); + when(mockBooking.getDefendantName()).thenReturn("Defendant Name"); + + mockCaptureSession = mock(CaptureSession.class); + when(mockCaptureSession.getBooking()).thenReturn(mockBooking); + + mockRecording = mock(Recording.class); + when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); + + EditCutInstructionDTO instruction1 = new EditCutInstructionDTO(5, 10, "first reason"); + EditCutInstructionDTO instruction2 = new EditCutInstructionDTO(23, 26, "second reason"); + EditCutInstructionDTO instruction3 = new EditCutInstructionDTO(31, 32, "third reason"); + + instructionsList = Arrays.asList(instruction1, instruction2, instruction3); + + dto = new CreateEditRequestDTO(); + dto.setId(editRequestId); + dto.setJointlyAgreed(true); + dto.setStatus(EditRequestStatus.APPROVED); + dto.setRejectionReason("Rejection Reason"); + + EditInstructions defaultEditInstructions = new EditInstructions(instructionsList, new ArrayList<>()); + String editInstructionsAsJson = JsonUtils.toJson(defaultEditInstructions); + + editRequest = new EditRequest(); + editRequest.updateEditRequestFromDto(dto, mockRecording, editInstructionsAsJson); + } + + @Test + @DisplayName("Cannot create email parameters from null edit request") + void cannotCreateEmailParametersFromNullEditRequest() { + assertThrows(NotFoundException.class, () -> new EditEmailParameters(null, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with null recording") + void cannotCreateEmailParametersFromNullRecording() { + editRequest.setSourceRecording(null); + + assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with null capture session") + void cannotCreateEmailParametersFromNullCaptureSession() { + when(mockRecording.getCaptureSession()).thenReturn(null); + assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with null booking") + void cannotCreateEmailParametersFromNullBooking() { + when(mockCaptureSession.getBooking()).thenReturn(null); + assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with null case") + void cannotCreateEmailParametersFromNullCase() { + when(mockBooking.getCaseId()).thenReturn(null); + assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with null court") + void cannotCreateEmailParametersFromNullCourt() { + when(mockCase.getCourt()).thenReturn(null); + assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with blank court email") + void cannotCreateEmailParametersFromNullCourtEmail() { + when(mockCourt.getGroupEmail()).thenReturn(" "); + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"{}", "this-is-not-json"}) + @DisplayName("Cannot create email parameters with blank edit instructions") + void cannotCreateEmailParametersFromNullEditInstructions(String editInstructions) { + editRequest.updateEditRequestFromDto(dto, mockRecording, editInstructions); + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with empty request edit instructions") + void cannotCreateEmailParametersFromEmptyRequestEditInstructions() { + editRequest.updateEditRequestFromDto(dto, mockRecording, "{\"requestedInstructions\": []}"); + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters if instructions say not to notify") + void cannotCreateEmailParametersIfInstructionsSayNotToNotify() { + EditInstructions instructions = new EditInstructions( + instructionsList, new ArrayList<>(), + false, false + ); + editRequest.setEditInstruction(JsonUtils.toJson(instructions)); + + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters if portal url is not set") + void cannotCreateEmailParametersIfPortalUrlIsNotSet() { + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, null)); + } + + @Test + @DisplayName("Should accurately create edit email parameters for REJECTED edit") + void shouldAccuratelyCreateEmailParameters() { + editRequest.setStatus(EditRequestStatus.REJECTED); + editRequest.setRejectionReason("Rejected Reason"); + editRequest.setJointlyAgreed(false); + + EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + + assertThat(editEmailParameters.getJointlyAgreed()).isFalse(); + + String expectedSummary = """ + Edit 1:\s + Start time: 00:00:05 + End time: 00:00:10 + Time Removed: 00:00:05 + Reason: first reason + + Edit 2:\s + Start time: 00:00:23 + End time: 00:00:26 + Time Removed: 00:00:03 + Reason: second reason + + Edit 3:\s + Start time: 00:00:31 + End time: 00:00:32 + Time Removed: 00:00:01 + Reason: third reason + + """; + Map paramsMap = editEmailParameters.getEmailParameters(); + + assertThat(paramsMap).containsAllEntriesOf(Map.of( + "edit_summary", expectedSummary, + "rejection_reason", editRequest.getRejectionReason(), + "jointly_agreed", "No", + "case_reference", mockCase.getReference(), + "court_name", mockCourt.getName(), + "witness_name", "Witness Name", + "defendant_names", "Defendant Name", + "edit_count", 3, + "portal_link", "http://localhost:8080" + )); + } + + @Test + @DisplayName("Should accurately create edit email parameters for JOINTLY_AGREED edit") + void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { + editRequest.setStatus(EditRequestStatus.SUBMITTED); + editRequest.setRejectionReason(null); + editRequest.setJointlyAgreed(true); + + EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + + assertThat(editEmailParameters.getJointlyAgreed()).isTrue(); + + + String expectedSummary = """ + Edit 1:\s + Start time: 00:00:05 + End time: 00:00:10 + Time Removed: 00:00:05 + Reason: first reason + + Edit 2:\s + Start time: 00:00:23 + End time: 00:00:26 + Time Removed: 00:00:03 + Reason: second reason + + Edit 3:\s + Start time: 00:00:31 + End time: 00:00:32 + Time Removed: 00:00:01 + Reason: third reason + + """; + Map paramsMap = editEmailParameters.getEmailParameters(); + assertThat(paramsMap).containsAllEntriesOf(Map.of( + "rejection_reason", "", + "jointly_agreed", "Yes", + "case_reference", mockCase.getReference(), + "court_name", mockCourt.getName(), + "witness_name", "Witness Name", + "defendant_names", "Defendant Name", + "edit_count", 3, + "portal_link", "http://localhost:8080", + "edit_summary", expectedSummary + )); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplateTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplateTest.java new file mode 100644 index 0000000000..e083d53140 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplateTest.java @@ -0,0 +1,87 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class EditRequestEmailTemplateTest { + + @Test + @DisplayName("Should create REJECTED edit email template") + void getRejectionEmailTemplate() { + EditEmailParameters parameters = mock(EditEmailParameters.class); + when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.REJECTED); + when(parameters.getJointlyAgreed()).thenReturn(true); + when(parameters.getToEmailAddress()).thenReturn("test@email.com"); + + Map testVariables = ImmutableMap.of("jointly_agreed", "Yes"); + when(parameters.getEmailParameters()).thenReturn(testVariables); + + EditRequestEmailTemplate template = new EditRequestEmailTemplate(parameters); + + assertThat(template.getTemplateId()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); + assertThat(template.getEditingEmailType()).isEqualTo(EditingEmailType.REJECTED); + assertThat(template.getTo()).isEqualTo("test@email.com"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "Yes"); + } + + @Test + @DisplayName("Should create JOINTLY_AGREED edit email template") + void getJointlyAgreedEmailTemplate() { + EditEmailParameters parameters = mock(EditEmailParameters.class); + when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(parameters.getJointlyAgreed()).thenReturn(true); + when(parameters.getEmailParameters()).thenReturn(ImmutableMap.of("jointly_agreed", "Yes")); + when(parameters.getToEmailAddress()).thenReturn("test@email.com"); + + EditRequestEmailTemplate template = new EditRequestEmailTemplate(parameters); + + assertThat(template.getTemplateId()).isEqualTo("018ad5d2-c7ba-42a8-ad50-6baaaecf210c"); + assertThat(template.getEditingEmailType()).isEqualTo(EditingEmailType.JOINTLY_AGREED); + assertThat(template.getTo()).isEqualTo("test@email.com"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "Yes"); + } + + @Test + @DisplayName("Should create NOT_JOINTLY_AGREED edit email template") + void getNotJointlyAgreedEmailTemplate() { + EditEmailParameters parameters = mock(EditEmailParameters.class); + when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(parameters.getJointlyAgreed()).thenReturn(false); + when(parameters.getEmailParameters()).thenReturn(ImmutableMap.of("jointly_agreed", "No")); + when(parameters.getToEmailAddress()).thenReturn("test@email.com"); + + EditRequestEmailTemplate template = new EditRequestEmailTemplate(parameters); + + assertThat(template.getTemplateId()).isEqualTo("fb11d2a9-086d-4f27-9208-a3ddfe696919"); + assertThat(template.getEditingEmailType()).isEqualTo(EditingEmailType.NOT_JOINTLY_AGREED); + assertThat(template.getTo()).isEqualTo("test@email.com"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "No"); + } + + @Test + @DisplayName("Should throw exception if unrecognised email type") + void throwExceptionIfUnrecognisedEmailType() { + EditEmailParameters parameters = mock(EditEmailParameters.class); + when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.COMPLETE); + + assertThrows(IllegalArgumentException.class, () -> new EditRequestEmailTemplate(parameters)); + + var message = assertThrows( + IllegalArgumentException.class, + () -> new EditRequestEmailTemplate(parameters) + ).getMessage(); + + assertThat(message).isEqualTo("Could not work out which type of edit email to send: edit status " + + "COMPLETE, jointly agreed false"); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java index 3c407df21d..d33c038d8c 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java @@ -1,8 +1,10 @@ package uk.gov.hmcts.reform.preapi.services; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -15,21 +17,27 @@ import uk.gov.hmcts.reform.preapi.entities.Case; import uk.gov.hmcts.reform.preapi.entities.Court; import uk.gov.hmcts.reform.preapi.entities.EditRequest; +import uk.gov.hmcts.reform.preapi.entities.Participant; import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.ShareBooking; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.CourtType; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.enums.ParticipantType; import uk.gov.hmcts.reform.preapi.util.HelperFactory; import java.sql.Timestamp; import java.util.Set; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static uk.gov.hmcts.reform.preapi.services.EditNotificationService.isNotifiable; @SpringBootTest(classes = EditNotificationService.class) class EditNotificationServiceTest { @@ -64,6 +72,8 @@ class EditNotificationServiceTest { private Case testCase; private Court court; private Booking booking; + private Participant witness; + private Participant defendant; private ShareBooking shareBooking1; private ShareBooking shareBooking2; @@ -71,32 +81,52 @@ class EditNotificationServiceTest { @BeforeEach void setup() { - shareWith1 = HelperFactory.createUser("First", "User", "example1@example.com", - new Timestamp(System.currentTimeMillis()), null, null); + shareWith1 = HelperFactory.createUser( + "First", "User", "example1@example.com", + new Timestamp(System.currentTimeMillis()), null, null + ); - shareWith2 = HelperFactory.createUser("Second", "User", "example2@example.com", - new Timestamp(System.currentTimeMillis()), null, null); + shareWith2 = HelperFactory.createUser( + "Second", "User", "example2@example.com", + new Timestamp(System.currentTimeMillis()), null, null + ); - sharedBy = HelperFactory.createUser("Court", "Clerk", "court.clerk@example.com", - new Timestamp(System.currentTimeMillis()), null, null); + sharedBy = HelperFactory.createUser( + "Court", "Clerk", "court.clerk@example.com", + new Timestamp(System.currentTimeMillis()), null, null + ); court = HelperFactory.createCourt(CourtType.CROWN, "Test Court", "TC"); + court.setGroupEmail(testEmail); testCase = HelperFactory.createCase(court, "Test Case", false, null); booking = HelperFactory.createBooking(testCase, new Timestamp(System.currentTimeMillis()), null); - shareBooking1 = HelperFactory.createShareBooking(shareWith1, sharedBy, booking, - new Timestamp(System.currentTimeMillis())); + witness = new Participant(); + witness.setFirstName("Witness"); + witness.setParticipantType(ParticipantType.WITNESS); + defendant = new Participant(); + defendant.setFirstName("Defendant lastname"); + defendant.setParticipantType(ParticipantType.DEFENDANT); + booking.setParticipants(Set.of(witness, defendant)); + + shareBooking1 = HelperFactory.createShareBooking( + shareWith1, sharedBy, booking, + new Timestamp(System.currentTimeMillis()) + ); - shareBooking2 = HelperFactory.createShareBooking(shareWith2, sharedBy, booking, - new Timestamp(System.currentTimeMillis())); + shareBooking2 = HelperFactory.createShareBooking( + shareWith2, sharedBy, booking, + new Timestamp(System.currentTimeMillis()) + ); booking.setShares(Set.of(shareBooking1, shareBooking2)); when(emailServiceFactory.getEnabledEmailService()).thenReturn(emailService); when(emailService.recordingEdited(any(), any())).thenReturn(emailResponse); when(mockEditRequest.getSourceRecording()).thenReturn(mockRecording); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); when(mockCaptureSession.getBooking()).thenReturn(booking); } @@ -114,45 +144,104 @@ void testSendNotifications() { @DisplayName("Should be able to notify appropriately when edit request is submitted jointly agreed") @Test void testEditRequestSubmittedJointlyAgreed() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(mockEditRequest.getJointlyAgreed()).thenReturn(true); - court.setGroupEmail(testEmail); - underTest.onEditRequestSubmitted(mockEditRequest); - verify(emailService, times(1)).editingJointlyAgreed(testEmail, mockEditRequest); + underTest.editRequestStatusWasUpdated(mockEditRequest); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(EditRequest.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(paramsCaptor.capture()); verifyNoMoreInteractions(emailService); + + assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); } @DisplayName("Should be able to notify appropriately when edit request is submitted not jointly agreed") @Test void testEditRequestSubmittedNotJointlyAgreed() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(mockEditRequest.getJointlyAgreed()).thenReturn(false); - court.setGroupEmail(testEmail); - underTest.onEditRequestSubmitted(mockEditRequest); + underTest.editRequestStatusWasUpdated(mockEditRequest); - verify(emailService, times(1)).editingNotJointlyAgreed(testEmail, mockEditRequest); + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(EditRequest.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(paramsCaptor.capture()); verifyNoMoreInteractions(emailService); + + assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); } @DisplayName("Should be able to notify appropriately when edit request is rejected") @Test void testEditRequestRejected() { - court.setGroupEmail(testEmail); - underTest.onEditRequestRejected(mockEditRequest); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.REJECTED); + + underTest.editRequestStatusWasUpdated(mockEditRequest); - verify(emailService, times(1)).editingRejected(testEmail, mockEditRequest); + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(EditRequest.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(paramsCaptor.capture()); verifyNoMoreInteractions(emailService); + assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); } - @DisplayName("Should not attempt to email if court email address is null") + @DisplayName("Should not notify for null edit request") @Test - void testEditRequestNotificationsWhenNoCourtEmail() { - court.setGroupEmail(null); + void testNullEditRequestNotNotified() { + assertDoesNotThrow(() -> underTest.editRequestStatusWasUpdated(null)); + verifyNoInteractions(emailService); + } - underTest.onEditRequestSubmitted(mockEditRequest); + @DisplayName("Should not notify when edit request is approved, completed, or non-submission status") + @Test + void testNoEditRequestNotificationForOtherStatusTypes() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.COMPLETE); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.APPROVED); + underTest.editRequestStatusWasUpdated(mockEditRequest); verifyNoInteractions(emailService); - underTest.onEditRequestRejected(mockEditRequest); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + underTest.editRequestStatusWasUpdated(mockEditRequest); verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PROCESSING); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.ERROR); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + } + + @DisplayName("Should pass on attempt to email as null court email address is now handled downstream") + @Test + void testEditRequestNotificationsWhenNoCourtEmail() { + court.setGroupEmail(null); + + underTest.editRequestStatusWasUpdated(mockEditRequest); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(EditRequest.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(paramsCaptor.capture()); + verifyNoMoreInteractions(emailService); + assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); + } + + @Test + void testIfEmailShouldBeSentOnStatusChange() { + Assertions.assertTrue(isNotifiable(EditRequestStatus.REJECTED)); + Assertions.assertTrue(isNotifiable(EditRequestStatus.SUBMITTED)); + + Assertions.assertFalse(isNotifiable(EditRequestStatus.DRAFT)); + Assertions.assertFalse(isNotifiable(EditRequestStatus.APPROVED)); + Assertions.assertFalse(isNotifiable(EditRequestStatus.PENDING)); + Assertions.assertFalse(isNotifiable(EditRequestStatus.PROCESSING)); + Assertions.assertFalse(isNotifiable(EditRequestStatus.COMPLETE)); + Assertions.assertFalse(isNotifiable(EditRequestStatus.ERROR)); } } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java index 216ebe4e7c..6e91e92a25 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Pageable; -import org.springframework.data.util.Pair; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -27,7 +26,6 @@ import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; @@ -67,9 +65,6 @@ class EditRequestServiceTest { @MockitoBean private RecordingService recordingService; - @MockitoBean - private EditNotificationService editNotificationService; - @MockitoBean private Recording mockRecording; @@ -175,7 +170,7 @@ void setup() throws InterruptedException { when(mockEditRequest.getEditInstruction()).thenReturn("{}"); when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.CREATED, mockEditRequest)); + .thenReturn(mockEditRequest); when(editRequestCrudService.findById(mockEditRequestId, false)).thenReturn(editRequestDTO); when(editRequestDTO.getId()).thenReturn(mockEditRequestId); when(editRequestDTO.getStatus()).thenReturn(EditRequestStatus.PENDING); @@ -207,10 +202,9 @@ void createEditRequestSuccess() { when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecording.getId())) .thenReturn(Optional.of(mockRecording)); when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.CREATED, mockEditRequest)); + .thenReturn(mockEditRequest); - UpsertResult response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.CREATED); + underTest.upsert(dto); verify(recordingService, times(1)).syncRecordingMetadataWithStorage(mockRecording.getId()); verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecording.getId()); @@ -338,8 +332,6 @@ void upsertEditInstructionsWithCSVFile() { EditRequest returnedByDb = new EditRequest(); returnedByDb.setCreatedBy(courtClerkUser); - when(editRequestCrudService.upsert(any(), any(), any())) - .thenReturn(Pair.of(UpsertResult.CREATED, mockEditRequest)); when(editRequestCrudService.findByIdIfExists(any())).thenReturn(Optional.of(returnedByDb)); when(editRequestCrudService.findById(any(UUID.class), anyBoolean())).thenReturn(editRequestDTO); @@ -393,62 +385,7 @@ void upsertEditInstructionsWithEmptyFile() { ); } - @Test - @DisplayName("Should trigger request submission jointly agreed email on submission") - void upsertOnSubmittedJointlyAgreed() { - when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - when(dto.getJointlyAgreed()).thenReturn(true); - - when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); - - when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.UPDATED, mockEditRequest)); - - UpsertResult response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(mockAuth, times(1)).getAppAccess(); - verify(editRequestCrudService, times(1)).upsert(dto, mockRecording, courtClerkUser); - verify(editNotificationService, times(1)).onEditRequestSubmitted(mockEditRequest); - } - - @Test - @DisplayName("Should trigger request submission not jointly agreed email on submission") - void upsertOnSubmittedNotJointlyAgreed() { - when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - when(dto.getJointlyAgreed()).thenReturn(false); - when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.UPDATED, mockEditRequest)); - - UpsertResult response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(mockAuth, times(1)).getAppAccess(); - verify(editRequestCrudService, times(1)).upsert(dto, mockRecording, courtClerkUser); - verify(editNotificationService, times(1)).onEditRequestSubmitted(mockEditRequest); - } - - @Test - @DisplayName("Should trigger request rejection email on edit request rejection") - void upsertOnRejected() { - when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.UPDATED, mockEditRequest)); - - when(dto.getStatus()).thenReturn(EditRequestStatus.REJECTED); - when(dto.getJointlyAgreed()).thenReturn(false); - - when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - - UpsertResult response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(mockAuth, times(1)).getAppAccess(); - verify(editRequestCrudService, times(1)).upsert(dto, mockRecording, courtClerkUser); - verify(editNotificationService, times(1)).onEditRequestRejected(mockEditRequest); - } @DisplayName("Should throw an exception if edit instructions have unsafe data") @Test diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudServiceTest.java index 1803b443df..ad0a986fc7 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudServiceTest.java @@ -3,9 +3,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.util.Pair; import org.springframework.test.context.bean.override.mockito.MockitoBean; import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; @@ -20,10 +20,11 @@ import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.services.EditNotificationService; import uk.gov.hmcts.reform.preapi.util.HelperFactory; import java.sql.Timestamp; @@ -41,6 +42,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static uk.gov.hmcts.reform.preapi.utils.JsonUtils.toJson; @@ -51,6 +53,9 @@ class EditRequestCrudServiceTest { @MockitoBean private EditRequestRepository editRequestRepository; + @MockitoBean + private EditNotificationService editNotificationService; + @MockitoBean private IEditingService editingService; @@ -72,6 +77,9 @@ class EditRequestCrudServiceTest { @MockitoBean private EditRequest mockEditRequest; + @MockitoBean + private EditRequest newlyUpdatedEditRequest; + @MockitoBean private CreateEditRequestDTO dto; @@ -123,10 +131,21 @@ void setUp() { .end(120L) .build()); - when(dto.getId()).thenReturn(UUID.randomUUID()); + UUID newEditRequestId = UUID.randomUUID(); + when(dto.getId()).thenReturn(newEditRequestId); when(dto.getSourceRecordingId()).thenReturn(mockRecordingId); - when(dto.getStatus()).thenReturn(EditRequestStatus.PENDING); + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(dto.getEditInstructions()).thenReturn(instructions); + + when(newlyUpdatedEditRequest.getId()).thenReturn(newEditRequestId); + when(newlyUpdatedEditRequest.getSourceRecording()).thenReturn(mockRecording); + when(newlyUpdatedEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(newlyUpdatedEditRequest.getEditInstruction()).thenReturn(toJson(instructions)); + + + when(editingService.mergeOldAndNewEditInstructions(any(CreateEditRequestDTO.class), any(Recording.class), + any(EditRequest.class))) + .thenReturn(newlyUpdatedEditRequest); } @Test @@ -170,28 +189,21 @@ void getPendingReencodeEditRequestsSuccess() { } @Test - @DisplayName("Should create a new edit request") + @DisplayName("Should create a new edit request and trigger email for SUBMITTED edit") void createEditRequestSuccess() { when(mockRecording.getParentRecording()).thenReturn(null); - - EditRequest editRequestToReturnFromEditingService = new EditRequest(); - editRequestToReturnFromEditingService.setId(dto.getId()); - editRequestToReturnFromEditingService.setStatus(dto.getStatus()); - editRequestToReturnFromEditingService.setSourceRecording(mockRecording); - editRequestToReturnFromEditingService.setEditInstruction(toJson(dto.getEditInstructions())); - + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); - when(editingService.prepareEditRequestToCreateOrUpdate(any(CreateEditRequestDTO.class), any(Recording.class), - any(EditRequest.class))).thenReturn(editRequestToReturnFromEditingService); - Pair response = underTest.upsert(dto, mockRecording, mockUser); - assertThat(response.getFirst()).isEqualTo(UpsertResult.CREATED); - assertThat(response.getSecond().getStatus()).isEqualTo(EditRequestStatus.PENDING); - assertThat(response.getSecond().getId()).isEqualTo(dto.getId()); + EditRequest response = underTest.upsert(dto, mockRecording, mockUser); + assertThat(response.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); + assertThat(response.getId()).isEqualTo(dto.getId()); verify(editRequestRepository, times(1)).findByIdNotLocked(dto.getId()); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); + verify(editRequestRepository, times(1)).saveAndFlush(any(EditRequest.class)); + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); } @Test @@ -211,17 +223,71 @@ void findByIdSuccess() { } @Test - @DisplayName("Should throw bad request when trying to create new edit request with empty instructions") - void badRequestEmptyInstructions() { + @DisplayName("Should allow draft edit request to be created with empty instructions") + void createDraftEditWithEmptyInstructions() { when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); + when(dto.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(editRequestRepository.findByIdNotLocked(dto.getId())).thenReturn(Optional.empty()); + + underTest.upsert(dto, mockRecording, mockUser); + + verify(editRequestRepository, times(1)).saveAndFlush(any(EditRequest.class)); + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(any(EditRequest.class)); + } + + @Test + @DisplayName("Should allow draft edit request to be upserted with empty instructions") + void updateDraftEditWithEmptyInstructions() { + when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); + when(dto.getStatus()).thenReturn(EditRequestStatus.DRAFT); + + List oldInstructions = new ArrayList<>(); + oldInstructions.add(EditCutInstructionDTO.builder() + .start(60L) + .end(120L) + .build()); + + EditRequest existingEditRequest = new EditRequest(); + existingEditRequest.setId(UUID.randomUUID()); + existingEditRequest.setStatus(EditRequestStatus.DRAFT); + existingEditRequest.setEditInstruction(toJson(oldInstructions)); + when(editRequestRepository.findByIdNotLocked(dto.getId())).thenReturn(Optional.of(existingEditRequest)); + + underTest.upsert(dto, mockRecording, mockUser); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); + verify(editRequestRepository, times(1)).saveAndFlush(captor.capture()); + + assertThat(captor.getValue().getId()).isEqualTo(existingEditRequest.getId()); + assertThat(captor.getValue().getStatus()).isEqualTo(EditRequestStatus.DRAFT); + assertThat(captor.getValue().getEditInstruction()) + .isEqualTo("{\"requestedInstructions\":[],\"ffmpegInstructions\":[]," + + "\"forceReencode\":false,\"sendNotifications\":false}"); + + verifyNoInteractions(editNotificationService); + } + + @Test + @DisplayName("Should throw error when edit request is submitted with empty instructions") + void badRequestEmptyInstructions() { + CreateEditRequestDTO createEditRequestDTO = new CreateEditRequestDTO(); + createEditRequestDTO.setId(UUID.randomUUID()); + createEditRequestDTO.setEditInstructions(new ArrayList<>()); + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + createEditRequestDTO.setJointlyAgreed(true); + createEditRequestDTO.setSourceRecordingId(mockRecordingId); String message = assertThrows( - BadRequestException.class, - () -> underTest.upsert(dto, mockRecording, mockUser) + IllegalArgumentException.class, + () -> underTest.upsert(createEditRequestDTO, mockRecording, mockUser) ).getMessage(); assertThat(message) - .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); + .isEqualTo("Invalid edit request: " + + "instructions may only be empty for DRAFT edit requests or forced re-encodes"); + + verifyNoInteractions(editNotificationService); } @Test @@ -271,41 +337,30 @@ void createEditRequestWithCutsAndForceReencode() { .isEqualTo("Invalid Instruction: Cannot request cuts and force reencode on the same edit request"); verify(editRequestRepository, never()).save(any(EditRequest.class)); + verifyNoInteractions(editNotificationService); } @Test - @DisplayName("Should delete edit request when upserting with empty instructions") - void deleteEmptyInstructions() { + @DisplayName("Should throw exception when editInstructions is null for *new* submitted edit request") + void validateEditInstructionsIsEmptyForNewEditRequest() { when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); - when(editRequestRepository.findByIdNotLocked(dto.getId())) - .thenReturn(Optional.of(mockEditRequest)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(mockEditRequest)); - - Pair result = underTest.upsert(dto, mockRecording, mockUser); - assertThat(result.getFirst()).isEqualTo(UpsertResult.UPDATED); - - verify(editRequestRepository, times(1)).delete(result.getSecond()); - } - - @Test - @DisplayName("Should throw exception when editInstructions is null for *new* edit request") - void validateEditInstructionsIsEmptyForNewEditRequest() throws Exception { - when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); - when(dto.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); String message = assertThrows( - BadRequestException.class, + IllegalArgumentException.class, () -> underTest.upsert(dto, mockRecording, mockUser) ).getMessage(); assertThat(message) - .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); + .isEqualTo("Invalid edit request: " + + "instructions may only be empty for DRAFT edit requests or forced re-encodes"); + verifyNoInteractions(editNotificationService); } @Test @DisplayName("Should ignore attempt to delete non-existent edit request") - void deleteNonExistentEditRequestSuccess() throws Exception { + void deleteNonExistentEditRequestSuccess() { when(dto.getStatus()).thenReturn(EditRequestStatus.DRAFT); when(editRequestRepository.findById(dto.getSourceRecordingId())) .thenReturn(Optional.empty()); @@ -339,4 +394,84 @@ void findRecordingIdsWithForceReencodeRequestsEmptySet() { verify(editRequestRepository, never()).findSourceRecordingIdsWithForceReencodeRequests(any()); } + @Test + @DisplayName("*New* edit request triggers email for SUBMITTED edit request") + void newEditRequestTriggersEmailOnSubmission() { + when(mockRecording.getParentRecording()).thenReturn(null); + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); + + underTest.upsert(dto, mockRecording, mockUser); + + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); + } + + @Test + @DisplayName("*Updated* edit request triggers email for SUBMITTED edit request when status has changed") + void updatedEditRequestTriggersEmailOnSubmissionWhenStatusChanged() { + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(mockEditRequest)); + + + underTest.upsert(dto, mockRecording, mockUser); + + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); + } + + @Test + @DisplayName("*No* email for updated SUBMITTED edit request when status has *not* changed") + void updatedEditRequestNoEmailOnSubmissionWhenStatusHasNotChanged() { + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + underTest.upsert(dto, mockRecording, mockUser); + + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); + } + + @Test + @DisplayName("Should send request to notification service regardless of which status changed") + void shouldTriggerNotificationWhicheverStatusChanged() { + // Won't actually trigger an email but this is handled by notification service + when(dto.getStatus()).thenReturn(EditRequestStatus.COMPLETE); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + + underTest.upsert(dto, mockRecording, mockUser); + + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); + } + + @Test + @DisplayName("Should invoke notification service when updating edit request status") + void shouldInvokeNotificationServiceWhenStatusChanged() { + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.of(mockEditRequest)); + underTest.updateEditRequestStatus(mockEditRequestId, EditRequestStatus.COMPLETE); + verify(editNotificationService, times(1)).editRequestStatusWasUpdated(mockEditRequest); + } + + @Test + @DisplayName("Should create a new reencode edit request") + void createReencodeEditRequestSuccess() { + when(dto.isForceReencode()).thenReturn(true); + when(dto.getEditInstructions()).thenReturn(null); + when(dto.getSendNotifications()).thenReturn(true); + underTest.upsert(dto, mockRecording, mockUser); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); + verify(editRequestRepository, times(1)).saveAndFlush(captor.capture()); + + EditInstructions editInstructions = EditInstructions.tryFromJson(captor.getValue().getEditInstruction()); + + assertThat(editInstructions).isNotNull(); + assertThat(editInstructions.getRequestedInstructions()).isEmpty(); + assertThat(editInstructions.getFfmpegInstructions()).isEmpty(); + assertThat(editInstructions.isForceReencode()).isTrue(); + assertThat(editInstructions.shouldSendNotifications()).isTrue(); + } } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegServiceTest.java index 7710353d16..9ca3ea4a3f 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegServiceTest.java @@ -736,7 +736,7 @@ void upsertIsOriginalRecordingEditFalseIsInstructionCombinationFalse() { when(mockRecording.getFilename()).thenReturn("filename.mp4"); when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(30)); - EditRequest result = underTest.prepareEditRequestToCreateOrUpdate(mockCreateEditRequestDTO, + EditRequest result = underTest.mergeOldAndNewEditInstructions(mockCreateEditRequestDTO, mockRecording, realEditRequest); assertThat(result.getId()).isEqualTo(mockCreateEditRequestDTO.getId()); assertThat(result.getSourceRecording().getId()).isEqualTo(mockRecording.getId()); @@ -770,7 +770,7 @@ void upsertIsOriginalRecordingEditFalseIsInstructionCombinationTrue() { when(mockRecording.getParentRecording()).thenReturn(mockParentRecording); when(mockRecording.getEditInstruction()).thenReturn(toJson(originalEdits)); - EditRequest result = underTest.prepareEditRequestToCreateOrUpdate(mockCreateEditRequestDTO, + EditRequest result = underTest.mergeOldAndNewEditInstructions(mockCreateEditRequestDTO, mockRecording, realEditRequest); assertThat(result.getId()).isEqualTo(realEditRequest.getId()); assertThat(result.getSourceRecording().getId()).isEqualTo(mockParentRecording.getId()); @@ -803,7 +803,7 @@ void upsertIsOriginalRecordingEditFalseIsInstructionCombinationTrue() { @DisplayName("Should create a new edit request with notifications disabled") void createEditRequestWithNotificationsDisabledSuccess() { when(mockCreateEditRequestDTO.getSendNotifications()).thenReturn(false); - EditRequest response = underTest.prepareEditRequestToCreateOrUpdate(mockCreateEditRequestDTO, + EditRequest response = underTest.mergeOldAndNewEditInstructions(mockCreateEditRequestDTO, mockRecording, realEditRequest); @@ -817,7 +817,7 @@ void createEditRequestWithNotificationsDisabledSuccess() { @Test @DisplayName("Should update an edit request") void updateEditRequestSuccess() { - EditRequest response = underTest.prepareEditRequestToCreateOrUpdate(mockCreateEditRequestDTO, + EditRequest response = underTest.mergeOldAndNewEditInstructions(mockCreateEditRequestDTO, mockRecording, realEditRequest); assertThat(response.getId()).isEqualTo(mockCreateEditRequestDTO.getId()); @@ -828,21 +828,6 @@ void updateEditRequestSuccess() { .contains("\"ffmpegInstructions\":[{\"start\":0,\"end\":10},{\"start\":20,\"end\":180}]"); } - @Test - @DisplayName("Should create a new reencode edit request") - void createReencodeEditRequestSuccess() { - when(mockCreateEditRequestDTO.isForceReencode()).thenReturn(true); - EditRequest response = underTest.prepareEditRequestToCreateOrUpdate(mockCreateEditRequestDTO, - mockRecording, realEditRequest); - - EditInstructions editInstructions = EditInstructions.tryFromJson(response.getEditInstruction()); - assertThat(editInstructions).isNotNull(); - assertThat(editInstructions.getRequestedInstructions()).isEmpty(); - assertThat(editInstructions.getFfmpegInstructions()).isEmpty(); - assertThat(editInstructions.isForceReencode()).isTrue(); - assertThat(editInstructions.shouldSendNotifications()).isTrue(); - } - private String generateEditInstructionsJson(List ffmpegEditInstructions) throws JsonProcessingException { diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/tasks/ReEncodeRecordingsFromCsvTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/tasks/ReEncodeRecordingsFromCsvTest.java index 9968f2a790..63a50c37f4 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/tasks/ReEncodeRecordingsFromCsvTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/tasks/ReEncodeRecordingsFromCsvTest.java @@ -10,7 +10,6 @@ import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.base.BaseAppAccessDTO; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.UpsertResult; import uk.gov.hmcts.reform.preapi.security.authentication.UserAuthentication; import uk.gov.hmcts.reform.preapi.security.service.UserAuthenticationService; import uk.gov.hmcts.reform.preapi.services.EditRequestService; @@ -28,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -161,9 +161,9 @@ void continuesProcessingWhenSubmitFails() throws IOException { %s, %s,CASE-456 """.formatted(failingRecordingId, successfulRecordingId)); - when(editRequestService.upsert(any(CreateEditRequestDTO.class))) - .thenThrow(new IllegalStateException("failed")) - .thenReturn(UpsertResult.CREATED); + + doThrow(new IllegalStateException("failed")) + .when(editRequestService).upsert(any(CreateEditRequestDTO.class)); task(csv, false).run(); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java new file mode 100644 index 0000000000..ad6d6409fe --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -0,0 +1,37 @@ +package uk.gov.hmcts.reform.preapi.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class StringToolsTest { + + @Test + @DisplayName("Should parse and render time as strings") + void should_parse_and_render_time_as_strings() { + String parsedTime = StringTools.formatTimeAsString(35); + assertThat(parsedTime).isEqualTo("00:00:35"); + } + + @Test + @DisplayName("Should complain if time is negative") + void should_complain_if_time_is_negative() { + Assertions.assertThrows(IllegalArgumentException.class, () -> StringTools.formatTimeAsString(-3)); + } + + @Test + @DisplayName("Should cope with big time") + void should_cope_with_big_time() { + String parsedTime = StringTools.formatTimeAsString(3569429); + assertThat(parsedTime).isEqualTo("991:30:29"); + } + + @Test + @DisplayName("Should cope with zero time") + void should_cope_with_zero_time() { + String parsedTime = StringTools.formatTimeAsString(0); + assertThat(parsedTime).isEqualTo("00:00:00"); + } +}