From f19a47fe3babc48e2d1fc1044031f2941d584635 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 3 Jun 2026 14:13:14 +0100 Subject: [PATCH 01/37] Refactor email notifications to use common notifier --- .../reform/preapi/email/GovNotifyFT.java | 72 ++++----- .../reform/preapi/email/IEmailService.java | 9 +- .../preapi/email/govnotify/GovNotify.java | 148 ++++-------------- .../templates/EditEmailParameters.java | 39 +++++ .../templates/EditRequestStatusChanged.java | 45 ++++++ .../govnotify/templates/EditingEmailType.java | 7 + .../templates/EditingJointlyAgreed.java | 33 ---- .../templates/EditingNotJointlyAgreed.java | 32 ---- .../govnotify/templates/EditingRejection.java | 34 ---- .../services/EditNotificationService.java | 120 ++++++++++---- .../reform/preapi/utils/StringTools.java | 25 +++ 11 files changed, 269 insertions(+), 295 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParameters.java create mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java create mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingEmailType.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingJointlyAgreed.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingNotJointlyAgreed.java delete mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditingRejection.java create mode 100644 src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java 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..084c40bcf2 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 @@ -5,18 +5,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; 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.email.govnotify.templates.EditEmailParameters; 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 java.sql.Timestamp; -import java.util.Set; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,32 +45,25 @@ private Case createCase() { return forCase; } - private Participant createParticipant(ParticipantType type) { - var participant = new Participant(); - participant.setFirstName("First"); - participant.setLastName("Last"); - participant.setParticipantType(type); - return participant; - } - 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; + private EditEmailParameters createEditEmailParameters() { + return EditEmailParameters.builder() + .toEmailAddress(FROM_EMAIL_ADDRESS) + .caseReference("123456") + .witnessName("First") + .defendantName("First Last") + .courtName("Court Name") + .editSummary(""" + Edit 1:\s + Start time: 00:00:00 + End time: 00:00:30 + Time Removed: 00:00:00 + Reason:\s""") + .editRequestStatus(EditRequestStatus.DRAFT) + .numberOfRequestedEditInstructions(1) + .jointlyAgreed(true) + .rejectionReason(null) + .build(); } private void compareBody(String expected, EmailResponse emailResponse) { @@ -276,10 +265,8 @@ void verifyEmail() { @DisplayName("Should send editing jointly agreed email") @SuppressWarnings("LineLength") void editingJointlyAgreed() { - var user = createUser(); - var forEditRequest = createEditRequest(); + EmailResponse response = client.sendEmailAboutEditingRequest(createEditEmailParameters()); - 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", @@ -310,10 +297,10 @@ Defendant name(s): First Last @DisplayName("Should send editing not jointly agreed email") @SuppressWarnings("LineLength") void editingNotJointlyAgreed() { - var user = createUser(); - var forEditRequest = createEditRequest(); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setJointlyAgreed(false); - var response = client.editingNotJointlyAgreed(user.getEmail(), forEditRequest); + Optional response = client.sendEmailAboutEditingRequest(editEmailParameters); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( "[Do Not Reply] Pre-recorded Evidence: Edit request for case reference 123456 (NOT JOINTLY AGREED)", @@ -344,12 +331,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); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setRejectionReason("REJECTION REASON"); + editEmailParameters.setJointlyAgreed(true); - var response = client.editingRejected(user.getEmail(), forEditRequest); + var response = client.sendEmailAboutEditingRequest(editEmailParameters); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( "[Do Not Reply] Pre-recorded Evidence: Edit request REJECTION for case reference 123456", 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..13eaaa96b6 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 @@ -1,10 +1,11 @@ package uk.gov.hmcts.reform.preapi.email; +import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditEmailParameters; import uk.gov.hmcts.reform.preapi.entities.Case; -import uk.gov.hmcts.reform.preapi.entities.EditRequest; 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(EditEmailParameters editEmailParameters); } 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..f6790a8aa6 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,23 +4,19 @@ 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.EditRequestStatusChanged; 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; import uk.gov.hmcts.reform.preapi.exception.EmailFailedToSendException; import uk.gov.service.notify.NotificationClient; @@ -28,12 +24,7 @@ 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 @@ -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,33 @@ 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(); + public Optional sendEmailAboutEditingRequest(EditEmailParameters editEmailParameters) { + EditRequestStatusChanged template = new EditRequestStatusChanged(editEmailParameters, portalUrl); - String witnessName = booking.getWitnessName(); - String defendant = booking.getDefendantName(); + String to = editEmailParameters.getToEmailAddress(); - EditingJointlyAgreed template = new EditingJointlyAgreed( - to, - booking.getCaseId().getReference(), - requestInstructions.size(), - booking.getCaseId().getCourt().getName(), - witnessName, - defendant, - generateEditSummary(requestInstructions), - portalUrl - ); - - 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); + if (to == null) { + log.error( + "Court {} does not have a group email for sending edit request submission email for case: {}", + editEmailParameters.getCourtName(), editEmailParameters.getCaseReference() + ); + 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(); - - String defendant = booking.getDefendantName(); - - EditingNotJointlyAgreed template = new EditingNotJointlyAgreed( - to, - booking.getCaseId().getReference(), - requestInstructions.size(), - booking.getCaseId().getCourt().getName(), - witnessName, - defendant, - generateEditSummary(requestInstructions), - portalUrl - ); 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..2aaa62b10c --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParameters.java @@ -0,0 +1,39 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; + +import java.util.Map; + +@Getter +@Setter +@Builder(toBuilder = true) +public class EditEmailParameters { + private EditRequestStatus editRequestStatus; + private String toEmailAddress; // court group email + private String witnessName; + private String defendantName; + private String caseReference; + private Integer numberOfRequestedEditInstructions; + private String courtName; + private String editSummary; + private String rejectionReason; + private Boolean jointlyAgreed; + + public Map getEmailParameterMap(String portalUrl) { + return Map.of( + "rejection_reason", getEditSummary(), + "jointly_agreed", getJointlyAgreed() ? "Yes" : "No", + "case_reference", getCaseReference(), + "court_name", getCourtName(), + "witness_name", getWitnessName(), + "defendant_names", getDefendantName(), + "edit_summary", getEditSummary(), + "portal_link", portalUrl, + "edit_count", getNumberOfRequestedEditInstructions() + ); + + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java new file mode 100644 index 0000000000..3a8c599db4 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java @@ -0,0 +1,45 @@ +package uk.gov.hmcts.reform.preapi.email.govnotify.templates; + +import lombok.Getter; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; + +public class EditRequestStatusChanged extends BaseTemplate { + + @Getter + private final EditingEmailType editingEmailType; + + public EditRequestStatusChanged(EditEmailParameters editEmailParameters, String portalUrl) { + super( + editEmailParameters.getToEmailAddress(), + editEmailParameters.getEmailParameterMap(portalUrl) + ); + + 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("Could not work out which type of edit email to send:" + + " edit status %s, jointly agreed %b" + + editEmailParameters.getEditRequestStatus()); + } +} 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..64aaa8b83a 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 @@ -1,24 +1,42 @@ package uk.gov.hmcts.reform.preapi.services; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; 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.EditEmailParameters; 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.Recording; import uk.gov.hmcts.reform.preapi.entities.ShareBooking; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; +import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; + +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.utils.StringTools.isBlankString; @Service @Slf4j public class EditNotificationService { private final EmailServiceFactory emailServiceFactory; + private final RecordingRepository recordingRepository; @Autowired - public EditNotificationService(final EmailServiceFactory emailServiceFactory) { + public EditNotificationService( + final EmailServiceFactory emailServiceFactory, + final RecordingRepository recordingRepository) { this.emailServiceFactory = emailServiceFactory; + this.recordingRepository = recordingRepository; } @Transactional @@ -29,44 +47,88 @@ 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()); - return; + public void editRequestStatusWasUpdated(EditRequest editRequest) { + if (editRequest.getStatus() != EditRequestStatus.SUBMITTED + && editRequest.getStatus() != EditRequestStatus.REJECTED + && editRequest.getStatus() != EditRequestStatus.APPROVED) { + // For edited recordings, notification is sent by RecordingListener instead + log.info("No notification needed for edit request status {}", editRequest.getStatus().name()); } - String groupEmail = court.getGroupEmail(); + Recording sourceRecording = recordingRepository.findById(editRequest.getSourceRecordingId()) + .orElseThrow(() -> new NotFoundException("Unable to send email: could not find source recording " + + editRequest.getSourceRecordingId())); - try { - IEmailService enabledEmailService = emailServiceFactory.getEnabledEmailService(); + if (sourceRecording.getEditRequest() == null) { + throw new ResourceInWrongStateException(format( + "Unable to send email: source recording %s did not have an active edit request", + sourceRecording.getId() + )); + } + + EditEmailParameters editEmailParameters = getEmailParameters(sourceRecording); - if (Boolean.TRUE.equals(request.getJointlyAgreed())) { - enabledEmailService.editingJointlyAgreed(groupEmail, request); - } else { - enabledEmailService.editingNotJointlyAgreed(groupEmail, request); - } + try { + emailServiceFactory.getEnabledEmailService().sendEmailAboutEditingRequest(editEmailParameters); } 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; + private @NotNull EditEmailParameters getEmailParameters(Recording outputRecording) { + EditRequest editRequest = outputRecording.getEditRequest(); + if (editRequest == null) { + throw new NotFoundException("No edit request found when trying to send notification"); } - try { - emailServiceFactory.getEnabledEmailService().editingRejected(court.getGroupEmail(), request); - } catch (Exception e) { - log.error("Error sending email on edit request rejection: {}", e.getMessage()); + Booking booking = outputRecording.getCaptureSession().getBooking(); + if (booking == null) { + throw new NotFoundException("No booking found when trying to send edit notification"); } + + String courtEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); + + if (isBlankString(courtEmailAddress)) { + log.error( + "Court {} does not have a group email for sending edit request submission email for request: {}", + booking.getCaseId().getCourt().getName(), outputRecording.getEditRequest().getId() + ); + } + + String editSummary = generateEditSummary(EditRequestDTO.toDTO(editRequest.getEditCutInstructions())); + + return EditEmailParameters.builder() + .toEmailAddress(courtEmailAddress) + .caseReference(booking.getCaseId().getReference()) + .witnessName(booking.getWitnessName()) + .defendantName(booking.getDefendantName()) + .courtName(booking.getCaseId().getCourt().getName()) + .editSummary(editSummary) + .editRequestStatus(editRequest.getStatus()) + .numberOfRequestedEditInstructions(editRequest.getEditCutInstructions().size()) + .jointlyAgreed(editRequest.getJointlyAgreed()) + .rejectionReason(editRequest.getRejectionReason()) + .build(); + } + + private String generateEditSummary(List editInstructions) { + StringJoiner summary = new StringJoiner(""); + for (int i = 0; i < editInstructions.size(); i++) { + EditCutInstructionsDTO 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(EditCutInstructionsDTO 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/utils/StringTools.java b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java new file mode 100644 index 0000000000..cc57c82cc5 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java @@ -0,0 +1,25 @@ +package uk.gov.hmcts.reform.preapi.utils; + +import java.time.Duration; +import java.time.LocalTime; + +import static java.lang.String.format; + +public class StringTools { + public static boolean isBlankString(String string) { + return string == null || string.trim().isEmpty(); + } + + 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); + } + +} From 61056862ca81ad264f22002a9d16ff4f916c9592 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 12:31:43 +0100 Subject: [PATCH 02/37] Create email parameters from edit request --- .../templates/EditEmailParameters.java | 152 ++++++++-- .../templates/EditEmailParametersTest.java | 262 ++++++++++++++++++ 2 files changed, 392 insertions(+), 22 deletions(-) create mode 100644 src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParametersTest.java 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 index 2aaa62b10c..841c86bb71 100644 --- 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 @@ -1,39 +1,147 @@ package uk.gov.hmcts.reform.preapi.email.govnotify.templates; -import lombok.Builder; import lombok.Getter; -import lombok.Setter; +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 -@Setter -@Builder(toBuilder = true) public class EditEmailParameters { + + private Boolean jointlyAgreed; private EditRequestStatus editRequestStatus; private String toEmailAddress; // court group email - private String witnessName; - private String defendantName; - private String caseReference; - private Integer numberOfRequestedEditInstructions; - private String courtName; - private String editSummary; - private String rejectionReason; - private Boolean jointlyAgreed; + private Map emailParameters; + + public EditEmailParameters() { + } + + public EditEmailParameters(EditRequest editRequest, String portalUrl) { + validateEditRequestIsOkayForNotification(editRequest); + + if (portalUrl == null) { + throw new BadRequestException("Portal URL is missing"); + } - public Map getEmailParameterMap(String portalUrl) { - return Map.of( - "rejection_reason", getEditSummary(), - "jointly_agreed", getJointlyAgreed() ? "Yes" : "No", - "case_reference", getCaseReference(), - "court_name", getCourtName(), - "witness_name", getWitnessName(), - "defendant_names", getDefendantName(), - "edit_summary", getEditSummary(), + Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); + + List requestInstructions = + EditInstructions.fromJson(editRequest.getEditInstruction()).getRequestedInstructions(); + + String summary = generateEditSummary(requestInstructions); + + this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); + this.editRequestStatus = editRequest.getStatus(); + this.jointlyAgreed = editRequest.getJointlyAgreed(); + this.emailParameters = Map.of( + "rejection_reason", editRequest.getRejectionReason(), + "jointly_agreed", editRequest.getJointlyAgreed() ? "Yes" : "No", + "case_reference", booking.getCaseId().getReference(), + "court_name", booking.getCaseId().getCourt().getName(), + "witness_name", booking.getWitnessName(), + "defendant_names", booking.getDefendantName(), + "edit_summary", summary, "portal_link", portalUrl, - "edit_count", getNumberOfRequestedEditInstructions() + "edit_count", requestInstructions.size() ); + } + + 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()); + } + + 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() + )); + } + + if (editRequest.getEditInstruction() == null) { + throw new BadRequestException("No instructions found when trying to send edit notification for edit ID: " + + editRequest.getId()); + } + + EditInstructions instructions = EditInstructions.fromJson(editRequest.getEditInstruction()); + + 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()); + } } + + 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/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..f3d43f90b7 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditEmailParametersTest.java @@ -0,0 +1,262 @@ +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.springframework.boot.test.context.SpringBootTest; +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; +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.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = EditEmailParameters.class) +public class EditEmailParametersTest { + + @MockitoBean + private Recording mockRecording; + + @MockitoBean + private CaptureSession mockCaptureSession; + + @MockitoBean + private Booking mockBooking; + + @MockitoBean + private Court mockCourt; + + @MockitoBean + private Case mockCase; + + private final UUID editRequestId = UUID.randomUUID(); + private EditRequest editRequest; + private List instructionsList; + + private final String portalUrl = "http://localhost:8080"; + + @BeforeEach + public void setUp() { + when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); + when(mockCaptureSession.getBooking()).thenReturn(mockBooking); + + when(mockCourt.getName()).thenReturn("Court Name"); + when(mockCourt.getGroupEmail()).thenReturn("Group Email"); + + when(mockCase.getCourt()).thenReturn(mockCourt); + when(mockCase.getReference()).thenReturn("Test Case"); + + when(mockBooking.getCaseId()).thenReturn(mockCase); + when(mockBooking.getWitnessName()).thenReturn("Witness Name"); + when(mockBooking.getDefendantName()).thenReturn("Defendant Name"); + + 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); + + EditInstructions defaultEditInstructions = new EditInstructions(instructionsList, new ArrayList<>()); + String editInstructionsAsJson = JsonUtils.toJson(defaultEditInstructions); + + CreateEditRequestDTO dto = new CreateEditRequestDTO(); + dto.setId(editRequestId); + dto.setJointlyAgreed(true); + dto.setStatus(EditRequestStatus.APPROVED); + dto.setRejectionReason("Rejection Reason"); + + editRequest = new EditRequest(); + editRequest.updateEditRequestFromDto(dto, mockRecording, editInstructionsAsJson); + } + +// @Test +// @DisplayName("Cannot create email parameters from null edit request") +// void cannotCreateEmailParametersFromNullEditRequest() { +// EditRequest editRequest = null; +// assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest)); +// } + + @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)); + } + + @Test + @DisplayName("Cannot create email parameters with null edit instructions") + void cannotCreateEmailParametersFromNullEditInstructions() { + editRequest.setEditInstruction(null); + 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") + public void shouldAccuratelyCreateEmailParameters() { + editRequest.setStatus(EditRequestStatus.REJECTED); + editRequest.setRejectionReason("Rejected Reason"); + editRequest.setJointlyAgreed(false); + + EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + + assertThat(editEmailParameters.getJointlyAgreed()).isEqualTo(false); + + Map paramsMap = editEmailParameters.getEmailParameters(); + + assertThat(paramsMap).isNotNull(); + assertThat(paramsMap).isNotEmpty(); + assertThat(paramsMap.get("rejection_reason")).isEqualTo(editRequest.getRejectionReason()); + assertThat(paramsMap.get("jointly_agreed")).isEqualTo("No"); + assertThat(paramsMap.get("case_reference")).isEqualTo(mockCase.getReference()); + assertThat(paramsMap.get("court_name")).isEqualTo(mockCourt.getName()); + assertThat(paramsMap.get("witness_name")).isEqualTo("Witness Name"); + assertThat(paramsMap.get("defendant_names")).isEqualTo("Defendant Name"); + assertThat(paramsMap.get("edit_count")).isEqualTo(3); + assertThat(paramsMap.get("portal_link")).isEqualTo("http://localhost:8080"); + + Object gotSummary = paramsMap.get("edit_summary"); + assertThat(gotSummary).isNotNull(); + assertThat(gotSummary).isInstanceOf(String.class); + + 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 + + """; + + assertThat(gotSummary).isEqualTo(expectedSummary); + } + + @Test + @DisplayName("Should accurately create edit email parameters for JOINTLY_AGREED edit") + public void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { + editRequest.setStatus(EditRequestStatus.SUBMITTED); + editRequest.setRejectionReason(null); + editRequest.setJointlyAgreed(true); + + EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + + assertThat(editEmailParameters.getJointlyAgreed()).isEqualTo(true); + + Map paramsMap = editEmailParameters.getEmailParameters(); + + assertThat(paramsMap).isNotNull(); + assertThat(paramsMap).isNotEmpty(); + assertThat(paramsMap.get("rejection_reason")).isEqualTo(null); + assertThat(paramsMap.get("jointly_agreed")).isEqualTo("Yes"); + assertThat(paramsMap.get("case_reference")).isEqualTo(mockCase.getReference()); + assertThat(paramsMap.get("court_name")).isEqualTo(mockCourt.getName()); + assertThat(paramsMap.get("witness_name")).isEqualTo("Witness Name"); + assertThat(paramsMap.get("defendant_names")).isEqualTo("Defendant Name"); + assertThat(paramsMap.get("edit_count")).isEqualTo(3); + assertThat(paramsMap.get("portal_link")).isEqualTo("test.portal.url"); + assertThat(paramsMap.get("edit_summary")).isEqualTo(""" + Edit 1: + Start time: 00:00:05 + End time: 00:00:10 + Time Removed: 00:00:05 + Reason: first reason + + Edit 2: + Start time: 00:00:23 + End time: 00:00:26 + Time Removed: 00:00:03 + Reason: second reason + + Edit 3: + Start time: 00:00:31 + End time: 00:00:32 + Time Removed: 00:00:01 + Reason: third reason + + """); + } + +} From 4ea2c364d9f4fd8e507a2c339f10b6f76d07f857 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 12:36:22 +0100 Subject: [PATCH 03/37] Edit email template --- ...ged.java => EditRequestEmailTemplate.java} | 18 ++-- .../EditRequestEmailTemplateTest.java | 88 +++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) rename src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/{EditRequestStatusChanged.java => EditRequestEmailTemplate.java} (66%) create mode 100644 src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplateTest.java diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java similarity index 66% rename from src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java rename to src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java index 3a8c599db4..3f52c96db1 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestStatusChanged.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplate.java @@ -3,15 +3,17 @@ import lombok.Getter; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -public class EditRequestStatusChanged extends BaseTemplate { +import static java.lang.String.format; + +public class EditRequestEmailTemplate extends BaseTemplate { @Getter private final EditingEmailType editingEmailType; - public EditRequestStatusChanged(EditEmailParameters editEmailParameters, String portalUrl) { + public EditRequestEmailTemplate(EditEmailParameters editEmailParameters) { super( editEmailParameters.getToEmailAddress(), - editEmailParameters.getEmailParameterMap(portalUrl) + editEmailParameters.getEmailParameters() ); this.editingEmailType = calculateEditingEmailType(editEmailParameters); @@ -27,7 +29,7 @@ public String getTemplateId() { } private EditingEmailType calculateEditingEmailType(EditEmailParameters editEmailParameters) { - if(editEmailParameters.getEditRequestStatus() == EditRequestStatus.REJECTED) { + if (editEmailParameters.getEditRequestStatus() == EditRequestStatus.REJECTED) { return EditingEmailType.REJECTED; } @@ -38,8 +40,10 @@ private EditingEmailType calculateEditingEmailType(EditEmailParameters editEmail return EditingEmailType.NOT_JOINTLY_AGREED; } } - throw new IllegalArgumentException("Could not work out which type of edit email to send:" + - " edit status %s, jointly agreed %b" - + editEmailParameters.getEditRequestStatus()); + 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/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..7333a10fc9 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/templates/EditRequestEmailTemplateTest.java @@ -0,0 +1,88 @@ +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; + +public class EditRequestEmailTemplateTest { + + @Test + @DisplayName("Should create REJECTED edit email template") + public 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().get("jointly_agreed")).isEqualTo("Yes"); + } + + @Test + @DisplayName("Should create JOINTLY_AGREED edit email template") + public 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().get("jointly_agreed")).isEqualTo("Yes"); + } + + @Test + @DisplayName("Should create NOT_JOINTLY_AGREED edit email template") + public 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().get("jointly_agreed")).isEqualTo("No"); + } + + @Test + @DisplayName("Should throw exception if unrecognised email type") + public 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"); + } + +} From 10ebdbd4d2e5f990bb3fdee9b51c8f0e70dc55eb Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 12:41:50 +0100 Subject: [PATCH 04/37] Gov Notify email service --- .../reform/preapi/email/GovNotifyFT.java | 77 +-- .../reform/preapi/email/IEmailService.java | 4 +- .../preapi/email/govnotify/GovNotify.java | 19 +- .../email/{ => govnotify}/GovNotifyTest.java | 444 ++++++++---------- 4 files changed, 246 insertions(+), 298 deletions(-) rename src/test/java/uk/gov/hmcts/reform/preapi/email/{ => govnotify}/GovNotifyTest.java (54%) 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 084c40bcf2..7094d7a905 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,20 +1,33 @@ 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.email.govnotify.templates.EditEmailParameters; +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.entities.User; 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.ArrayList; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @SpringBootTest class GovNotifyFT { @@ -28,6 +41,9 @@ class GovNotifyFT { @Autowired GovNotify client; + @MockitoBean + EditRequest editRequest; + private User createUser() { var user = new User(); user.setFirstName(USER_FIRST_NAME); @@ -39,31 +55,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; } + @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); - private EditEmailParameters createEditEmailParameters() { - return EditEmailParameters.builder() - .toEmailAddress(FROM_EMAIL_ADDRESS) - .caseReference("123456") - .witnessName("First") - .defendantName("First Last") - .courtName("Court Name") - .editSummary(""" - Edit 1:\s - Start time: 00:00:00 - End time: 00:00:30 - Time Removed: 00:00:00 - Reason:\s""") - .editRequestStatus(EditRequestStatus.DRAFT) - .numberOfRequestedEditInstructions(1) - .jointlyAgreed(true) - .rejectionReason(null) - .build(); + EditCutInstructionDTO instruction1 = new EditCutInstructionDTO(0, 30, "first reason"); + EditInstructions defaultEditInstructions = new EditInstructions(List.of(instruction1), new ArrayList<>()); + when(editRequest.getEditInstruction()).thenReturn(JsonUtils.toJson(defaultEditInstructions)); + + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(editRequest.getSourceRecording()).thenReturn(recording); } private void compareBody(String expected, EmailResponse emailResponse) { @@ -265,7 +285,10 @@ void verifyEmail() { @DisplayName("Should send editing jointly agreed email") @SuppressWarnings("LineLength") void editingJointlyAgreed() { - EmailResponse response = client.sendEmailAboutEditingRequest(createEditEmailParameters()); + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + EmailResponse response = client.sendEmailAboutEditingRequest(editRequest).orElseThrow(); assertEquals(FROM_EMAIL_ADDRESS, response.getFromEmail()); assertEquals( @@ -290,17 +313,18 @@ Defendant name(s): First Last 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() { - EditEmailParameters editEmailParameters = createEditEmailParameters(); - editEmailParameters.setJointlyAgreed(false); + when(editRequest.getJointlyAgreed()).thenReturn(false); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - Optional response = client.sendEmailAboutEditingRequest(editEmailParameters); + 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)", @@ -331,11 +355,10 @@ Defendant name(s): First Last @DisplayName("Should send editing rejection email") @SuppressWarnings("LineLength") void editingRejectionEmail() { - EditEmailParameters editEmailParameters = createEditEmailParameters(); - editEmailParameters.setRejectionReason("REJECTION REASON"); - editEmailParameters.setJointlyAgreed(true); + when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getStatus()).thenReturn(EditRequestStatus.REJECTED); - var response = client.sendEmailAboutEditingRequest(editEmailParameters); + 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", 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 13eaaa96b6..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 @@ -1,7 +1,7 @@ package uk.gov.hmcts.reform.preapi.email; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditEmailParameters; import uk.gov.hmcts.reform.preapi.entities.Case; +import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.User; import java.sql.Timestamp; @@ -22,5 +22,5 @@ public interface IEmailService { EmailResponse emailVerification(String email, String firstName, String lastName, String verificationCode); - Optional sendEmailAboutEditingRequest(EditEmailParameters editEmailParameters); + 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 f6790a8aa6..f95560f2bc 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 @@ -11,12 +11,13 @@ 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.EditEmailParameters; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditRequestStatusChanged; +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.Case; +import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.User; import uk.gov.hmcts.reform.preapi.exception.EmailFailedToSendException; import uk.gov.service.notify.NotificationClient; @@ -28,7 +29,6 @@ @Slf4j @Service -@SuppressWarnings("PMD.CouplingBetweenObjects") public class GovNotify implements IEmailService { private final NotificationClient client; private final String portalUrl; @@ -160,18 +160,13 @@ public EmailResponse emailVerification(String email, String firstName, String la } @Override - public Optional sendEmailAboutEditingRequest(EditEmailParameters editEmailParameters) { - EditRequestStatusChanged template = new EditRequestStatusChanged(editEmailParameters, portalUrl); + public Optional sendEmailAboutEditingRequest(EditRequest editRequest) { - String to = editEmailParameters.getToEmailAddress(); + EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); - if (to == null) { - log.error( - "Court {} does not have a group email for sending edit request submission email for case: {}", - editEmailParameters.getCourtName(), editEmailParameters.getCaseReference() - ); - return Optional.empty(); - } + EditRequestEmailTemplate template = new EditRequestEmailTemplate(editEmailParameters); + + String to = editEmailParameters.getToEmailAddress(); try { log.info("Edit request {} email sent to {}", template.getEditingEmailType(), to); 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 54% 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..a2ed803d20 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,27 +1,24 @@ -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.email.EmailResponse; +import uk.gov.hmcts.reform.preapi.email.EmailServiceFactory; 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.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.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.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.ParticipantType; import uk.gov.hmcts.reform.preapi.exception.EmailFailedToSendException; import uk.gov.service.notify.NotificationClient; import uk.gov.service.notify.NotificationClientException; @@ -29,7 +26,7 @@ import java.sql.Timestamp; import java.util.List; -import java.util.Set; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -64,6 +61,17 @@ public class GovNotifyTest { } }"""; + + private GovNotify underTest; + + @BeforeEach + void setUp() throws NotificationClientException { + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) + .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); + + underTest = new GovNotify("http://localhost:8080", mockGovNotifyClient); + } + @DisplayName("Should create CaseClosed template") @Test void shouldCreateCaseClosedTemplate() { @@ -95,11 +103,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"); } @@ -141,65 +151,17 @@ void shouldCreateEmailServiceFactory() { assertThat(emailServiceFactory.getEnabledEmailService("GovNotify")).isEqualTo(govNotify); assertThrows(IllegalArgumentException.class, () -> emailServiceFactory.getEnabledEmailService("nonexistent")); - assertThrows(IllegalArgumentException.class, () -> - new EmailServiceFactory("nonexistent", true, List.of(govNotify)) + assertThrows( + IllegalArgumentException.class, () -> + new EmailServiceFactory("nonexistent", true, List.of(govNotify)) ); } - 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()); + var response = underTest.recordingReady(getSampleUser(), getSampleCase()); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -208,12 +170,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(getSampleUser(), getSampleCase()); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -223,11 +181,7 @@ 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()); + var response = underTest.portalInvite(getSampleUser()); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -237,11 +191,7 @@ 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")); + var response = underTest.casePendingClosure(getSampleUser(), getSampleCase(), Timestamp.valueOf("2025-01-01 00:00:00.0")); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -251,11 +201,7 @@ 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()); + var response = underTest.caseClosed(getSampleUser(), getSampleCase()); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -265,162 +211,155 @@ 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()); + var response = underTest.caseClosureCancelled(getSampleUser(), getSampleCase()); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); } - @Test - @DisplayName("Should send editing rejection 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); - - 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)); - - 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); - - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); - } - - @Test - @DisplayName("Should send not jointly agreed submission email") - void sendNotJointlyAgreedEmail() throws NotificationClientException { - when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); - - var request = getEditRequest(); - request.setStatus(EditRequestStatus.SUBMITTED); - request.setJointlyAgreed(false); - - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingNotJointlyAgreed("group-email@example.com", request); - - 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") +// void sendEditRejectionEmail() throws NotificationClientException { +// EditRequest mockEditRequest = mock(EditRequest.class); +// when() +// +// +// EmailResponse response = underTest.sendEmailAboutEditingRequest(params) +// .orElseThrow(() -> new EmailFailedToSendException("Something went wrong")); +// +// assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); +// assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); +// assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); +// +// ArgumentCaptor emailAddressCaptor = ArgumentCaptor.forClass(String.class); +// ArgumentCaptor templateCaptor = ArgumentCaptor.forClass(String.class); +// ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(String.class); +// +// verify(mockGovNotifyClient, times(1)).sendEmail( +// templateCaptor.capture(), +// emailAddressCaptor.capture(), +// variablesCaptor.capture(), +// referenceCaptor.capture() +// ); +// +// // Gov Notify template ID +// assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); +// assertThat(emailAddressCaptor.getValue()).isEqualTo(params.getToEmailAddress()); +// assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(params.getCaseReference()); +// assertThat(variablesCaptor.getValue().get("defendant_names")).isEqualTo(params.getDefendantName()); +// assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(params.getCourtName()); +// assertThat(variablesCaptor.getValue().get("edit_summary")).isEqualTo(params.getEditSummary()); +// assertThat(variablesCaptor.getValue().get("edit_count")) +// .isEqualTo(params.getNumberOfRequestedEditInstructions()); +// assertThat(variablesCaptor.getValue().get("jointly_agreed")).isEqualTo("Yes"); +// assertThat(variablesCaptor.getValue().get("rejection_reason")).isEqualTo(params.getRejectionReason()); +// assertThat(variablesCaptor.getValue().get("portal_link")).isEqualTo("http://localhost:8080"); +// } @DisplayName(("Should fail to send recording ready email")) @Test void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.recordingReady(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.recordingReady(getSampleUser(), getSampleCase()) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); } @DisplayName(("Should fail to send recording edited email")) @Test void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { - var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) .thenThrow(mock(NotificationClientException.class)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.recordingEdited(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.recordingEdited(getSampleUser(), getSampleCase()) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); } @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)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.portalInvite(getUser())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.portalInvite(getSampleUser()) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().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)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.casePendingClosure(getUser(), - getCase(), - Timestamp.valueOf("2025-01-01 00:00:00.0"))) + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.casePendingClosure( + getSampleUser(), + getSampleCase(), + 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: " + getSampleUser().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)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.caseClosed(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.caseClosed(getSampleUser(), getSampleCase()) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().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)); - var message = assertThrows(EmailFailedToSendException.class, - () -> govNotify.caseClosureCancelled(getUser(), getCase())).getMessage(); + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.caseClosureCancelled(getSampleUser(), getSampleCase()) + ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().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 user = getSampleUser(); + var response = underTest.emailVerification(user.getEmail(), user.getFirstName(), user.getLastName(), "123456"); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -430,14 +369,13 @@ 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)); - var user = getUser(); + var user = getSampleUser(); var message = assertThrows( EmailFailedToSendException.class, - () -> govNotify.emailVerification( + () -> underTest.emailVerification( user.getEmail(), user.getFirstName(), user.getLastName(), @@ -445,68 +383,25 @@ void shouldFailToSendEmailVerificationEmail() throws NotificationClientException ) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getUser().getEmail()); - } - - @Test - @DisplayName("Should fail to send editing rejection email") - void shouldFailToSendEditingRejectionEmail() 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); - - 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(); - - assertThat(message).isEqualTo("Failed to send email to: " + getCase().getCourt().getGroupEmail()); - } + assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + } + +// @Test +// @DisplayName("Should fail to send editing email") +// void shouldFailToSendEditingEmail() throws NotificationClientException { +// when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) +// .thenThrow(mock(NotificationClientException.class)); +// +// EditEmailParameters params = getSampleEditEmailParams(EditRequestStatus.REJECTED, true); +// +// var message = assertThrows( +// EmailFailedToSendException.class, +// () -> underTest.sendEmailAboutEditingRequest(params) +// ) +// .getMessage(); +// +// assertThat(message).isEqualTo("Failed to send email to: " + getSampleCase().getCourt().getGroupEmail()); +// } @DisplayName("Should prefer alternative email when it ends with .cjsm.net") @Test @@ -517,8 +412,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 +427,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 +442,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 +457,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 +472,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 +487,44 @@ 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; + } + +// private static EditEmailParameters getSampleEditEmailParams(EditRequestStatus editRequestStatus, +// boolean jointlyAgreed) { +// Case testCase = getSampleCase(); +// return EditEmailParameters.builder() +// .toEmailAddress(testCase.getCourt().getGroupEmail()) +// .caseReference(testCase.getReference()) +// .courtName(testCase.getCourt().getName()) +// .witnessName(getSampleUser().getFirstName()) +// .defendantName(getSampleUser().getFullName()) +// .editSummary("my edits") +// .editRequestStatus(editRequestStatus) +// .numberOfRequestedEditInstructions(3) +// .jointlyAgreed(jointlyAgreed) +// .rejectionReason("REJECTION REASON") +// .build(); +// } } From 1b1f08d6e929ded6af2d88a860c9e2ba57113c83 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 12:52:37 +0100 Subject: [PATCH 05/37] Main edit services --- .../services/EditNotificationService.java | 100 ++------------- .../preapi/services/EditRequestService.java | 18 +-- .../reform/preapi/utils/StringTools.java | 9 +- .../services/EditNotificationServiceTest.java | 114 ++++++++++++++---- .../services/EditRequestServiceTest.java | 6 +- 5 files changed, 108 insertions(+), 139 deletions(-) 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 64aaa8b83a..22d1e37797 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 @@ -1,42 +1,23 @@ package uk.gov.hmcts.reform.preapi.services; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; -import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.email.EmailServiceFactory; -import uk.gov.hmcts.reform.preapi.email.govnotify.templates.EditEmailParameters; import uk.gov.hmcts.reform.preapi.entities.Booking; import uk.gov.hmcts.reform.preapi.entities.EditRequest; -import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.entities.ShareBooking; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.exception.NotFoundException; -import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; -import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; - -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.utils.StringTools.isBlankString; @Service @Slf4j public class EditNotificationService { private final EmailServiceFactory emailServiceFactory; - private final RecordingRepository recordingRepository; @Autowired - public EditNotificationService( - final EmailServiceFactory emailServiceFactory, - final RecordingRepository recordingRepository) { + public EditNotificationService(final EmailServiceFactory emailServiceFactory) { this.emailServiceFactory = emailServiceFactory; - this.recordingRepository = recordingRepository; } @Transactional @@ -48,87 +29,22 @@ public void sendNotifications(Booking booking) { } public void editRequestStatusWasUpdated(EditRequest editRequest) { + if (editRequest == null) { + log.error("Tried to send email notification about a null edit request"); + return; + } + if (editRequest.getStatus() != EditRequestStatus.SUBMITTED - && editRequest.getStatus() != EditRequestStatus.REJECTED - && editRequest.getStatus() != EditRequestStatus.APPROVED) { + && editRequest.getStatus() != EditRequestStatus.REJECTED) { // For edited recordings, notification is sent by RecordingListener instead log.info("No notification needed for edit request status {}", editRequest.getStatus().name()); } - Recording sourceRecording = recordingRepository.findById(editRequest.getSourceRecordingId()) - .orElseThrow(() -> new NotFoundException("Unable to send email: could not find source recording " - + editRequest.getSourceRecordingId())); - - if (sourceRecording.getEditRequest() == null) { - throw new ResourceInWrongStateException(format( - "Unable to send email: source recording %s did not have an active edit request", - sourceRecording.getId() - )); - } - - EditEmailParameters editEmailParameters = getEmailParameters(sourceRecording); - try { - emailServiceFactory.getEnabledEmailService().sendEmailAboutEditingRequest(editEmailParameters); + emailServiceFactory.getEnabledEmailService().sendEmailAboutEditingRequest(editRequest); } catch (Exception e) { log.error("Error sending email on edit request submission: {}", e.getMessage()); } } - private @NotNull EditEmailParameters getEmailParameters(Recording outputRecording) { - EditRequest editRequest = outputRecording.getEditRequest(); - if (editRequest == null) { - throw new NotFoundException("No edit request found when trying to send notification"); - } - - Booking booking = outputRecording.getCaptureSession().getBooking(); - if (booking == null) { - throw new NotFoundException("No booking found when trying to send edit notification"); - } - - String courtEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); - - if (isBlankString(courtEmailAddress)) { - log.error( - "Court {} does not have a group email for sending edit request submission email for request: {}", - booking.getCaseId().getCourt().getName(), outputRecording.getEditRequest().getId() - ); - } - - String editSummary = generateEditSummary(EditRequestDTO.toDTO(editRequest.getEditCutInstructions())); - - return EditEmailParameters.builder() - .toEmailAddress(courtEmailAddress) - .caseReference(booking.getCaseId().getReference()) - .witnessName(booking.getWitnessName()) - .defendantName(booking.getDefendantName()) - .courtName(booking.getCaseId().getCourt().getName()) - .editSummary(editSummary) - .editRequestStatus(editRequest.getStatus()) - .numberOfRequestedEditInstructions(editRequest.getEditCutInstructions().size()) - .jointlyAgreed(editRequest.getJointlyAgreed()) - .rejectionReason(editRequest.getRejectionReason()) - .build(); - } - - private String generateEditSummary(List editInstructions) { - StringJoiner summary = new StringJoiner(""); - for (int i = 0; i < editInstructions.size(); i++) { - EditCutInstructionsDTO 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(EditCutInstructionsDTO 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/services/EditRequestService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java index 62e5f1a854..832777eeea 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 @@ -118,7 +118,10 @@ public UpsertResult upsert(CreateEditRequestDTO dto) { User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); Pair result = editRequestCrudService.upsert(dto, sourceRecording, user); - notifyOnUpdatedRequest(dto, result); + if (result.getFirst().equals(UpsertResult.UPDATED)) { + editNotificationService.editRequestStatusWasUpdated(result.getSecond()); + } + return result.getFirst(); } @@ -166,19 +169,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/utils/StringTools.java b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java index cc57c82cc5..79ca207b24 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java @@ -1,13 +1,10 @@ package uk.gov.hmcts.reform.preapi.utils; -import java.time.Duration; -import java.time.LocalTime; - import static java.lang.String.format; -public class StringTools { - public static boolean isBlankString(String string) { - return string == null || string.trim().isEmpty(); +public final class StringTools { + private StringTools() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } public static String formatTimeAsString(Integer time) { 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..db35c925fa 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 @@ -3,6 +3,7 @@ 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,15 +16,19 @@ 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.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -64,6 +69,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 +78,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 +141,84 @@ 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 when edit request is approved, completed, or non-submission status") @Test - void testEditRequestNotificationsWhenNoCourtEmail() { - court.setGroupEmail(null); + void testNoEditRequestNotificationForOtherStatusTypes() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.COMPLETE); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.APPROVED); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); + + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + underTest.editRequestStatusWasUpdated(mockEditRequest); + verifyNoInteractions(emailService); - underTest.onEditRequestSubmitted(mockEditRequest); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + underTest.editRequestStatusWasUpdated(mockEditRequest); verifyNoInteractions(emailService); - underTest.onEditRequestRejected(mockEditRequest); + 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); } } 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..5f2596deb5 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 @@ -410,7 +410,7 @@ void upsertOnSubmittedJointlyAgreed() { 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); + verify(editNotificationService, times(1)).editRequestStatusWasUpdated(mockEditRequest); } @Test @@ -427,7 +427,7 @@ void upsertOnSubmittedNotJointlyAgreed() { 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); + verify(editNotificationService, times(1)).editRequestStatusWasUpdated(mockEditRequest); } @Test @@ -447,7 +447,7 @@ void upsertOnRejected() { 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); + verify(editNotificationService, times(1)).editRequestStatusWasUpdated(mockEditRequest); } @DisplayName("Should throw an exception if edit instructions have unsafe data") From c5da0934e8d20fd404013e24ab7e96f91dd19118 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 13:20:46 +0100 Subject: [PATCH 06/37] Tidy up --- .../templates/EditEmailParameters.java | 29 +-- .../preapi/email/govnotify/GovNotifyTest.java | 165 ++++++++++-------- .../templates/EditEmailParametersTest.java | 23 +-- 3 files changed, 120 insertions(+), 97 deletions(-) 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 index 841c86bb71..2f17c1a4e5 100644 --- 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 @@ -28,36 +28,35 @@ public class EditEmailParameters { private Map emailParameters; public EditEmailParameters() { + // This constructor is intentionally empty. It is only needed for mocks in tests. } public EditEmailParameters(EditRequest editRequest, String portalUrl) { - validateEditRequestIsOkayForNotification(editRequest); - if (portalUrl == null) { throw new BadRequestException("Portal URL is missing"); } - Booking booking = editRequest.getSourceRecording().getCaptureSession().getBooking(); + validateEditRequestIsOkayForNotification(editRequest); - List requestInstructions = - EditInstructions.fromJson(editRequest.getEditInstruction()).getRequestedInstructions(); + Booking booking = validateAndRetrieveBooking(editRequest); + List requestInstructions = validateAndRetrieveRequestInstructions(editRequest); String summary = generateEditSummary(requestInstructions); - this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); - this.editRequestStatus = editRequest.getStatus(); - this.jointlyAgreed = editRequest.getJointlyAgreed(); this.emailParameters = Map.of( - "rejection_reason", editRequest.getRejectionReason(), + "edit_summary", summary, + "rejection_reason", editRequest.getRejectionReason() == null ? "" : editRequest.getRejectionReason(), "jointly_agreed", editRequest.getJointlyAgreed() ? "Yes" : "No", "case_reference", booking.getCaseId().getReference(), "court_name", booking.getCaseId().getCourt().getName(), "witness_name", booking.getWitnessName(), "defendant_names", booking.getDefendantName(), - "edit_summary", summary, "portal_link", portalUrl, "edit_count", requestInstructions.size() ); + this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); + this.editRequestStatus = editRequest.getStatus(); + this.jointlyAgreed = editRequest.getJointlyAgreed(); } private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { @@ -74,7 +73,9 @@ private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { 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: " @@ -102,13 +103,20 @@ private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { 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.fromJson(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 : " @@ -121,6 +129,7 @@ private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { throw new BadRequestException("No instructions found when trying to send edit notification for edit ID: " + editRequest.getId()); } + return requestInstructions; } private String generateEditSummary(List editInstructions) { diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index a2ed803d20..73a2fa99f0 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -8,6 +8,8 @@ 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.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.govnotify.templates.CaseClosed; @@ -16,22 +18,33 @@ 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.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.entities.User; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; 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.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.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.when; @SpringBootTest(classes = GovNotify.class) @@ -64,12 +77,38 @@ public class GovNotifyTest { private GovNotify underTest; + private EditRequest editRequest; + @BeforeEach void setUp() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); underTest = new GovNotify("http://localhost:8080", mockGovNotifyClient); + + Case testCase = getSampleCase(); + + Booking booking = mock(Booking.class); + when(booking.getCaseId()).thenReturn(testCase); + 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") @@ -190,8 +229,11 @@ void shouldSendPortalInviteEmail() throws NotificationClientException { @DisplayName(("Should send case pending closure email")) @Test - void shouldSendCasePendingClosureEmail() throws NotificationClientException { - var response = underTest.casePendingClosure(getSampleUser(), getSampleCase(), Timestamp.valueOf("2025-01-01 00:00:00.0")); + void shouldSendCasePendingClosureEmail() { + var response = underTest.casePendingClosure( + getSampleUser(), getSampleCase(), + Timestamp.valueOf("2025-01-01 00:00:00.0") + ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -221,44 +263,41 @@ void shouldSendCaseClosureCancelledEmail() throws NotificationClientException { @Captor private ArgumentCaptor> variablesCaptor; -// @Test -// @DisplayName("Should send editing rejection email") -// void sendEditRejectionEmail() throws NotificationClientException { -// EditRequest mockEditRequest = mock(EditRequest.class); -// when() -// -// -// EmailResponse response = underTest.sendEmailAboutEditingRequest(params) -// .orElseThrow(() -> new EmailFailedToSendException("Something went wrong")); -// -// assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); -// assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); -// assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); -// -// ArgumentCaptor emailAddressCaptor = ArgumentCaptor.forClass(String.class); -// ArgumentCaptor templateCaptor = ArgumentCaptor.forClass(String.class); -// ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(String.class); -// -// verify(mockGovNotifyClient, times(1)).sendEmail( -// templateCaptor.capture(), -// emailAddressCaptor.capture(), -// variablesCaptor.capture(), -// referenceCaptor.capture() -// ); -// -// // Gov Notify template ID -// assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); -// assertThat(emailAddressCaptor.getValue()).isEqualTo(params.getToEmailAddress()); -// assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(params.getCaseReference()); -// assertThat(variablesCaptor.getValue().get("defendant_names")).isEqualTo(params.getDefendantName()); -// assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(params.getCourtName()); -// assertThat(variablesCaptor.getValue().get("edit_summary")).isEqualTo(params.getEditSummary()); -// assertThat(variablesCaptor.getValue().get("edit_count")) -// .isEqualTo(params.getNumberOfRequestedEditInstructions()); -// assertThat(variablesCaptor.getValue().get("jointly_agreed")).isEqualTo("Yes"); -// assertThat(variablesCaptor.getValue().get("rejection_reason")).isEqualTo(params.getRejectionReason()); -// assertThat(variablesCaptor.getValue().get("portal_link")).isEqualTo("http://localhost:8080"); -// } + @Test + @DisplayName("Should send editing email") + void sendEditRejectionEmail() throws NotificationClientException { + 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"); + + ArgumentCaptor emailAddressCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor templateCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(String.class); + + verify(mockGovNotifyClient, times(1)).sendEmail( + templateCaptor.capture(), + emailAddressCaptor.capture(), + variablesCaptor.capture(), + referenceCaptor.capture() + ); + + // Gov Notify template ID + assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); + Case expectedCase = getSampleCase(); + assertThat(emailAddressCaptor.getValue()).isEqualTo(expectedCase.getCourt().getGroupEmail()); + assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(expectedCase.getReference()); + assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(expectedCase.getCourt().getName()); + assertThat(variablesCaptor.getValue().get("defendant_names")).isEqualTo("Defendant Name"); + assertThat(variablesCaptor.getValue().get("edit_summary")).isNotNull(); + assertThat(variablesCaptor.getValue().get("edit_count")) + .isEqualTo(1); + assertThat(variablesCaptor.getValue().get("jointly_agreed")).isEqualTo("Yes"); + assertThat(variablesCaptor.getValue().get("rejection_reason")).isEqualTo(""); + assertThat(variablesCaptor.getValue().get("portal_link")).isEqualTo("http://localhost:8080"); + } @DisplayName(("Should fail to send recording ready email")) @Test @@ -386,22 +425,20 @@ void shouldFailToSendEmailVerificationEmail() throws NotificationClientException assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); } -// @Test -// @DisplayName("Should fail to send editing email") -// void shouldFailToSendEditingEmail() throws NotificationClientException { -// when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) -// .thenThrow(mock(NotificationClientException.class)); -// -// EditEmailParameters params = getSampleEditEmailParams(EditRequestStatus.REJECTED, true); -// -// var message = assertThrows( -// EmailFailedToSendException.class, -// () -> underTest.sendEmailAboutEditingRequest(params) -// ) -// .getMessage(); -// -// assertThat(message).isEqualTo("Failed to send email to: " + getSampleCase().getCourt().getGroupEmail()); -// } + @Test + @DisplayName("Should fail to send editing email") + void shouldFailToSendEditingEmail() throws NotificationClientException { + when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) + .thenThrow(mock(NotificationClientException.class)); + + var message = assertThrows( + EmailFailedToSendException.class, + () -> underTest.sendEmailAboutEditingRequest(editRequest) + ).getMessage(); + + assertThat(message).isEqualTo("Failed to send email to: " + + getSampleCase().getCourt().getGroupEmail()); + } @DisplayName("Should prefer alternative email when it ends with .cjsm.net") @Test @@ -511,20 +548,4 @@ private static Case getSampleCase() { return forCase; } -// private static EditEmailParameters getSampleEditEmailParams(EditRequestStatus editRequestStatus, -// boolean jointlyAgreed) { -// Case testCase = getSampleCase(); -// return EditEmailParameters.builder() -// .toEmailAddress(testCase.getCourt().getGroupEmail()) -// .caseReference(testCase.getReference()) -// .courtName(testCase.getCourt().getName()) -// .witnessName(getSampleUser().getFirstName()) -// .defendantName(getSampleUser().getFullName()) -// .editSummary("my edits") -// .editRequestStatus(editRequestStatus) -// .numberOfRequestedEditInstructions(3) -// .jointlyAgreed(jointlyAgreed) -// .rejectionReason("REJECTION REASON") -// .build(); -// } } 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 index f3d43f90b7..2e4b8ae4ff 100644 --- 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 @@ -74,26 +74,19 @@ public void setUp() { instructionsList = Arrays.asList(instruction1, instruction2, instruction3); - EditInstructions defaultEditInstructions = new EditInstructions(instructionsList, new ArrayList<>()); - String editInstructionsAsJson = JsonUtils.toJson(defaultEditInstructions); - CreateEditRequestDTO 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() { -// EditRequest editRequest = null; -// assertThrows(NotFoundException.class, () -> new EditEmailParameters(editRequest)); -// } - @Test @DisplayName("Cannot create email parameters with null recording") void cannotCreateEmailParametersFromNullRecording() { @@ -229,28 +222,28 @@ public void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { assertThat(paramsMap).isNotNull(); assertThat(paramsMap).isNotEmpty(); - assertThat(paramsMap.get("rejection_reason")).isEqualTo(null); + assertThat(paramsMap.get("rejection_reason")).isEqualTo(""); assertThat(paramsMap.get("jointly_agreed")).isEqualTo("Yes"); assertThat(paramsMap.get("case_reference")).isEqualTo(mockCase.getReference()); assertThat(paramsMap.get("court_name")).isEqualTo(mockCourt.getName()); assertThat(paramsMap.get("witness_name")).isEqualTo("Witness Name"); assertThat(paramsMap.get("defendant_names")).isEqualTo("Defendant Name"); assertThat(paramsMap.get("edit_count")).isEqualTo(3); - assertThat(paramsMap.get("portal_link")).isEqualTo("test.portal.url"); + assertThat(paramsMap.get("portal_link")).isEqualTo("http://localhost:8080"); assertThat(paramsMap.get("edit_summary")).isEqualTo(""" - Edit 1: + Edit 1:\s Start time: 00:00:05 End time: 00:00:10 Time Removed: 00:00:05 Reason: first reason - Edit 2: + Edit 2:\s Start time: 00:00:23 End time: 00:00:26 Time Removed: 00:00:03 Reason: second reason - Edit 3: + Edit 3:\s Start time: 00:00:31 End time: 00:00:32 Time Removed: 00:00:01 From d5ac0320edbe2c4ec78b2d57205435fe7f1fa8a3 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 13:30:50 +0100 Subject: [PATCH 07/37] Checkstyle --- .../java/uk/gov/hmcts/reform/preapi/email/GovNotifyFT.java | 2 -- 1 file changed, 2 deletions(-) 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 7094d7a905..cd5c364c33 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 @@ -8,7 +8,6 @@ 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.email.govnotify.templates.EditEmailParameters; import uk.gov.hmcts.reform.preapi.entities.Booking; import uk.gov.hmcts.reform.preapi.entities.CaptureSession; import uk.gov.hmcts.reform.preapi.entities.Case; @@ -23,7 +22,6 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; From 29799480debb137d56c5830d71425a8c32bef810 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 13:40:21 +0100 Subject: [PATCH 08/37] Correction --- .../hmcts/reform/preapi/services/EditNotificationService.java | 1 + 1 file changed, 1 insertion(+) 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 22d1e37797..8f3d40d165 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 @@ -38,6 +38,7 @@ public void editRequestStatusWasUpdated(EditRequest editRequest) { && editRequest.getStatus() != EditRequestStatus.REJECTED) { // For edited recordings, notification is sent by RecordingListener instead log.info("No notification needed for edit request status {}", editRequest.getStatus().name()); + return; } try { From 12be299e5550218c59004ef25e534c30700201d4 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 14:17:55 +0100 Subject: [PATCH 09/37] Fixed test --- .../reform/preapi/email/GovNotifyFT.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 cd5c364c33..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 @@ -299,14 +299,14 @@ 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 @@ -334,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 @@ -354,6 +354,7 @@ Defendant name(s): First Last @SuppressWarnings("LineLength") void editingRejectionEmail() { when(editRequest.getJointlyAgreed()).thenReturn(true); + when(editRequest.getRejectionReason()).thenReturn("REJECTION REASON"); when(editRequest.getStatus()).thenReturn(EditRequestStatus.REJECTED); var response = client.sendEmailAboutEditingRequest(editRequest).orElseThrow(); @@ -370,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 From 55bab0d17144cf80eb54ff56a6a21aace07554c8 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 14:40:16 +0100 Subject: [PATCH 10/37] Absorb error when constructing parameters for edit email --- .../reform/preapi/email/govnotify/GovNotify.java | 8 +++++++- .../preapi/email/govnotify/GovNotifyTest.java | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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 f95560f2bc..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 @@ -162,7 +162,13 @@ public EmailResponse emailVerification(String email, String firstName, String la @Override public Optional sendEmailAboutEditingRequest(EditRequest editRequest) { - EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + EditEmailParameters editEmailParameters; + try { + editEmailParameters = new EditEmailParameters(editRequest, portalUrl); + } catch (Exception e) { + log.error("Failed to create email parameters for edit request submission: {}", e.getMessage()); + return Optional.empty(); + } EditRequestEmailTemplate template = new EditRequestEmailTemplate(editEmailParameters); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index 73a2fa99f0..1ae3971a99 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +46,7 @@ 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) @@ -329,6 +331,17 @@ void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); } + @DisplayName(("Should absorb error thrown by email parameters creation")) + @Test + void shouldAbsorbErrorThrownByEmailParameters() { + editRequest.setSourceRecording(null); // will cause exception + + Optional emailResponse = underTest.sendEmailAboutEditingRequest(editRequest); + + assertThat(emailResponse).isEmpty(); + verifyNoInteractions(mockGovNotifyClient); + } + @DisplayName(("Should fail to send portal invite email")) @Test void shouldFailToSendPortalInviteEmail() throws NotificationClientException { From 1b0e83a5ae5412986325079f44cc683cbf7a64dc Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 19:12:40 +0100 Subject: [PATCH 11/37] Extra unit test to document current behaviour: new edit requests do not trigger emails --- .../services/EditRequestServiceTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 5f2596deb5..b08c437917 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 @@ -52,6 +52,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; @@ -393,6 +394,26 @@ void upsertEditInstructionsWithEmptyFile() { ); } + @Test + @DisplayName("*New* edit request does not trigger email on submission (to review: maybe it should)") + void newEditRequestDoesNotTriggerEmailOnSubmission() { + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + when(dto.getJointlyAgreed()).thenReturn(true); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + // Important bit here: CRUD service returns CREATED for new edit request + when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) + .thenReturn(Pair.of(UpsertResult.CREATED, mockEditRequest)); + + UpsertResult response = underTest.upsert(dto); + assertThat(response).isEqualTo(UpsertResult.CREATED); + + verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); + verify(mockAuth, times(1)).getAppAccess(); + verify(editRequestCrudService, times(1)).upsert(dto, mockRecording, courtClerkUser); + verifyNoInteractions(editNotificationService); + } + @Test @DisplayName("Should trigger request submission jointly agreed email on submission") void upsertOnSubmittedJointlyAgreed() { From c49120282c5459909cc2bf3c4011971f8e21ede4 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 19:49:09 +0100 Subject: [PATCH 12/37] Fix code quality issues --- .../templates/EditEmailParameters.java | 4 - .../preapi/email/govnotify/GovNotifyTest.java | 75 ++++++----- .../templates/EditEmailParametersTest.java | 121 ++++++++---------- .../EditRequestEmailTemplateTest.java | 14 +- .../reform/preapi/utils/StringToolsTest.java | 38 ++++++ 5 files changed, 138 insertions(+), 114 deletions(-) create mode 100644 src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java 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 index 2f17c1a4e5..bb717f4b34 100644 --- 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 @@ -27,10 +27,6 @@ public class EditEmailParameters { private String toEmailAddress; // court group email private Map emailParameters; - public EditEmailParameters() { - // This constructor is intentionally empty. It is only needed for mocks in tests. - } - public EditEmailParameters(EditRequest editRequest, String portalUrl) { if (portalUrl == null) { throw new BadRequestException("Portal URL is missing"); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index 1ae3971a99..c493ac2efd 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -81,6 +81,9 @@ public class GovNotifyTest { 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())) @@ -88,10 +91,8 @@ void setUp() throws NotificationClientException { underTest = new GovNotify("http://localhost:8080", mockGovNotifyClient); - Case testCase = getSampleCase(); - Booking booking = mock(Booking.class); - when(booking.getCaseId()).thenReturn(testCase); + when(booking.getCaseId()).thenReturn(sampleCase); when(booking.getWitnessName()).thenReturn("Witness Name"); when(booking.getDefendantName()).thenReturn("Defendant Name"); @@ -201,8 +202,8 @@ void shouldCreateEmailServiceFactory() { @DisplayName(("Should send recording ready email")) @Test - void shouldSendRecordingReadyEmail() throws NotificationClientException { - var response = underTest.recordingReady(getSampleUser(), getSampleCase()); + void shouldSendRecordingReadyEmail() { + var response = underTest.recordingReady(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -212,7 +213,7 @@ void shouldSendRecordingReadyEmail() throws NotificationClientException { @DisplayName(("Should send recording edited email")) @Test void shouldSendRecordingEditedEmail() { - var response = underTest.recordingEdited(getSampleUser(), getSampleCase()); + var response = underTest.recordingEdited(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -221,8 +222,8 @@ void shouldSendRecordingEditedEmail() { @DisplayName(("Should send portal invite email")) @Test - void shouldSendPortalInviteEmail() throws NotificationClientException { - var response = underTest.portalInvite(getSampleUser()); + void shouldSendPortalInviteEmail() { + var response = underTest.portalInvite(sampleUser); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -233,7 +234,7 @@ void shouldSendPortalInviteEmail() throws NotificationClientException { @Test void shouldSendCasePendingClosureEmail() { var response = underTest.casePendingClosure( - getSampleUser(), getSampleCase(), + sampleUser, sampleCase, Timestamp.valueOf("2025-01-01 00:00:00.0") ); @@ -244,8 +245,8 @@ void shouldSendCasePendingClosureEmail() { @DisplayName(("Should send case closed email")) @Test - void shouldSendCaseClosedEmail() throws NotificationClientException { - var response = underTest.caseClosed(getSampleUser(), getSampleCase()); + void shouldSendCaseClosedEmail() { + var response = underTest.caseClosed(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -254,8 +255,8 @@ void shouldSendCaseClosedEmail() throws NotificationClientException { @DisplayName(("Should send case closure cancelled email")) @Test - void shouldSendCaseClosureCancelledEmail() throws NotificationClientException { - var response = underTest.caseClosureCancelled(getSampleUser(), getSampleCase()); + void shouldSendCaseClosureCancelledEmail() { + var response = underTest.caseClosureCancelled(sampleUser, sampleCase); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -288,10 +289,9 @@ void sendEditRejectionEmail() throws NotificationClientException { // Gov Notify template ID assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); - Case expectedCase = getSampleCase(); - assertThat(emailAddressCaptor.getValue()).isEqualTo(expectedCase.getCourt().getGroupEmail()); - assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(expectedCase.getReference()); - assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(expectedCase.getCourt().getName()); + assertThat(emailAddressCaptor.getValue()).isEqualTo(sampleCase.getCourt().getGroupEmail()); + assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(sampleCase.getReference()); + assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(sampleCase.getCourt().getName()); assertThat(variablesCaptor.getValue().get("defendant_names")).isEqualTo("Defendant Name"); assertThat(variablesCaptor.getValue().get("edit_summary")).isNotNull(); assertThat(variablesCaptor.getValue().get("edit_count")) @@ -310,10 +310,10 @@ void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { var message = assertThrows( EmailFailedToSendException.class, - () -> underTest.recordingReady(getSampleUser(), getSampleCase()) + () -> underTest.recordingReady(sampleUser, sampleCase) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send recording edited email")) @@ -325,10 +325,10 @@ void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { var message = assertThrows( EmailFailedToSendException.class, - () -> underTest.recordingEdited(getSampleUser(), getSampleCase()) + () -> underTest.recordingEdited(sampleUser, sampleCase) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should absorb error thrown by email parameters creation")) @@ -351,10 +351,10 @@ void shouldFailToSendPortalInviteEmail() throws NotificationClientException { var message = assertThrows( EmailFailedToSendException.class, - () -> underTest.portalInvite(getSampleUser()) + () -> underTest.portalInvite(sampleUser) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case pending closure email")) @@ -367,14 +367,14 @@ void shouldFailToSendCasePendingClosureEmail() throws NotificationClientExceptio var message = assertThrows( EmailFailedToSendException.class, () -> underTest.casePendingClosure( - getSampleUser(), - getSampleCase(), + sampleUser, + sampleCase, Timestamp.valueOf("2025-01-01 00:00:00.0") ) ) .getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case closed email")) @@ -386,10 +386,10 @@ void shouldFailToSendCaseClosedEmail() throws NotificationClientException { var message = assertThrows( EmailFailedToSendException.class, - () -> underTest.caseClosed(getSampleUser(), getSampleCase()) + () -> underTest.caseClosed(sampleUser, sampleCase) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should fail to send case closure cancelled email")) @@ -401,17 +401,17 @@ void shouldFailToSendCaseClosureCancelledEmail() throws NotificationClientExcept var message = assertThrows( EmailFailedToSendException.class, - () -> underTest.caseClosureCancelled(getSampleUser(), getSampleCase()) + () -> underTest.caseClosureCancelled(sampleUser, sampleCase) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @DisplayName(("Should send email verification email")) @Test void shouldSendEmailVerificationEmail() { - var user = getSampleUser(); - var response = underTest.emailVerification(user.getEmail(), user.getFirstName(), user.getLastName(), "123456"); + var response = underTest.emailVerification(sampleUser.getEmail(), sampleUser.getFirstName(), + sampleUser.getLastName(), "123456"); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -424,18 +424,17 @@ void shouldFailToSendEmailVerificationEmail() throws NotificationClientException when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) .thenThrow(mock(NotificationClientException.class)); - var user = getSampleUser(); var message = assertThrows( EmailFailedToSendException.class, () -> underTest.emailVerification( - user.getEmail(), - user.getFirstName(), - user.getLastName(), + sampleUser.getEmail(), + sampleUser.getFirstName(), + sampleUser.getLastName(), "123456" ) ).getMessage(); - assertThat(message).isEqualTo("Failed to send email to: " + getSampleUser().getEmail()); + assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @Test @@ -450,7 +449,7 @@ void shouldFailToSendEditingEmail() throws NotificationClientException { ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " - + getSampleCase().getCourt().getGroupEmail()); + + sampleCase.getCourt().getGroupEmail()); } @DisplayName("Should prefer alternative email when it ends with .cjsm.net") 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 index 2e4b8ae4ff..ffc0625562 100644 --- 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 @@ -3,8 +3,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -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; import uk.gov.hmcts.reform.preapi.entities.Booking; @@ -27,24 +25,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SpringBootTest(classes = EditEmailParameters.class) -public class EditEmailParametersTest { +class EditEmailParametersTest { - @MockitoBean private Recording mockRecording; - - @MockitoBean private CaptureSession mockCaptureSession; - - @MockitoBean private Booking mockBooking; - - @MockitoBean private Court mockCourt; - - @MockitoBean private Case mockCase; private final UUID editRequestId = UUID.randomUUID(); @@ -54,20 +43,26 @@ public class EditEmailParametersTest { private final String portalUrl = "http://localhost:8080"; @BeforeEach - public void setUp() { - when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); - when(mockCaptureSession.getBooking()).thenReturn(mockBooking); - + 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"); @@ -157,31 +152,27 @@ void cannotCreateEmailParametersIfPortalUrlIsNotSet() { @Test @DisplayName("Should accurately create edit email parameters for REJECTED edit") - public void shouldAccuratelyCreateEmailParameters() { + void shouldAccuratelyCreateEmailParameters() { editRequest.setStatus(EditRequestStatus.REJECTED); editRequest.setRejectionReason("Rejected Reason"); editRequest.setJointlyAgreed(false); EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); - assertThat(editEmailParameters.getJointlyAgreed()).isEqualTo(false); + assertThat(editEmailParameters.getJointlyAgreed()).isFalse(); Map paramsMap = editEmailParameters.getEmailParameters(); - assertThat(paramsMap).isNotNull(); assertThat(paramsMap).isNotEmpty(); - assertThat(paramsMap.get("rejection_reason")).isEqualTo(editRequest.getRejectionReason()); - assertThat(paramsMap.get("jointly_agreed")).isEqualTo("No"); - assertThat(paramsMap.get("case_reference")).isEqualTo(mockCase.getReference()); - assertThat(paramsMap.get("court_name")).isEqualTo(mockCourt.getName()); - assertThat(paramsMap.get("witness_name")).isEqualTo("Witness Name"); - assertThat(paramsMap.get("defendant_names")).isEqualTo("Defendant Name"); - assertThat(paramsMap.get("edit_count")).isEqualTo(3); - assertThat(paramsMap.get("portal_link")).isEqualTo("http://localhost:8080"); - - Object gotSummary = paramsMap.get("edit_summary"); - assertThat(gotSummary).isNotNull(); - assertThat(gotSummary).isInstanceOf(String.class); + + assertThat(paramsMap).containsEntry("rejection_reason", editRequest.getRejectionReason()); + assertThat(paramsMap).containsEntry("jointly_agreed", "No"); + assertThat(paramsMap).containsEntry("case_reference", mockCase.getReference()); + assertThat(paramsMap).containsEntry("court_name", mockCourt.getName()); + assertThat(paramsMap).containsEntry("witness_name", "Witness Name"); + assertThat(paramsMap).containsEntry("defendant_names", "Defendant Name"); + assertThat(paramsMap).containsEntry("edit_count", 3); + assertThat(paramsMap).containsEntry("portal_link", "http://localhost:8080"); String expectedSummary = """ Edit 1:\s @@ -203,53 +194,53 @@ public void shouldAccuratelyCreateEmailParameters() { Reason: third reason """; - - assertThat(gotSummary).isEqualTo(expectedSummary); + assertThat(paramsMap).containsEntry("edit_summary", expectedSummary); } @Test @DisplayName("Should accurately create edit email parameters for JOINTLY_AGREED edit") - public void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { + void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { editRequest.setStatus(EditRequestStatus.SUBMITTED); editRequest.setRejectionReason(null); editRequest.setJointlyAgreed(true); EditEmailParameters editEmailParameters = new EditEmailParameters(editRequest, portalUrl); - assertThat(editEmailParameters.getJointlyAgreed()).isEqualTo(true); + assertThat(editEmailParameters.getJointlyAgreed()).isTrue(); Map paramsMap = editEmailParameters.getEmailParameters(); - assertThat(paramsMap).isNotNull(); assertThat(paramsMap).isNotEmpty(); - assertThat(paramsMap.get("rejection_reason")).isEqualTo(""); - assertThat(paramsMap.get("jointly_agreed")).isEqualTo("Yes"); - assertThat(paramsMap.get("case_reference")).isEqualTo(mockCase.getReference()); - assertThat(paramsMap.get("court_name")).isEqualTo(mockCourt.getName()); - assertThat(paramsMap.get("witness_name")).isEqualTo("Witness Name"); - assertThat(paramsMap.get("defendant_names")).isEqualTo("Defendant Name"); - assertThat(paramsMap.get("edit_count")).isEqualTo(3); - assertThat(paramsMap.get("portal_link")).isEqualTo("http://localhost:8080"); - assertThat(paramsMap.get("edit_summary")).isEqualTo(""" - 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 - - """); + assertThat(paramsMap).containsEntry("rejection_reason", ""); + assertThat(paramsMap).containsEntry("jointly_agreed", "Yes"); + assertThat(paramsMap).containsEntry("case_reference", mockCase.getReference()); + assertThat(paramsMap).containsEntry("court_name", mockCourt.getName()); + assertThat(paramsMap).containsEntry("witness_name", "Witness Name"); + assertThat(paramsMap).containsEntry("defendant_names", "Defendant Name"); + assertThat(paramsMap).containsEntry("edit_count", 3); + assertThat(paramsMap).containsEntry("portal_link", "http://localhost:8080"); + assertThat(paramsMap).containsEntry( + "edit_summary", """ + 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 + + """ + ); } } 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 index 7333a10fc9..87a0c5d535 100644 --- 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 @@ -17,7 +17,7 @@ public class EditRequestEmailTemplateTest { @Test @DisplayName("Should create REJECTED edit email template") - public void getRejectionEmailTemplate() { + void getRejectionEmailTemplate() { EditEmailParameters parameters = mock(EditEmailParameters.class); when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.REJECTED); when(parameters.getJointlyAgreed()).thenReturn(true); @@ -31,12 +31,12 @@ public void getRejectionEmailTemplate() { 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().get("jointly_agreed")).isEqualTo("Yes"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "Yes"); } @Test @DisplayName("Should create JOINTLY_AGREED edit email template") - public void getJointlyAgreedEmailTemplate() { + void getJointlyAgreedEmailTemplate() { EditEmailParameters parameters = mock(EditEmailParameters.class); when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(parameters.getJointlyAgreed()).thenReturn(true); @@ -48,12 +48,12 @@ public void getJointlyAgreedEmailTemplate() { 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().get("jointly_agreed")).isEqualTo("Yes"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "Yes"); } @Test @DisplayName("Should create NOT_JOINTLY_AGREED edit email template") - public void getNotJointlyAgreedEmailTemplate() { + void getNotJointlyAgreedEmailTemplate() { EditEmailParameters parameters = mock(EditEmailParameters.class); when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(parameters.getJointlyAgreed()).thenReturn(false); @@ -65,12 +65,12 @@ public void getNotJointlyAgreedEmailTemplate() { 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().get("jointly_agreed")).isEqualTo("No"); + assertThat(template.getVariables()).containsEntry("jointly_agreed", "No"); } @Test @DisplayName("Should throw exception if unrecognised email type") - public void throwExceptionIfUnrecognisedEmailType() { + void throwExceptionIfUnrecognisedEmailType() { EditEmailParameters parameters = mock(EditEmailParameters.class); when(parameters.getEditRequestStatus()).thenReturn(EditRequestStatus.COMPLETE); 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..7ae16e63fc --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -0,0 +1,38 @@ +package uk.gov.hmcts.reform.preapi.utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.Assert.assertThrows; + +public class StringToolsTest { + + @Test + @DisplayName("Should parse and render time as strings") + public 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") + public void should_complain_if_time_is_negative() { + assertThrows(IllegalArgumentException.class, () -> StringTools.formatTimeAsString(-3)); + } + + @Test + @DisplayName("Should cope with big time") + public void should_cope_with_big_time() { + String parsedTime = StringTools.formatTimeAsString(3569429); + assertThat(parsedTime).isEqualTo("991:53:29"); + } + + @Test + @DisplayName("Should cope with zero time") + public void should_cope_with_zero_time() { + String parsedTime = StringTools.formatTimeAsString(0); + assertThat(parsedTime).isEqualTo("00:00:00"); + } +} From 5a777f9e7b4acdefb26410d1b4820e4943df236a Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 19:59:13 +0100 Subject: [PATCH 13/37] Corrections --- .../email/govnotify/templates/EditEmailParameters.java | 8 ++++---- .../uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index bb717f4b34..c22d9b4a68 100644 --- 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 @@ -22,10 +22,10 @@ @Getter public class EditEmailParameters { - private Boolean jointlyAgreed; - private EditRequestStatus editRequestStatus; - private String toEmailAddress; // court group email - private Map emailParameters; + 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) { 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 index 7ae16e63fc..5e6668c004 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -26,7 +26,7 @@ public void should_complain_if_time_is_negative() { @DisplayName("Should cope with big time") public void should_cope_with_big_time() { String parsedTime = StringTools.formatTimeAsString(3569429); - assertThat(parsedTime).isEqualTo("991:53:29"); + assertThat(parsedTime).isEqualTo("991:30:29"); } @Test From 461fbe5477fd1b0150bc3bbadc57749f665b83f2 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 11 Jun 2026 20:32:08 +0100 Subject: [PATCH 14/37] More code quality improvements --- .../preapi/email/govnotify/GovNotifyTest.java | 33 +++++++++++-------- .../EditRequestEmailTemplateTest.java | 3 +- .../reform/preapi/utils/StringToolsTest.java | 10 +++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index c493ac2efd..8640e5117c 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -12,6 +12,7 @@ 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; @@ -185,17 +186,20 @@ 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)) + new EmailServiceFactory("nonexistent", true, emailServiceList) ); } @@ -290,15 +294,14 @@ void sendEditRejectionEmail() throws NotificationClientException { // Gov Notify template ID assertThat(templateCaptor.getValue()).isEqualTo("aa2a836f-b6f0-46dc-91e0-1698822c5137"); assertThat(emailAddressCaptor.getValue()).isEqualTo(sampleCase.getCourt().getGroupEmail()); - assertThat(variablesCaptor.getValue().get("case_reference")).isEqualTo(sampleCase.getReference()); - assertThat(variablesCaptor.getValue().get("court_name")).isEqualTo(sampleCase.getCourt().getName()); - assertThat(variablesCaptor.getValue().get("defendant_names")).isEqualTo("Defendant Name"); - assertThat(variablesCaptor.getValue().get("edit_summary")).isNotNull(); - assertThat(variablesCaptor.getValue().get("edit_count")) - .isEqualTo(1); - assertThat(variablesCaptor.getValue().get("jointly_agreed")).isEqualTo("Yes"); - assertThat(variablesCaptor.getValue().get("rejection_reason")).isEqualTo(""); - assertThat(variablesCaptor.getValue().get("portal_link")).isEqualTo("http://localhost:8080"); + Map variables = variablesCaptor.getValue(); + assertThat(variables).containsEntry("case_reference", sampleCase.getReference()); + assertThat(variables).containsEntry("court_name", sampleCase.getCourt().getName()); + assertThat(variables).containsEntry("defendant_names", "Defendant Name"); + assertThat(variables).containsEntry("edit_count", 1); + assertThat(variables).containsEntry("jointly_agreed", "Yes"); + assertThat(variables).containsEntry("rejection_reason", ""); + assertThat(variables).containsEntry("portal_link", "http://localhost:8080"); } @DisplayName(("Should fail to send recording ready email")) @@ -410,8 +413,10 @@ void shouldFailToSendCaseClosureCancelledEmail() throws NotificationClientExcept @DisplayName(("Should send email verification email")) @Test void shouldSendEmailVerificationEmail() { - var response = underTest.emailVerification(sampleUser.getEmail(), sampleUser.getFirstName(), - sampleUser.getLastName(), "123456"); + var response = underTest.emailVerification( + sampleUser.getEmail(), sampleUser.getFirstName(), + sampleUser.getLastName(), "123456" + ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); 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 index 87a0c5d535..e083d53140 100644 --- 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 @@ -1,6 +1,5 @@ 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; @@ -13,7 +12,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class EditRequestEmailTemplateTest { +class EditRequestEmailTemplateTest { @Test @DisplayName("Should create REJECTED edit email template") 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 index 5e6668c004..e4fa3d325e 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -6,11 +6,11 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.assertThrows; -public class StringToolsTest { +class StringToolsTest { @Test @DisplayName("Should parse and render time as strings") - public void 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"); @@ -18,20 +18,20 @@ public void should_parse_and_render_time_as_strings() { @Test @DisplayName("Should complain if time is negative") - public void should_complain_if_time_is_negative() { + void should_complain_if_time_is_negative() { assertThrows(IllegalArgumentException.class, () -> StringTools.formatTimeAsString(-3)); } @Test @DisplayName("Should cope with big time") - public void 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") - public void should_cope_with_zero_time() { + void should_cope_with_zero_time() { String parsedTime = StringTools.formatTimeAsString(0); assertThat(parsedTime).isEqualTo("00:00:00"); } From bce2f2ed34e3ff980ce70f0de99f621fc8efc0dc Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Fri, 12 Jun 2026 10:07:43 +0100 Subject: [PATCH 15/37] More test coverage --- .../templates/EditEmailParameters.java | 7 +++-- .../templates/EditEmailParametersTest.java | 30 ++++++++++++++++++- .../services/EditNotificationServiceTest.java | 8 +++++ .../reform/preapi/utils/StringToolsTest.java | 1 - 4 files changed, 41 insertions(+), 5 deletions(-) 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 index c22d9b4a68..6554af62a4 100644 --- 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 @@ -39,10 +39,11 @@ public EditEmailParameters(EditRequest editRequest, String portalUrl) { List requestInstructions = validateAndRetrieveRequestInstructions(editRequest); String summary = generateEditSummary(requestInstructions); + Boolean jointlyAgreed = editRequest.getJointlyAgreed(); this.emailParameters = Map.of( "edit_summary", summary, "rejection_reason", editRequest.getRejectionReason() == null ? "" : editRequest.getRejectionReason(), - "jointly_agreed", editRequest.getJointlyAgreed() ? "Yes" : "No", + "jointly_agreed", jointlyAgreed ? "Yes" : "No", "case_reference", booking.getCaseId().getReference(), "court_name", booking.getCaseId().getCourt().getName(), "witness_name", booking.getWitnessName(), @@ -52,7 +53,7 @@ public EditEmailParameters(EditRequest editRequest, String portalUrl) { ); this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); this.editRequestStatus = editRequest.getStatus(); - this.jointlyAgreed = editRequest.getJointlyAgreed(); + this.jointlyAgreed = jointlyAgreed; } private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { @@ -108,7 +109,7 @@ private static List validateAndRetrieveRequestInstruction + editRequest.getId()); } - EditInstructions instructions = EditInstructions.fromJson(editRequest.getEditInstruction()); + EditInstructions instructions = EditInstructions.tryFromJson(editRequest.getEditInstruction()); if (instructions == null) { throw new BadRequestException("Could not parse instructions for edit request : " + editRequest.getId()); 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 index ffc0625562..c9ec0be8f7 100644 --- 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 @@ -39,6 +39,7 @@ class EditEmailParametersTest { private final UUID editRequestId = UUID.randomUUID(); private EditRequest editRequest; private List instructionsList; + private CreateEditRequestDTO dto; private final String portalUrl = "http://localhost:8080"; @@ -69,7 +70,7 @@ void setUp() { instructionsList = Arrays.asList(instruction1, instruction2, instruction3); - CreateEditRequestDTO dto = new CreateEditRequestDTO(); + dto = new CreateEditRequestDTO(); dto.setId(editRequestId); dto.setJointlyAgreed(true); dto.setStatus(EditRequestStatus.APPROVED); @@ -82,6 +83,12 @@ void setUp() { 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() { @@ -132,6 +139,27 @@ void cannotCreateEmailParametersFromNullEditInstructions() { assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); } + @Test + @DisplayName("Cannot create email parameters with nonsense edit instructions") + void cannotCreateEmailParametersFromNonsenseEditInstructions() { + editRequest.updateEditRequestFromDto(dto, mockRecording, "this-is-not-json"); + assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); + } + + @Test + @DisplayName("Cannot create email parameters with empty edit instructions") + void cannotCreateEmailParametersFromEmptyEditInstructions() { + editRequest.updateEditRequestFromDto(dto, mockRecording, "{}"); + 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() { 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 db35c925fa..a666e58805 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 @@ -29,6 +29,7 @@ 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; @@ -180,6 +181,13 @@ void testEditRequestRejected() { assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); } + @DisplayName("Should not notify for null edit request") + @Test + void testNullEditRequestNotNotified() { + assertDoesNotThrow(() -> underTest.editRequestStatusWasUpdated(null)); + verifyNoInteractions(emailService); + } + @DisplayName("Should not notify when edit request is approved, completed, or non-submission status") @Test void testNoEditRequestNotificationForOtherStatusTypes() { 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 index e4fa3d325e..411866eb4a 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -13,7 +13,6 @@ class StringToolsTest { void should_parse_and_render_time_as_strings() { String parsedTime = StringTools.formatTimeAsString(35); assertThat(parsedTime).isEqualTo("00:00:35"); - } @Test From de7c982639e6f5063101c70f291daa5023300749 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Mon, 15 Jun 2026 13:28:43 +0100 Subject: [PATCH 16/37] Add static method for testing edit request status --- .../preapi/services/EditNotificationService.java | 11 ++++++++--- .../services/EditNotificationServiceTest.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) 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 8f3d40d165..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 @@ -34,9 +34,8 @@ public void editRequestStatusWasUpdated(EditRequest editRequest) { return; } - if (editRequest.getStatus() != EditRequestStatus.SUBMITTED - && editRequest.getStatus() != EditRequestStatus.REJECTED) { - // For edited recordings, notification is sent by RecordingListener instead + 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; } @@ -48,4 +47,10 @@ public void editRequestStatusWasUpdated(EditRequest editRequest) { } } + // 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/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java index a666e58805..0790b00c8d 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,5 +1,6 @@ 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; @@ -36,6 +37,7 @@ 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 { @@ -229,4 +231,17 @@ void testEditRequestNotificationsWhenNoCourtEmail() { assertThat(paramsCaptor.getValue()).isEqualTo(mockEditRequest); } + @Test + public 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)); + } + } From 464d9009a126c639c59e3ecda3a6f56c5b566345 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Mon, 15 Jun 2026 16:06:52 +0100 Subject: [PATCH 17/37] CHANGE: trigger notification service whenever status is updated --- .../preapi/services/EditRequestService.java | 6 - .../services/edit/EditRequestCrudService.java | 18 ++- .../services/EditRequestServiceTest.java | 79 +------------ .../edit/EditRequestCrudServiceTest.java | 110 +++++++++++++++--- 4 files changed, 112 insertions(+), 101 deletions(-) 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 832777eeea..7b9379a33b 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 @@ -47,7 +47,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 +55,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; } @@ -118,9 +115,6 @@ public UpsertResult upsert(CreateEditRequestDTO dto) { User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); Pair result = editRequestCrudService.upsert(dto, sourceRecording, user); - if (result.getFirst().equals(UpsertResult.UPDATED)) { - editNotificationService.editRequestStatusWasUpdated(result.getSecond()); - } return result.getFirst(); } 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..9df5408eb2 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 @@ -19,6 +19,7 @@ import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.services.EditNotificationService; import java.sql.Timestamp; import java.time.Instant; @@ -32,12 +33,15 @@ 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 +56,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 +96,9 @@ public EditRequest updateEditRequestStatus(UUID id, EditRequestStatus status) { } } editRequestRepository.save(request); + + editNotificationService.editRequestStatusWasUpdated(request); + return request; } @@ -128,6 +136,12 @@ public void delete(CreateEditRequestDTO dto) { } editRequestRepository.save(request); + + boolean editStatusWasUpdated = !isUpdate || !existingEditRequest.get().getStatus().equals(dto.getStatus()); + if (editStatusWasUpdated) { + editNotificationService.editRequestStatusWasUpdated(request); + } + if (isUpdate) { return Pair.of(UpsertResult.UPDATED, request); } 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 b08c437917..91fee8fb53 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 @@ -49,6 +49,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -68,9 +69,6 @@ class EditRequestServiceTest { @MockitoBean private RecordingService recordingService; - @MockitoBean - private EditNotificationService editNotificationService; - @MockitoBean private Recording mockRecording; @@ -394,82 +392,7 @@ void upsertEditInstructionsWithEmptyFile() { ); } - @Test - @DisplayName("*New* edit request does not trigger email on submission (to review: maybe it should)") - void newEditRequestDoesNotTriggerEmailOnSubmission() { - when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - when(dto.getJointlyAgreed()).thenReturn(true); - when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - - // Important bit here: CRUD service returns CREATED for new edit request - when(editRequestCrudService.upsert(dto, mockRecording, courtClerkUser)) - .thenReturn(Pair.of(UpsertResult.CREATED, mockEditRequest)); - - UpsertResult response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.CREATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(mockAuth, times(1)).getAppAccess(); - verify(editRequestCrudService, times(1)).upsert(dto, mockRecording, courtClerkUser); - verifyNoInteractions(editNotificationService); - } - - @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)).editRequestStatusWasUpdated(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)).editRequestStatusWasUpdated(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)).editRequestStatusWasUpdated(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..fe2c9cab7d 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 @@ -24,6 +24,7 @@ import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; 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.prepareEditRequestToCreateOrUpdate(any(CreateEditRequestDTO.class), any(Recording.class), + any(EditRequest.class))) + .thenReturn(newlyUpdatedEditRequest); } @Test @@ -170,28 +189,22 @@ 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().getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); assertThat(response.getSecond().getId()).isEqualTo(dto.getId()); verify(editRequestRepository, times(1)).findByIdNotLocked(dto.getId()); verify(editRequestRepository, times(1)).save(any(EditRequest.class)); + verify(editNotificationService, times(1)) + .editRequestStatusWasUpdated(newlyUpdatedEditRequest); } @Test @@ -222,6 +235,8 @@ void badRequestEmptyInstructions() { assertThat(message) .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); + + verifyNoInteractions(editNotificationService); } @Test @@ -271,12 +286,14 @@ 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() { when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); + when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); when(editRequestRepository.findByIdNotLocked(dto.getId())) .thenReturn(Optional.of(mockEditRequest)); when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(mockEditRequest)); @@ -285,13 +302,14 @@ void deleteEmptyInstructions() { assertThat(result.getFirst()).isEqualTo(UpsertResult.UPDATED); verify(editRequestRepository, times(1)).delete(result.getSecond()); + verifyNoInteractions(editNotificationService); } @Test @DisplayName("Should throw exception when editInstructions is null for *new* edit request") - void validateEditInstructionsIsEmptyForNewEditRequest() throws Exception { + void validateEditInstructionsIsEmptyForNewEditRequest() { 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( @@ -301,6 +319,7 @@ void validateEditInstructionsIsEmptyForNewEditRequest() throws Exception { assertThat(message) .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); + verifyNoInteractions(editNotificationService); } @Test @@ -339,4 +358,65 @@ 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); + } + } From 63601b40b662f49ee69550c5b4af9e40b37ead03 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Mon, 15 Jun 2026 16:08:02 +0100 Subject: [PATCH 18/37] Checkstyle --- .../hmcts/reform/preapi/services/EditRequestServiceTest.java | 2 -- 1 file changed, 2 deletions(-) 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 91fee8fb53..04bddfabe0 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 @@ -49,11 +49,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; 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; From a63887095d63a364d4b370b8b456d8e51677bb87 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Mon, 15 Jun 2026 16:49:04 +0100 Subject: [PATCH 19/37] Sonar issues --- .../templates/EditEmailParameters.java | 7 +- .../preapi/email/govnotify/GovNotifyTest.java | 139 +++++++++--------- .../templates/EditEmailParametersTest.java | 122 +++++++-------- .../services/EditNotificationServiceTest.java | 2 +- .../reform/preapi/utils/StringToolsTest.java | 4 +- 5 files changed, 133 insertions(+), 141 deletions(-) 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 index 6554af62a4..fcaeb811fc 100644 --- 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 @@ -39,11 +39,13 @@ public EditEmailParameters(EditRequest editRequest, String portalUrl) { List requestInstructions = validateAndRetrieveRequestInstructions(editRequest); String summary = generateEditSummary(requestInstructions); - Boolean jointlyAgreed = editRequest.getJointlyAgreed(); + this.jointlyAgreed = editRequest.getJointlyAgreed(); + String jointlyAgreedText = jointlyAgreed ? "Yes" : "No"; + this.emailParameters = Map.of( "edit_summary", summary, "rejection_reason", editRequest.getRejectionReason() == null ? "" : editRequest.getRejectionReason(), - "jointly_agreed", jointlyAgreed ? "Yes" : "No", + "jointly_agreed", jointlyAgreedText, "case_reference", booking.getCaseId().getReference(), "court_name", booking.getCaseId().getCourt().getName(), "witness_name", booking.getWitnessName(), @@ -53,7 +55,6 @@ public EditEmailParameters(EditRequest editRequest, String portalUrl) { ); this.toEmailAddress = booking.getCaseId().getCourt().getGroupEmail(); this.editRequestStatus = editRequest.getStatus(); - this.jointlyAgreed = jointlyAgreed; } private void validateEditRequestIsOkayForNotification(EditRequest editRequest) { diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java index 8640e5117c..4a70149c5a 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/govnotify/GovNotifyTest.java @@ -88,7 +88,7 @@ public class GovNotifyTest { @BeforeEach void setUp() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); + .thenReturn(new SendEmailResponse(govNotifyEmailResponse)); underTest = new GovNotify("http://localhost:8080", mockGovNotifyClient); @@ -147,11 +147,11 @@ void shouldCreateRecordingReadyTemplate() { @Test void shouldCreateCasePendingClosureTemplate() { var template = new CasePendingClosure( - "to", - "firstName", - "lastName", - "caseRef", - Timestamp.valueOf("2025-01-01 00:00:00.0") + "to", + "firstName", + "lastName", + "caseRef", + Timestamp.valueOf("2025-01-01 00:00:00.0") ); assertThat(template.getTemplateId()).isEqualTo("5322ba5c-f4c4-4d1b-807c-16f56f0d8d0c"); } @@ -160,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"); } @@ -188,18 +188,18 @@ void shouldCreateEmailServiceFactory() { var govNotify = new GovNotify("GovNotify", mockGovNotifyClient); List emailServiceList = List.of(govNotify); var emailServiceFactory = new EmailServiceFactory("GovNotify", true, - emailServiceList); + 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")); + () -> emailServiceFactory.getEnabledEmailService("nonexistent")); assertThrows( - IllegalArgumentException.class, () -> - new EmailServiceFactory("nonexistent", true, emailServiceList) + IllegalArgumentException.class, () -> + new EmailServiceFactory("nonexistent", true, emailServiceList) ); } @@ -238,8 +238,8 @@ void shouldSendPortalInviteEmail() { @Test void shouldSendCasePendingClosureEmail() { var response = underTest.casePendingClosure( - sampleUser, sampleCase, - Timestamp.valueOf("2025-01-01 00:00:00.0") + sampleUser, sampleCase, + Timestamp.valueOf("2025-01-01 00:00:00.0") ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); @@ -274,7 +274,7 @@ void shouldSendCaseClosureCancelledEmail() { @DisplayName("Should send editing email") void sendEditRejectionEmail() throws NotificationClientException { EmailResponse response = underTest.sendEmailAboutEditingRequest(editRequest) - .orElseThrow(() -> new EmailFailedToSendException("Something went wrong")); + .orElseThrow(() -> new EmailFailedToSendException("Something went wrong")); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); @@ -285,23 +285,24 @@ void sendEditRejectionEmail() throws NotificationClientException { ArgumentCaptor referenceCaptor = ArgumentCaptor.forClass(String.class); verify(mockGovNotifyClient, times(1)).sendEmail( - templateCaptor.capture(), - emailAddressCaptor.capture(), - variablesCaptor.capture(), - referenceCaptor.capture() + templateCaptor.capture(), + emailAddressCaptor.capture(), + variablesCaptor.capture(), + referenceCaptor.capture() ); // 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).containsEntry("case_reference", sampleCase.getReference()); - assertThat(variables).containsEntry("court_name", sampleCase.getCourt().getName()); - assertThat(variables).containsEntry("defendant_names", "Defendant Name"); - assertThat(variables).containsEntry("edit_count", 1); - assertThat(variables).containsEntry("jointly_agreed", "Yes"); - assertThat(variables).containsEntry("rejection_reason", ""); - assertThat(variables).containsEntry("portal_link", "http://localhost:8080"); + 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")) @@ -309,11 +310,11 @@ void sendEditRejectionEmail() throws NotificationClientException { void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.recordingReady(sampleUser, sampleCase) + EmailFailedToSendException.class, + () -> underTest.recordingReady(sampleUser, sampleCase) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -324,11 +325,11 @@ void shouldFailToSendRecordingReadyEmail() throws NotificationClientException { void shouldFailToSendRecordingEditedEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.recordingEdited(sampleUser, sampleCase) + EmailFailedToSendException.class, + () -> underTest.recordingEdited(sampleUser, sampleCase) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -350,11 +351,11 @@ void shouldAbsorbErrorThrownByEmailParameters() { void shouldFailToSendPortalInviteEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.portalInvite(sampleUser) + EmailFailedToSendException.class, + () -> underTest.portalInvite(sampleUser) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -365,17 +366,17 @@ void shouldFailToSendPortalInviteEmail() throws NotificationClientException { void shouldFailToSendCasePendingClosureEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.casePendingClosure( - sampleUser, - sampleCase, - Timestamp.valueOf("2025-01-01 00:00:00.0") - ) + EmailFailedToSendException.class, + () -> underTest.casePendingClosure( + sampleUser, + sampleCase, + Timestamp.valueOf("2025-01-01 00:00:00.0") + ) ) - .getMessage(); + .getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); } @@ -385,11 +386,11 @@ void shouldFailToSendCasePendingClosureEmail() throws NotificationClientExceptio void shouldFailToSendCaseClosedEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.caseClosed(sampleUser, sampleCase) + EmailFailedToSendException.class, + () -> underTest.caseClosed(sampleUser, sampleCase) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -400,11 +401,11 @@ void shouldFailToSendCaseClosedEmail() throws NotificationClientException { void shouldFailToSendCaseClosureCancelledEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.caseClosureCancelled(sampleUser, sampleCase) + EmailFailedToSendException.class, + () -> underTest.caseClosureCancelled(sampleUser, sampleCase) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -414,8 +415,8 @@ void shouldFailToSendCaseClosureCancelledEmail() throws NotificationClientExcept @Test void shouldSendEmailVerificationEmail() { var response = underTest.emailVerification( - sampleUser.getEmail(), sampleUser.getFirstName(), - sampleUser.getLastName(), "123456" + sampleUser.getEmail(), sampleUser.getFirstName(), + sampleUser.getLastName(), "123456" ); assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); @@ -427,16 +428,16 @@ void shouldSendEmailVerificationEmail() { @Test void shouldFailToSendEmailVerificationEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.emailVerification( - sampleUser.getEmail(), - sampleUser.getFirstName(), - sampleUser.getLastName(), - "123456" - ) + EmailFailedToSendException.class, + () -> underTest.emailVerification( + sampleUser.getEmail(), + sampleUser.getFirstName(), + sampleUser.getLastName(), + "123456" + ) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + sampleUser.getEmail()); @@ -446,15 +447,15 @@ void shouldFailToSendEmailVerificationEmail() throws NotificationClientException @DisplayName("Should fail to send editing email") void shouldFailToSendEditingEmail() throws NotificationClientException { when(mockGovNotifyClient.sendEmail(any(), any(), any(), any())) - .thenThrow(mock(NotificationClientException.class)); + .thenThrow(mock(NotificationClientException.class)); var message = assertThrows( - EmailFailedToSendException.class, - () -> underTest.sendEmailAboutEditingRequest(editRequest) + EmailFailedToSendException.class, + () -> underTest.sendEmailAboutEditingRequest(editRequest) ).getMessage(); assertThat(message).isEqualTo("Failed to send email to: " - + sampleCase.getCourt().getGroupEmail()); + + sampleCase.getCourt().getGroupEmail()); } @DisplayName("Should prefer alternative email when it ends with .cjsm.net") 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 index c9ec0be8f7..1b588e334e 100644 --- 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 @@ -3,6 +3,9 @@ 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; @@ -24,7 +27,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -132,24 +135,12 @@ void cannotCreateEmailParametersFromNullCourtEmail() { assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); } - @Test - @DisplayName("Cannot create email parameters with null edit instructions") - void cannotCreateEmailParametersFromNullEditInstructions() { - editRequest.setEditInstruction(null); - assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); - } - - @Test - @DisplayName("Cannot create email parameters with nonsense edit instructions") - void cannotCreateEmailParametersFromNonsenseEditInstructions() { - editRequest.updateEditRequestFromDto(dto, mockRecording, "this-is-not-json"); - assertThrows(BadRequestException.class, () -> new EditEmailParameters(editRequest, portalUrl)); - } - - @Test - @DisplayName("Cannot create email parameters with empty edit instructions") - void cannotCreateEmailParametersFromEmptyEditInstructions() { - editRequest.updateEditRequestFromDto(dto, mockRecording, "{}"); + @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)); } @@ -164,8 +155,8 @@ void cannotCreateEmailParametersFromEmptyRequestEditInstructions() { @DisplayName("Cannot create email parameters if instructions say not to notify") void cannotCreateEmailParametersIfInstructionsSayNotToNotify() { EditInstructions instructions = new EditInstructions( - instructionsList, new ArrayList<>(), - false, false + instructionsList, new ArrayList<>(), + false, false ); editRequest.setEditInstruction(JsonUtils.toJson(instructions)); @@ -189,40 +180,39 @@ void shouldAccuratelyCreateEmailParameters() { assertThat(editEmailParameters.getJointlyAgreed()).isFalse(); - Map paramsMap = editEmailParameters.getEmailParameters(); + String expectedSummary = """ + Edit 1:\s + Start time: 00:00:05 + End time: 00:00:10 + Time Removed: 00:00:05 + Reason: first reason - assertThat(paramsMap).isNotEmpty(); + 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 - assertThat(paramsMap).containsEntry("rejection_reason", editRequest.getRejectionReason()); - assertThat(paramsMap).containsEntry("jointly_agreed", "No"); - assertThat(paramsMap).containsEntry("case_reference", mockCase.getReference()); - assertThat(paramsMap).containsEntry("court_name", mockCourt.getName()); - assertThat(paramsMap).containsEntry("witness_name", "Witness Name"); - assertThat(paramsMap).containsEntry("defendant_names", "Defendant Name"); - assertThat(paramsMap).containsEntry("edit_count", 3); - assertThat(paramsMap).containsEntry("portal_link", "http://localhost:8080"); + """; + Map paramsMap = editEmailParameters.getEmailParameters(); - 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 - - """; - assertThat(paramsMap).containsEntry("edit_summary", expectedSummary); + 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 @@ -236,19 +226,8 @@ void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { assertThat(editEmailParameters.getJointlyAgreed()).isTrue(); - Map paramsMap = editEmailParameters.getEmailParameters(); - assertThat(paramsMap).isNotEmpty(); - assertThat(paramsMap).containsEntry("rejection_reason", ""); - assertThat(paramsMap).containsEntry("jointly_agreed", "Yes"); - assertThat(paramsMap).containsEntry("case_reference", mockCase.getReference()); - assertThat(paramsMap).containsEntry("court_name", mockCourt.getName()); - assertThat(paramsMap).containsEntry("witness_name", "Witness Name"); - assertThat(paramsMap).containsEntry("defendant_names", "Defendant Name"); - assertThat(paramsMap).containsEntry("edit_count", 3); - assertThat(paramsMap).containsEntry("portal_link", "http://localhost:8080"); - assertThat(paramsMap).containsEntry( - "edit_summary", """ + String expectedSummary = """ Edit 1:\s Start time: 00:00:05 End time: 00:00:10 @@ -267,8 +246,19 @@ void shouldAccuratelyCreateEmailParametersForJointlyAgreed() { 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/services/EditNotificationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditNotificationServiceTest.java index 0790b00c8d..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 @@ -232,7 +232,7 @@ void testEditRequestNotificationsWhenNoCourtEmail() { } @Test - public void testIfEmailShouldBeSentOnStatusChange() { + void testIfEmailShouldBeSentOnStatusChange() { Assertions.assertTrue(isNotifiable(EditRequestStatus.REJECTED)); Assertions.assertTrue(isNotifiable(EditRequestStatus.SUBMITTED)); 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 index 411866eb4a..ad6d6409fe 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/utils/StringToolsTest.java @@ -1,10 +1,10 @@ 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; -import static org.junit.Assert.assertThrows; class StringToolsTest { @@ -18,7 +18,7 @@ void should_parse_and_render_time_as_strings() { @Test @DisplayName("Should complain if time is negative") void should_complain_if_time_is_negative() { - assertThrows(IllegalArgumentException.class, () -> StringTools.formatTimeAsString(-3)); + Assertions.assertThrows(IllegalArgumentException.class, () -> StringTools.formatTimeAsString(-3)); } @Test From e7c0763d6cbec9105ae896e7a0369f6d5be9992b Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 16 Jun 2026 16:39:30 +0100 Subject: [PATCH 20/37] Functional test stubs --- .../EditControllerFullyAutomatedFT.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java 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..cf1a33a998 --- /dev/null +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -0,0 +1,52 @@ +package uk.gov.hmcts.reform.preapi.controllers; + +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.media.storage.AzureFinalStorageService; +import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase; + +// I split this out to capture the first half of the editing process, which doesn't yet have any tests +public class EditControllerFullyAutomatedFT extends FunctionalTestBase { + private static final String EDIT_CSV_DIR = "src/functionalTest/resources/test/edit/"; + private static final String EDIT_ENDPOINT = "/edits"; + + @MockitoBean + private AzureFinalStorageService azureFinalStorageService; + + @Test + @DisplayName("Should create a DRAFT edit request") + void editDraftSuccess() { + // Should be able to keep updating an edit request + } + + @Test + @DisplayName("Should be able to submit an edit request") + void editRequestSubmission() { + // The instructions should be read-only once submitted + // Should send an email notification to shared-with users and the court group email address + // Should audit-log who submitted it + } + + @Test + @DisplayName("When an edit request has been rejected, the submitter and shared-with users should be notified") + void rejectedEditRequest(){ + + } + + @Test + @DisplayName("When an edit request has been approved, it should be picked up for processing") + void approvedEditRequest(){ + // Not sure how this transition works in practice. Perhaps we won't need the PENDING status any more? + } + + + @Test + @DisplayName("Should not create an edit request with unsafe data in fields") + void editRequestWithUnsafeData() { + // Copy and rewrite the existing test to use the `PUT edits/{id}` endpoint instead of the CSV endpoint + } + + + +} From b30d7231fce9354f31294a093ccb4832a956f59f Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Mon, 29 Jun 2026 12:50:04 +0100 Subject: [PATCH 21/37] Basic test outline --- .../EditControllerFullyAutomatedFT.java | 213 +++++++++++++++++- 1 file changed, 201 insertions(+), 12 deletions(-) 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 index cf1a33a998..a6ba7f4bd7 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -1,42 +1,232 @@ 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.mockito.Mock; 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.EditRequestDTO; +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.exception.ResourceInWrongStateException; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; +import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; +import uk.gov.hmcts.reform.preapi.services.EditRequestService; +import uk.gov.hmcts.reform.preapi.services.RecordingService; import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +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 -public class EditControllerFullyAutomatedFT extends FunctionalTestBase { - private static final String EDIT_CSV_DIR = "src/functionalTest/resources/test/edit/"; +class EditControllerFullyAutomatedFT extends FunctionalTestBase { private static final String EDIT_ENDPOINT = "/edits"; @MockitoBean private AzureFinalStorageService azureFinalStorageService; + @MockitoBean + private RecordingRepository recordingRepository; + + private UUID recordingId; + private RecordingDTO recordingDTO; + + + @MockitoBean + 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(); + when(recording.getId()).thenReturn(recordingId); + when(recording.getCaptureSession()).thenReturn(captureSession); + + 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"); + + when(recordingRepository.findAll()).thenReturn(List.of(recording)); + when(recordingRepository.findById(recordingId)).thenReturn(Optional.of(recording)); + when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId)).thenReturn(Optional.of(recording)); + when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId, true)) + .thenReturn(Optional.of(recording)); + when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId, false)) + .thenReturn(Optional.of(recording)); + + 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, receiving a notification") + void editRequestSuccess() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = new CreateEditRequestDTO(); + createEditRequestDTO.setId(UUID.randomUUID()); + createEditRequestDTO.setStatus(EditRequestStatus.DRAFT); + createEditRequestDTO.setSourceRecordingId(recordingId); + + // Create as DRAFT + Response firstResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), + TestingSupportRoles.SUPER_USER + ); + + EditRequestDTO editRequestDTO = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); + + Assertions.assertThat(editRequestDTO.getStatus()).isEqualTo(EditRequestStatus.DRAFT); + Assertions.assertThat(editRequestDTO.getEditInstruction()).isNull(); + Assertions.assertThat(editRequestDTO.getSourceRecording()).isEqualTo(recordingDTO); + + // Update as DRAFT + List editInstructions = List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:00") + .endOfCut("00:00:01") + .build()); + createEditRequestDTO.setEditInstructions(editInstructions); + + Response secondResponse = doPutRequest( + EDIT_ENDPOINT + "/" + recordingId, + OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), + TestingSupportRoles.SUPER_USER + ); + + EditRequestDTO updatedDraft = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); + + Assertions.assertThat(updatedDraft.getStatus()).isEqualTo(EditRequestStatus.DRAFT); + Assertions.assertThat(updatedDraft.getEditInstruction().getRequestedInstructions()) + .isEqualTo(editInstructions); + Assertions.assertThat(updatedDraft.getSourceRecording()).isEqualTo(recordingDTO); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + Response thirdResponse = doPutRequest( + EDIT_ENDPOINT + "/" + recordingId, + OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), + TestingSupportRoles.SUPER_USER + ); + + EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); + + Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); + Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) + .isEqualTo(editInstructions); + Assertions.assertThat(submitted.getSourceRecording()).isEqualTo(recordingDTO); + } + @Test - @DisplayName("Should create a DRAFT edit request") - void editDraftSuccess() { - // Should be able to keep updating an edit request + @DisplayName("An edit request should be read-only once submitted") + void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { + 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); + + + when(recordingRepository.findByIdAndDeletedAtIsNull(editRequestId)).thenReturn(Optional.of(recording)); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + Response firstResponse = doPutRequest( + EDIT_ENDPOINT + "/" + editRequestId, + requestBody, + TestingSupportRoles.SUPER_USER + ); + + EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); + + Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); + Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) + .isEqualTo(editInstructions); + Assertions.assertThat(submitted.getSourceRecording()).isEqualTo(recordingDTO); + + // Should be read-only once submitted + List updatedInstructions = List.of(EditCutInstructionDTO.builder() + .startOfCut("00:00:00") + .endOfCut("00:00:01") + .build()); + createEditRequestDTO.setEditInstructions(updatedInstructions); + String resubmittedRequest = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + String message = assertThrows( + ResourceInWrongStateException.class, + () -> doPutRequest( + EDIT_ENDPOINT + "/" + recordingId, + resubmittedRequest, + TestingSupportRoles.SUPER_USER + ) + ).getMessage(); + + Assertions.assertThat(message) + .isEqualTo( + "Cannot resubmit edit request {}: submit a new edit request", + createEditRequestDTO.getId() + ); } @Test - @DisplayName("Should be able to submit an edit request") - void editRequestSubmission() { - // The instructions should be read-only once submitted - // Should send an email notification to shared-with users and the court group email address + @DisplayName("Should record an audit trail when edit request is submitted") + void editRequestSubmissionAuditLog() { // Should audit-log who submitted it } @Test @DisplayName("When an edit request has been rejected, the submitter and shared-with users should be notified") - void rejectedEditRequest(){ + void rejectedEditRequest() { } @Test @DisplayName("When an edit request has been approved, it should be picked up for processing") - void approvedEditRequest(){ + void approvedEditRequest() { // Not sure how this transition works in practice. Perhaps we won't need the PENDING status any more? } @@ -48,5 +238,4 @@ void editRequestWithUnsafeData() { } - } From cbde8d54e8c77e6ed16c97d3beb999e850857b0c Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 21 Jul 2026 17:37:46 +0100 Subject: [PATCH 22/37] First stab at functional tests for first half of auto editing --- .../EditControllerFullyAutomatedFT.java | 189 ++++++++++++------ .../controllers/RecordingControllerFT.java | 2 +- .../preapi/services/EditRequestServiceIT.java | 9 +- .../preapi/controllers/EditController.java | 3 +- .../controllers/TestingSupportController.java | 1 + .../preapi/services/EditRequestService.java | 8 +- .../services/edit/EditRequestCrudService.java | 51 ++--- .../services/edit/EditRequestValidator.java | 25 ++- .../preapi/services/edit/FfmpegService.java | 14 ++ .../preapi/controller/EditControllerTest.java | 8 - .../services/EditRequestServiceTest.java | 11 +- .../edit/EditRequestCrudServiceTest.java | 16 +- .../tasks/ReEncodeRecordingsFromCsvTest.java | 8 +- 13 files changed, 208 insertions(+), 137 deletions(-) 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 index a6ba7f4bd7..1b332300a4 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -3,10 +3,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.restassured.response.Response; import org.assertj.core.api.Assertions; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mock; 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; @@ -21,17 +21,16 @@ import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; -import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; -import uk.gov.hmcts.reform.preapi.services.EditRequestService; -import uk.gov.hmcts.reform.preapi.services.RecordingService; 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.Optional; import java.util.UUID; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; 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 @@ -41,14 +40,8 @@ class EditControllerFullyAutomatedFT extends FunctionalTestBase { @MockitoBean private AzureFinalStorageService azureFinalStorageService; - @MockitoBean - private RecordingRepository recordingRepository; - private UUID recordingId; private RecordingDTO recordingDTO; - - - @MockitoBean private Recording recording; @MockitoBean @@ -68,8 +61,13 @@ class EditControllerFullyAutomatedFT extends FunctionalTestBase { void setUp() { CreateRecordingResponse recordingDetails = createRecording(); recordingId = recordingDetails.recordingId(); - when(recording.getId()).thenReturn(recordingId); - when(recording.getCaptureSession()).thenReturn(captureSession); + 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); @@ -80,14 +78,6 @@ void setUp() { when(legalCase.getCourt()).thenReturn(mockCourt); when(mockCourt.getGroupEmail()).thenReturn("mock@email.com"); - when(recordingRepository.findAll()).thenReturn(List.of(recording)); - when(recordingRepository.findById(recordingId)).thenReturn(Optional.of(recording)); - when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId)).thenReturn(Optional.of(recording)); - when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId, true)) - .thenReturn(Optional.of(recording)); - when(recordingRepository.findByIdAndDeletedAtIsNull(recordingId, false)) - .thenReturn(Optional.of(recording)); - recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true) .as(RecordingDTO.class); @@ -95,65 +85,138 @@ void setUp() { .thenReturn(recordingDTO.getFilename()); when(azureFinalStorageService.getRecordingDuration(recordingDetails.recordingId())) .thenReturn(recordingDTO.getDuration()); - - } @Test @DisplayName("Should create a DRAFT edit request, update it and submit it, receiving a notification") void editRequestSuccess() throws JsonProcessingException { CreateEditRequestDTO createEditRequestDTO = new CreateEditRequestDTO(); - createEditRequestDTO.setId(UUID.randomUUID()); + UUID createEditRequestId = UUID.randomUUID(); + createEditRequestDTO.setId(createEditRequestId); createEditRequestDTO.setStatus(EditRequestStatus.DRAFT); createEditRequestDTO.setSourceRecordingId(recordingId); // Create as DRAFT - Response firstResponse = doPutRequest( - EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), - OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), - TestingSupportRoles.SUPER_USER - ); - - EditRequestDTO editRequestDTO = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); - - Assertions.assertThat(editRequestDTO.getStatus()).isEqualTo(EditRequestStatus.DRAFT); - Assertions.assertThat(editRequestDTO.getEditInstruction()).isNull(); - Assertions.assertThat(editRequestDTO.getSourceRecording()).isEqualTo(recordingDTO); + 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:00") - .endOfCut("00:00:01") + .startOfCut("00:00:02") + .endOfCut("00:00:03") .build()); createEditRequestDTO.setEditInstructions(editInstructions); - Response secondResponse = doPutRequest( - EDIT_ENDPOINT + "/" + recordingId, + 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() + )); + + // ...but should be allowed to update status with original instructions + createEditRequestDTO.setStatus(EditRequestStatus.DRAFT); + createEditRequestDTO.setEditInstructions(editInstructions); - EditRequestDTO updatedDraft = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); - - Assertions.assertThat(updatedDraft.getStatus()).isEqualTo(EditRequestStatus.DRAFT); - Assertions.assertThat(updatedDraft.getEditInstruction().getRequestedInstructions()) - .isEqualTo(editInstructions); - Assertions.assertThat(updatedDraft.getSourceRecording()).isEqualTo(recordingDTO); + Response setBackToDraft = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); + + assertResponseCode(setBackToDraft, 200); + Assertions.assertThat(setBackToDraft.jsonPath().getString("id")) + .isEqualTo(createEditRequestId.toString()); + Assertions.assertThat(setBackToDraft.jsonPath().getString("status")) + .isEqualTo(EditRequestStatus.DRAFT.name()); + 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); + + + // ...and then we're allowed to edit it again + createEditRequestDTO.setEditInstructions(updatedEditInstructions); + + Response updateEditInstructions = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); + assertResponseCode(updateEditInstructions, 200); + Assertions.assertThat(updateEditInstructions.jsonPath().getString("id")) + .isEqualTo(createEditRequestId.toString()); + Assertions.assertThat(updateEditInstructions.jsonPath().getString("status")) + .isEqualTo(EditRequestStatus.DRAFT.name()); + Assertions.assertThat(updateEditInstructions.jsonPath().getList("edit_instruction.requestedInstructions")) + .size().isEqualTo(editInstructions.size()); + Assertions.assertThat(updateEditInstructions.jsonPath() + .getInt("edit_instruction.requestedInstructions[0].start")) + .isEqualTo(6); + Assertions.assertThat(updateEditInstructions.jsonPath() + .getInt("edit_instruction.requestedInstructions[0].end")) + .isEqualTo(7); + } - // Submit - createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); - Response thirdResponse = doPutRequest( - EDIT_ENDPOINT + "/" + recordingId, + private @NotNull Response upsertEditRequestAndGetResponse(UUID createEditRequestId, + CreateEditRequestDTO createEditRequestDTO) + throws JsonProcessingException { + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestId, OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), TestingSupportRoles.SUPER_USER ); + assertResponseCode(putResponse, 200); - EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); - - Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); - Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) - .isEqualTo(editInstructions); - Assertions.assertThat(submitted.getSourceRecording()).isEqualTo(recordingDTO); + return doGetRequest( + EDIT_ENDPOINT + "/" + createEditRequestId, + TestingSupportRoles.SUPER_USER + ); } @Test @@ -170,19 +233,25 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { createEditRequestDTO.setEditInstructions(editInstructions); createEditRequestDTO.setJointlyAgreed(true); - - when(recordingRepository.findByIdAndDeletedAtIsNull(editRequestId)).thenReturn(Optional.of(recording)); - // Submit createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + // Fails on message com.fasterxml.jackson.databind.exc.MismatchedInputException: + // No content to map due to end-of-input Response firstResponse = doPutRequest( EDIT_ENDPOINT + "/" + editRequestId, requestBody, TestingSupportRoles.SUPER_USER ); - EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponse.body().asString(), EditRequestDTO.class); + assertResponseCode(firstResponse, 201); + + String firstResponseBody = firstResponse.body().asString(); + + Assertions.assertThat(firstResponseBody).isNotBlank(); + + EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponseBody, EditRequestDTO.class); Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) 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/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java b/src/integrationTest/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceIT.java index 52899b98e3..1d3bc48c43 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 @@ -13,12 +13,12 @@ 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.UUID; @@ -150,6 +150,7 @@ public void deleteEditRequest() { @Transactional public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { 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()); @@ -160,7 +161,7 @@ public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { editRequestId, recording, "{\"ffmpegInstructions\":[{\"start\":0,\"end\":60},{\"start\":120,\"end\":180}]}", - EditRequestStatus.PENDING, + EditRequestStatus.DRAFT, user, null, null, @@ -180,10 +181,10 @@ public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { 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(); 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..aeec81e6f6 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 @@ -122,7 +122,8 @@ public ResponseEntity upsertEditRequest( throw new PathPayloadMismatchException("editRequestId", "createEditRequestDTO.id"); } - return getUpsertResponse(editRequestService.upsert(createEditRequestDTO), id); + editRequestService.upsert(createEditRequestDTO); + return ResponseEntity.ok().build(); } @DeleteMapping("/{id}") 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/services/EditRequestService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java index 7b9379a33b..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; @@ -108,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); - - return result.getFirst(); + editRequestCrudService.upsert(dto, sourceRecording, user); } @Transactional 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 9df5408eb2..a9a40fe891 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,8 +14,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.repositories.EditRequestRepository; import uk.gov.hmcts.reform.preapi.services.EditNotificationService; @@ -27,6 +24,8 @@ import java.util.Set; import java.util.UUID; +import static uk.gov.hmcts.reform.preapi.services.edit.EditRequestValidator.editInstructionsAreEmpty; + @Slf4j @Service public class EditRequestCrudService { @@ -115,14 +114,25 @@ 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); + + // If is draft and is update and edit instructions are empty: then delete + if (dto.getStatus().equals(EditRequestStatus.DRAFT) + && editInstructionsAreEmpty(dto) + && !dto.isForceReencode()) { + log.info( + "Deleting edit request {} for source recording {} as edit instructions are empty", + existingEditRequest.orElseThrow().getId(), dto.getSourceRecordingId() + ); + editRequestRepository.delete(existingEditRequest.orElseThrow()); + return existingEditRequest.orElseThrow(); + } } EditRequest request = editingService.prepareEditRequestToCreateOrUpdate( @@ -142,10 +152,7 @@ public void delete(CreateEditRequestDTO dto) { editNotificationService.editRequestStatusWasUpdated(request); } - if (isUpdate) { - return Pair.of(UpsertResult.UPDATED, request); - } - return Pair.of(UpsertResult.CREATED, request); + return request; } @Transactional @@ -156,26 +163,6 @@ 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..6c6efd2037 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,6 +13,7 @@ import java.util.List; +import static java.lang.String.format; import static uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO.formatTime; @Slf4j @@ -31,7 +32,7 @@ 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"); } @@ -76,7 +77,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 +107,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 +134,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..44432df45a 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 @@ -11,6 +11,7 @@ import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; 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.UnknownServerException; import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; @@ -221,6 +222,19 @@ protected void generateConcatListFile(final Set segmentFiles, final Stri return request; } + if (EditRequestValidator.editInstructionsAreEmpty(dto)) { + if (dto.getStatus() == EditRequestStatus.DRAFT) { + String newEditInstruction = toJson(new EditInstructions( + List.of(), List.of(), false, + shouldSendNotifications(dto))); + request.updateEditRequestFromDto(dto, sourceRecording, newEditInstruction); + return request; + } else { + throw new IllegalArgumentException("Invalid edit request: instructions may only be empty for DRAFT" + + " edit requests or forced re-encodes"); + } + } + boolean isOriginalRecordingEdit = sourceRecording.getParentRecording() == null; boolean sourceInstructionsAreNotEmpty = !isOriginalRecordingEdit 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..f629e0a4e8 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 @@ -260,8 +260,6 @@ void upsertEditRequestCreated() throws Exception { .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)) @@ -282,8 +280,6 @@ void upsertReencodeEditRequestCreated() throws Exception { 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)) @@ -360,8 +356,6 @@ void upsertEditRequestDeleted() throws Exception { .build())); 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)) @@ -408,8 +402,6 @@ void upsertEditRequestUpdated() throws Exception { .build())); 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)) 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 04bddfabe0..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; @@ -172,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); @@ -204,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()); @@ -335,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); 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 fe2c9cab7d..ea572b9c95 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 @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; 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,7 +19,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.repositories.EditRequestRepository; @@ -196,10 +194,9 @@ void createEditRequestSuccess() { when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); - Pair response = underTest.upsert(dto, mockRecording, mockUser); - assertThat(response.getFirst()).isEqualTo(UpsertResult.CREATED); - assertThat(response.getSecond().getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); - 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)); @@ -298,10 +295,9 @@ void deleteEmptyInstructions() { .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); + EditRequest result = underTest.upsert(dto, mockRecording, mockUser); - verify(editRequestRepository, times(1)).delete(result.getSecond()); + verify(editRequestRepository, times(1)).delete(result); verifyNoInteractions(editNotificationService); } @@ -324,7 +320,7 @@ void validateEditInstructionsIsEmptyForNewEditRequest() { @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()); 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(); From cad50dc4ed700f2c19994489f1217cd34399b77a Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 21 Jul 2026 18:02:32 +0100 Subject: [PATCH 23/37] Checkstyle --- .../reform/preapi/services/edit/EditRequestValidator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 6c6efd2037..1e7d1d8428 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 @@ -17,6 +17,7 @@ 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; @@ -150,6 +151,6 @@ public static void extraValidationForExistingEditRequest(EditRequest existingEdi } public static boolean editInstructionsAreEmpty(CreateEditRequestDTO dto) { - return (dto.getEditInstructions() == null || dto.getEditInstructions().isEmpty()); + return dto.getEditInstructions() == null || dto.getEditInstructions().isEmpty(); } } From b47276ca0522c015c7d8a0816611cc4089a0b2f9 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 21 Jul 2026 18:18:24 +0100 Subject: [PATCH 24/37] Update tests --- .../reform/preapi/controllers/EditController.java | 5 +++-- .../reform/preapi/controller/EditControllerTest.java | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) 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 aeec81e6f6..4ca661f2e5 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 @@ -32,6 +32,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; @@ -123,7 +124,7 @@ public ResponseEntity upsertEditRequest( } editRequestService.upsert(createEditRequestDTO); - return ResponseEntity.ok().build(); + return getUpsertResponse(UpsertResult.UPDATED, id); } @DeleteMapping("/{id}") @@ -137,7 +138,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/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/controller/EditControllerTest.java index f629e0a4e8..a745551c05 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 @@ -265,14 +265,14 @@ void upsertEditRequestCreated() 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)); } @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(); dto.setId(UUID.randomUUID()); @@ -285,7 +285,7 @@ void upsertReencodeEditRequestCreated() 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)); @@ -315,7 +315,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)); @@ -361,7 +361,7 @@ void upsertEditRequestDeleted() throws Exception { .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)); } From e45202e53c3790f1a4dcf881962ab195bb89c229 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 21 Jul 2026 18:23:00 +0100 Subject: [PATCH 25/37] Refactor test to reduce duplication --- .../preapi/controller/EditControllerTest.java | 134 ++++++------------ 1 file changed, 41 insertions(+), 93 deletions(-) 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 a745551c05..0ce401bead 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); @@ -251,13 +254,7 @@ 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())); + CreateEditRequestDTO dto = createBogStandardEditRequest(); dto.setStatus(EditRequestStatus.DRAFT); mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) @@ -274,7 +271,7 @@ void upsertEditRequestCreated() throws Exception { @Test @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); @@ -294,13 +291,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); @@ -324,7 +315,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); @@ -347,13 +338,7 @@ 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); mockMvc.perform(delete(TEST_URL + "/edits/" + dto.getId()) @@ -369,15 +354,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()) @@ -393,13 +372,7 @@ void deleteEditRequestPathPayloadMismatch() throws Exception { @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); mockMvc.perform(put(TEST_URL + "/edits/" + dto.getId()) @@ -416,15 +389,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()) @@ -454,7 +421,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) @@ -469,7 +436,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") @@ -489,13 +456,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()) @@ -509,13 +470,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()) @@ -530,13 +485,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()) @@ -551,13 +500,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"); @@ -573,13 +516,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())); @@ -592,4 +529,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; + } + } From ebe3131aaaca14b64115709d643780e6096f6832 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Tue, 21 Jul 2026 18:26:37 +0100 Subject: [PATCH 26/37] Checkstyle --- .../uk/gov/hmcts/reform/preapi/controllers/EditController.java | 1 - 1 file changed, 1 deletion(-) 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 4ca661f2e5..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; From 738c6366f983e034818f4d5be0eeb56d68a3b518 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 13:20:29 +0100 Subject: [PATCH 27/37] More refactor and corrections --- .../services/edit/EditRequestCrudService.java | 59 ++++++---- .../services/edit/EditRequestValidator.java | 8 +- .../preapi/services/edit/FfmpegService.java | 32 +---- .../preapi/services/edit/IEditingService.java | 6 +- .../services/EditRequestServiceTest.java | 1 + .../edit/EditRequestCrudServiceTest.java | 109 ++++++++++++++---- .../services/edit/FfmpegServiceTest.java | 23 +--- 7 files changed, 139 insertions(+), 99 deletions(-) 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 a9a40fe891..6ed69dd505 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 @@ -14,17 +14,20 @@ 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.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.services.edit.EditRequestValidator.editInstructionsAreEmpty; +import static uk.gov.hmcts.reform.preapi.utils.JsonUtils.toJson; @Slf4j @Service @@ -121,38 +124,49 @@ public void delete(CreateEditRequestDTO dto) { if (isUpdate) { EditRequestValidator.extraValidationForExistingEditRequest(existingEditRequest.get(), dto); - - // If is draft and is update and edit instructions are empty: then delete - if (dto.getStatus().equals(EditRequestStatus.DRAFT) - && editInstructionsAreEmpty(dto) - && !dto.isForceReencode()) { - log.info( - "Deleting edit request {} for source recording {} as edit instructions are empty", - existingEditRequest.orElseThrow().getId(), dto.getSourceRecordingId() - ); - editRequestRepository.delete(existingEditRequest.orElseThrow()); - return existingEditRequest.orElseThrow(); - } } - 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); + editRequestRepository.save(updatedEditRequest); boolean editStatusWasUpdated = !isUpdate || !existingEditRequest.get().getStatus().equals(dto.getStatus()); if (editStatusWasUpdated) { - editNotificationService.editRequestStatusWasUpdated(request); + editNotificationService.editRequestStatusWasUpdated(updatedEditRequest); } - return request; + 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 editingService.mergeOldAndNewEditInstructions(dto, sourceRecording, + existingEditRequest.orElse(new EditRequest())); } @Transactional @@ -164,5 +178,4 @@ public Set findRecordingIdsWithForceReencodeRequests(Set sourceRecor return editRequestRepository.findSourceRecordingIdsWithForceReencodeRequests(sourceRecordingIds); } - } 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 1e7d1d8428..3bd0750fd4 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 @@ -33,10 +33,16 @@ static void ensureEditRequestHasSourceRecording(EditRequest request) { static void validateEditMode(CreateEditRequestDTO dto) { log.debug("Validating edit request {}", dto); - if (dto.isForceReencode() && editInstructionsAreEmpty(dto)) { + 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) { 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 44432df45a..7d9c0baabf 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 @@ -211,29 +211,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; - } - - if (EditRequestValidator.editInstructionsAreEmpty(dto)) { - if (dto.getStatus() == EditRequestStatus.DRAFT) { - String newEditInstruction = toJson(new EditInstructions( - List.of(), List.of(), false, - shouldSendNotifications(dto))); - request.updateEditRequestFromDto(dto, sourceRecording, newEditInstruction); - return request; - } else { - throw new IllegalArgumentException("Invalid edit request: instructions may only be empty for DRAFT" - + " edit requests or forced re-encodes"); - } - } + public @NotNull EditRequest mergeOldAndNewEditInstructions(final CreateEditRequestDTO dto, + final Recording sourceRecording, + final EditRequest request) { boolean isOriginalRecordingEdit = sourceRecording.getParentRecording() == null; @@ -268,7 +248,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); @@ -276,10 +256,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/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/EditRequestServiceTest.java index 6e91e92a25..ef813b0196 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 @@ -50,6 +50,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; 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 ea572b9c95..44cb1a3916 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,6 +3,7 @@ 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; @@ -21,6 +22,7 @@ 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.repositories.EditRequestRepository; import uk.gov.hmcts.reform.preapi.services.EditNotificationService; import uk.gov.hmcts.reform.preapi.util.HelperFactory; @@ -141,8 +143,8 @@ void setUp() { when(newlyUpdatedEditRequest.getEditInstruction()).thenReturn(toJson(instructions)); - when(editingService.prepareEditRequestToCreateOrUpdate(any(CreateEditRequestDTO.class), any(Recording.class), - any(EditRequest.class))) + when(editingService.mergeOldAndNewEditInstructions(any(CreateEditRequestDTO.class), any(Recording.class), + any(EditRequest.class))) .thenReturn(newlyUpdatedEditRequest); } @@ -221,17 +223,69 @@ 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)).save(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)).save(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); } @@ -287,34 +341,20 @@ void createEditRequestWithCutsAndForceReencode() { } @Test - @DisplayName("Should delete edit request when upserting with empty instructions") - void deleteEmptyInstructions() { - when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); - when(dto.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); - when(editRequestRepository.findByIdNotLocked(dto.getId())) - .thenReturn(Optional.of(mockEditRequest)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(mockEditRequest)); - - EditRequest result = underTest.upsert(dto, mockRecording, mockUser); - - verify(editRequestRepository, times(1)).delete(result); - verifyNoInteractions(editNotificationService); - } - - @Test - @DisplayName("Should throw exception when editInstructions is null for *new* edit request") + @DisplayName("Should throw exception when editInstructions is null for *new* submitted edit request") void validateEditInstructionsIsEmptyForNewEditRequest() { when(dto.getEditInstructions()).thenReturn(new ArrayList<>()); 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); } @@ -415,4 +455,23 @@ void shouldInvokeNotificationServiceWhenStatusChanged() { 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)).save(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 { From de776fa4fa1330e8dd2a872000312bb1496fd755 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 13:28:24 +0100 Subject: [PATCH 28/37] Checkstyle --- .../reform/preapi/services/edit/EditRequestCrudService.java | 1 - .../reform/preapi/services/edit/EditRequestValidator.java | 4 ++-- .../gov/hmcts/reform/preapi/services/edit/FfmpegService.java | 1 - .../hmcts/reform/preapi/services/EditRequestServiceTest.java | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) 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 6ed69dd505..d7d5c225e6 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 @@ -14,7 +14,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.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; 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 3bd0750fd4..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 @@ -38,8 +38,8 @@ static void validateEditMode(CreateEditRequestDTO dto) { "Invalid Instruction: Cannot request cuts and force reencode on the same edit request"); } - if (editInstructionsAreEmpty(dto) && - !(dto.isForceReencode() || dto.getStatus().equals(EditRequestStatus.DRAFT))) { + 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"); } 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 7d9c0baabf..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 @@ -11,7 +11,6 @@ import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; 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.UnknownServerException; import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; 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 ef813b0196..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 @@ -50,7 +50,6 @@ 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; From a810c5a6ea363b9eca3044aa734ad851c63b94b3 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 13:44:06 +0100 Subject: [PATCH 29/37] Update integration test --- .../preapi/services/EditRequestServiceIT.java | 28 +++++++++++++++---- .../services/edit/EditRequestCrudService.java | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) 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 1d3bc48c43..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,6 +8,8 @@ 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; @@ -21,6 +23,7 @@ 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,7 +151,7 @@ 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); @@ -157,10 +160,11 @@ public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { 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}]}", + editInstructions, EditRequestStatus.DRAFT, user, null, @@ -174,9 +178,20 @@ 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()); @@ -186,7 +201,10 @@ public void upsertWithEmptyInstructionsShouldDeleteEditRequest() { 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/services/edit/EditRequestCrudService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java index d7d5c225e6..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 @@ -132,7 +132,7 @@ public void delete(CreateEditRequestDTO dto) { updatedEditRequest.setCreatedAt(Timestamp.from(Instant.now())); } - editRequestRepository.save(updatedEditRequest); + editRequestRepository.saveAndFlush(updatedEditRequest); boolean editStatusWasUpdated = !isUpdate || !existingEditRequest.get().getStatus().equals(dto.getStatus()); if (editStatusWasUpdated) { From 107e11e689e87af9ca72892d7c12c56949fbe650 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 14:01:53 +0100 Subject: [PATCH 30/37] Update test --- .../preapi/services/edit/EditRequestCrudServiceTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 44cb1a3916..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 @@ -201,7 +201,7 @@ void createEditRequestSuccess() { 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); } @@ -231,7 +231,7 @@ void createDraftEditWithEmptyInstructions() { underTest.upsert(dto, mockRecording, mockUser); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); + verify(editRequestRepository, times(1)).saveAndFlush(any(EditRequest.class)); verify(editNotificationService, times(1)) .editRequestStatusWasUpdated(any(EditRequest.class)); } @@ -257,7 +257,7 @@ void updateDraftEditWithEmptyInstructions() { underTest.upsert(dto, mockRecording, mockUser); ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); - verify(editRequestRepository, times(1)).save(captor.capture()); + verify(editRequestRepository, times(1)).saveAndFlush(captor.capture()); assertThat(captor.getValue().getId()).isEqualTo(existingEditRequest.getId()); assertThat(captor.getValue().getStatus()).isEqualTo(EditRequestStatus.DRAFT); @@ -464,7 +464,7 @@ void createReencodeEditRequestSuccess() { underTest.upsert(dto, mockRecording, mockUser); ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); - verify(editRequestRepository, times(1)).save(captor.capture()); + verify(editRequestRepository, times(1)).saveAndFlush(captor.capture()); EditInstructions editInstructions = EditInstructions.tryFromJson(captor.getValue().getEditInstruction()); From 1097072ffcd1161244b5276886298ebabf6bf5c0 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 14:38:55 +0100 Subject: [PATCH 31/37] Delete duplicate test --- .../preapi/controller/EditControllerTest.java | 18 ------------------ 1 file changed, 18 deletions(-) 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 0ce401bead..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 @@ -251,23 +251,6 @@ void getEditsOutOfBounds() throws Exception { verify(editRequestService, times(1)).findAll(any(), any()); } - @Test - @DisplayName("Should return 201 when successfully created edit request") - void upsertEditRequestCreated() throws Exception { - CreateEditRequestDTO dto = createBogStandardEditRequest(); - dto.setStatus(EditRequestStatus.DRAFT); - - 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().isNoContent()) - .andExpect(header().string("Location", TEST_URL + "/edits/" + dto.getId())); - - verify(editRequestService, times(1)).upsert(any(CreateEditRequestDTO.class)); - } - @Test @DisplayName("Should return 204 when successfully created reencode edit request") void upsertReencodeEditRequestCreated() throws Exception { @@ -368,7 +351,6 @@ 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 { From f438bc52baed184b92f27e51f15a44e22ae81a0f Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 14:41:27 +0100 Subject: [PATCH 32/37] Temp --- .../EditControllerFullyAutomatedFT.java | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) 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 index 1b332300a4..b2f5a08756 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -222,16 +222,7 @@ void editRequestSuccess() throws JsonProcessingException { @Test @DisplayName("An edit request should be read-only once submitted") void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { - 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); + CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); // Submit createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); @@ -240,7 +231,7 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { // Fails on message com.fasterxml.jackson.databind.exc.MismatchedInputException: // No content to map due to end-of-input Response firstResponse = doPutRequest( - EDIT_ENDPOINT + "/" + editRequestId, + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), requestBody, TestingSupportRoles.SUPER_USER ); @@ -255,7 +246,7 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) - .isEqualTo(editInstructions); + .isEqualTo(createEditRequestDTO.getEditInstructions()); Assertions.assertThat(submitted.getSourceRecording()).isEqualTo(recordingDTO); // Should be read-only once submitted @@ -283,20 +274,42 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { @Test @DisplayName("Should record an audit trail when edit request is submitted") - void editRequestSubmissionAuditLog() { - // Should audit-log who submitted it - } + void editRequestSubmissionAuditLog() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); - @Test - @DisplayName("When an edit request has been rejected, the submitter and shared-with users should be notified") - void rejectedEditRequest() { + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response firstResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + + assertResponseCode(firstResponse, 201); + // TODO: Finish test here when https://tools.hmcts.net/jira/browse/S28-3556 is done + // Response auditResponse = doGetRequest(AUDIT_ENDPOINT...) } @Test @DisplayName("When an edit request has been approved, it should be picked up for processing") - void approvedEditRequest() { - // Not sure how this transition works in practice. Perhaps we won't need the PENDING status any more? + void approvedEditRequest() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); + + // Submit + createEditRequestDTO.setStatus(EditRequestStatus.APPROVED); + String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); + + Response firstResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(firstResponse, 201); + + } @@ -307,4 +320,17 @@ void editRequestWithUnsafeData() { } + private CreateEditRequestDTO bogStandardCreateEditRequestDTO() { + 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; + } } From 0a55160f0ff6b13bc7435973d2ba6bbeea131c51 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 15:12:10 +0100 Subject: [PATCH 33/37] Code quality --- .../preapi/email/govnotify/templates/EditEmailParameters.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index fcaeb811fc..f79cd31007 100644 --- 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 @@ -40,7 +40,7 @@ public EditEmailParameters(EditRequest editRequest, String portalUrl) { String summary = generateEditSummary(requestInstructions); this.jointlyAgreed = editRequest.getJointlyAgreed(); - String jointlyAgreedText = jointlyAgreed ? "Yes" : "No"; + String jointlyAgreedText = editRequest.getJointlyAgreed() ? "Yes" : "No"; this.emailParameters = Map.of( "edit_summary", summary, From 40deeb0d0c57c16a915e69a91197ae70f4f7481b Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Wed, 22 Jul 2026 16:31:22 +0100 Subject: [PATCH 34/37] Add test for transition from approved to processing --- .../EditControllerFullyAutomatedFT.java | 41 ++++++++++--------- .../preapi/util/FunctionalTestBase.java | 29 +++++++++++++ 2 files changed, 51 insertions(+), 19 deletions(-) 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 index b2f5a08756..397b6e0471 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -17,6 +17,7 @@ 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.ResourceInWrongStateException; @@ -30,6 +31,7 @@ import java.util.UUID; import static java.lang.String.format; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @@ -222,7 +224,7 @@ void editRequestSuccess() throws JsonProcessingException { @Test @DisplayName("An edit request should be read-only once submitted") void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { - CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); // Submit createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); @@ -275,7 +277,7 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { @Test @DisplayName("Should record an audit trail when edit request is submitted") void editRequestSubmissionAuditLog() throws JsonProcessingException { - CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); // Submit createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); @@ -296,20 +298,35 @@ void editRequestSubmissionAuditLog() throws JsonProcessingException { @Test @DisplayName("When an edit request has been approved, it should be picked up for processing") void approvedEditRequest() throws JsonProcessingException { - CreateEditRequestDTO createEditRequestDTO = bogStandardCreateEditRequestDTO(); + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); // Submit createEditRequestDTO.setStatus(EditRequestStatus.APPROVED); String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); - Response firstResponse = doPutRequest( + Response putResponse = doPutRequest( EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), requestBody, TestingSupportRoles.SUPER_USER ); - assertResponseCode(firstResponse, 201); + assertResponseCode(putResponse, 201); + + EditRequest approvedEditRequest = getEditRequest(createEditRequestDTO.getId()); + assertThat(approvedEditRequest.getStatus()).isEqualTo(EditRequestStatus.APPROVED); + // Manually trigger cron job: in prod, this is scheduled to run every N minutes + Response triggerPerformEditResponse = doPostRequest( + TRIGGER_TASK_ENDPOINT + "/PerformEditRequest", + "", // Empty body + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(triggerPerformEditResponse, 204); + EditRequest processingEditRequest = getEditRequest(createEditRequestDTO.getId()); + assertThat(processingEditRequest.getStatus()).isEqualTo(EditRequestStatus.PROCESSING); + + // Not a full test as we're not waiting for it to fully process. This is just to check that + // the edit request is picked up for processing once it is approved. } @@ -319,18 +336,4 @@ void editRequestWithUnsafeData() { // Copy and rewrite the existing test to use the `PUT edits/{id}` endpoint instead of the CSV endpoint } - - private CreateEditRequestDTO bogStandardCreateEditRequestDTO() { - 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; - } } 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..042bc1f7de 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 @@ -17,12 +17,16 @@ 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.entities.EditRequest; import uk.gov.hmcts.reform.preapi.enums.CaseState; import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.ParticipantType; @@ -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 = "/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 EditRequest getEditRequest(UUID id) { + Response response = doGetRequest( + EDIT_ENDPOINT + "/" + id, + TestingSupportRoles.SUPER_USER + ); + assertResponseCode(response, 201); + return response.body().as(EditRequest.class); + } + protected CreateParticipantDTO convertDtoToCreateDto(ParticipantDTO dto) { var create = new CreateParticipantDTO(); create.setId(dto.getId()); @@ -437,6 +452,20 @@ 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 CreateRecordingDTO createRecording(UUID captureSessionId) { var dto = new CreateRecordingDTO(); dto.setId(UUID.randomUUID()); From 1d0fcdcdb213992658641143d7d1e804b020a87a Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 23 Jul 2026 08:19:37 +0100 Subject: [PATCH 35/37] Corrections --- .../reform/preapi/controllers/EditControllerFT.java | 9 +++++---- .../gov/hmcts/reform/preapi/util/FunctionalTestBase.java | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) 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/util/FunctionalTestBase.java b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/util/FunctionalTestBase.java index 042bc1f7de..2f8fb00ce5 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 @@ -67,7 +67,7 @@ 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 = "/trigger-task"; + protected static final String TRIGGER_TASK_ENDPOINT = "/testing-support/trigger-task"; protected static final String EDIT_ENDPOINT = "/edits"; protected static final Map MULTIPART_HEADERS = @@ -361,13 +361,13 @@ protected Response putCaptureSession(CreateCaptureSessionDTO dto) throws JsonPro ); } - protected EditRequest getEditRequest(UUID id) { + protected EditRequestDTO getEditRequest(UUID id) { Response response = doGetRequest( EDIT_ENDPOINT + "/" + id, TestingSupportRoles.SUPER_USER ); - assertResponseCode(response, 201); - return response.body().as(EditRequest.class); + assertResponseCode(response, 200); + return response.body().as(EditRequestDTO.class); } protected CreateParticipantDTO convertDtoToCreateDto(ParticipantDTO dto) { From 7013a5eb3e41a17c9c097bd5a7de73fc8ad77682 Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 23 Jul 2026 09:45:29 +0100 Subject: [PATCH 36/37] Correct status codes --- .../preapi/controllers/EditControllerFullyAutomatedFT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 397b6e0471..0654989661 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -213,7 +213,7 @@ void editRequestSuccess() throws JsonProcessingException { OBJECT_MAPPER.writeValueAsString(createEditRequestDTO), TestingSupportRoles.SUPER_USER ); - assertResponseCode(putResponse, 200); + assertResponseCode(putResponse, 204); return doGetRequest( EDIT_ENDPOINT + "/" + createEditRequestId, @@ -238,7 +238,7 @@ void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { TestingSupportRoles.SUPER_USER ); - assertResponseCode(firstResponse, 201); + assertResponseCode(firstResponse, 204); String firstResponseBody = firstResponse.body().asString(); @@ -289,7 +289,7 @@ void editRequestSubmissionAuditLog() throws JsonProcessingException { TestingSupportRoles.SUPER_USER ); - assertResponseCode(firstResponse, 201); + assertResponseCode(firstResponse, 204); // TODO: Finish test here when https://tools.hmcts.net/jira/browse/S28-3556 is done // Response auditResponse = doGetRequest(AUDIT_ENDPOINT...) From 645d94ee2b14f9a4caa7f8f5427ae38b4134f3bf Mon Sep 17 00:00:00 2001 From: Lydia Ralph Date: Thu, 23 Jul 2026 14:15:01 +0100 Subject: [PATCH 37/37] Corrections --- .../EditControllerFullyAutomatedFT.java | 247 ++++++------------ .../preapi/util/FunctionalTestBase.java | 18 +- 2 files changed, 96 insertions(+), 169 deletions(-) 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 index 0654989661..35c8470d95 100644 --- a/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java +++ b/src/functionalTest/java/uk/gov/hmcts/reform/preapi/controllers/EditControllerFullyAutomatedFT.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.restassured.response.Response; import org.assertj.core.api.Assertions; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -11,16 +10,13 @@ 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.EditRequestDTO; 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.EditRequest; import uk.gov.hmcts.reform.preapi.entities.Recording; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase; @@ -32,7 +28,6 @@ import static java.lang.String.format; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; 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 @@ -81,16 +76,16 @@ void setUp() { when(mockCourt.getGroupEmail()).thenReturn("mock@email.com"); recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true) - .as(RecordingDTO.class); + .as(RecordingDTO.class); when(azureFinalStorageService.getMp4FileName(recordingDetails.recordingId().toString())) - .thenReturn(recordingDTO.getFilename()); + .thenReturn(recordingDTO.getFilename()); when(azureFinalStorageService.getRecordingDuration(recordingDetails.recordingId())) - .thenReturn(recordingDTO.getDuration()); + .thenReturn(recordingDTO.getDuration()); } @Test - @DisplayName("Should create a DRAFT edit request, update it and submit it, receiving a notification") + @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(); @@ -102,38 +97,38 @@ void editRequestSuccess() throws JsonProcessingException { Response createdAsDraft = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); assertResponseCode(createdAsDraft, 200); Assertions.assertThat(createdAsDraft.jsonPath().getString("id")) - .isEqualTo(createEditRequestId.toString()); + .isEqualTo(createEditRequestId.toString()); Assertions.assertThat(createdAsDraft.jsonPath().getString("status")) - .isEqualTo(EditRequestStatus.DRAFT.name()); + .isEqualTo(EditRequestStatus.DRAFT.name()); Assertions.assertThat(createdAsDraft.jsonPath().getString("source_recording.id")) - .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); + .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); Assertions.assertThat(createdAsDraft.jsonPath().getList("edit_instruction.requestedInstructions")) - .isEmpty(); + .isEmpty(); // Update as DRAFT List editInstructions = List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:02") - .endOfCut("00:00:03") - .build()); + .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()); + .isEqualTo(createEditRequestId.toString()); Assertions.assertThat(updatedAsDraft.jsonPath().getString("status")) - .isEqualTo(EditRequestStatus.DRAFT.name()); + .isEqualTo(EditRequestStatus.DRAFT.name()); Assertions.assertThat(updatedAsDraft.jsonPath().getString("source_recording.id")) - .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); + .isEqualTo(createEditRequestDTO.getSourceRecordingId().toString()); Assertions.assertThat(updatedAsDraft.jsonPath().getList("edit_instruction.requestedInstructions")) - .size().isEqualTo(editInstructions.size()); + .size().isEqualTo(editInstructions.size()); Assertions.assertThat(updatedAsDraft.jsonPath() - .getInt("edit_instruction.requestedInstructions[0].start")) - .isEqualTo(2); + .getInt("edit_instruction.requestedInstructions[0].start")) + .isEqualTo(2); Assertions.assertThat(updatedAsDraft.jsonPath() - .getInt("edit_instruction.requestedInstructions[0].end")) - .isEqualTo(3); + .getInt("edit_instruction.requestedInstructions[0].end")) + .isEqualTo(3); // Submit createEditRequestDTO.setStatus(EditRequestStatus.SUBMITTED); @@ -141,199 +136,115 @@ void editRequestSuccess() throws JsonProcessingException { Response submitted = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); assertResponseCode(submitted, 200); Assertions.assertThat(submitted.jsonPath().getString("id")) - .isEqualTo(createEditRequestId.toString()); + .isEqualTo(createEditRequestId.toString()); Assertions.assertThat(submitted.jsonPath().getString("status")) - .isEqualTo(EditRequestStatus.SUBMITTED.name()); + .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()); + .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 + 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() - )); - - // ...but should be allowed to update status with original instructions - createEditRequestDTO.setStatus(EditRequestStatus.DRAFT); - createEditRequestDTO.setEditInstructions(editInstructions); - - Response setBackToDraft = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); - - assertResponseCode(setBackToDraft, 200); - Assertions.assertThat(setBackToDraft.jsonPath().getString("id")) - .isEqualTo(createEditRequestId.toString()); - Assertions.assertThat(setBackToDraft.jsonPath().getString("status")) - .isEqualTo(EditRequestStatus.DRAFT.name()); - 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); - - - // ...and then we're allowed to edit it again - createEditRequestDTO.setEditInstructions(updatedEditInstructions); - - Response updateEditInstructions = upsertEditRequestAndGetResponse(createEditRequestId, createEditRequestDTO); - assertResponseCode(updateEditInstructions, 200); - Assertions.assertThat(updateEditInstructions.jsonPath().getString("id")) - .isEqualTo(createEditRequestId.toString()); - Assertions.assertThat(updateEditInstructions.jsonPath().getString("status")) - .isEqualTo(EditRequestStatus.DRAFT.name()); - Assertions.assertThat(updateEditInstructions.jsonPath().getList("edit_instruction.requestedInstructions")) - .size().isEqualTo(editInstructions.size()); - Assertions.assertThat(updateEditInstructions.jsonPath() - .getInt("edit_instruction.requestedInstructions[0].start")) - .isEqualTo(6); - Assertions.assertThat(updateEditInstructions.jsonPath() - .getInt("edit_instruction.requestedInstructions[0].end")) - .isEqualTo(7); - } - - private @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 - ); + .isEqualTo(format( + "Cannot alter edit request instructions after submission: " + + "edit request %s has status %s", + createEditRequestDTO.getId(), createEditRequestDTO.getStatus().toString() + )); } @Test - @DisplayName("An edit request should be read-only once submitted") - void editRequestShouldBeReadOnlyOnceSubmitted() throws JsonProcessingException { + @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); - // Fails on message com.fasterxml.jackson.databind.exc.MismatchedInputException: - // No content to map due to end-of-input Response firstResponse = doPutRequest( - EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), - requestBody, - TestingSupportRoles.SUPER_USER + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER ); assertResponseCode(firstResponse, 204); - String firstResponseBody = firstResponse.body().asString(); - - Assertions.assertThat(firstResponseBody).isNotBlank(); - - EditRequestDTO submitted = OBJECT_MAPPER.readValue(firstResponseBody, EditRequestDTO.class); - - Assertions.assertThat(submitted.getStatus()).isEqualTo(EditRequestStatus.SUBMITTED); - Assertions.assertThat(submitted.getEditInstruction().getRequestedInstructions()) - .isEqualTo(createEditRequestDTO.getEditInstructions()); - Assertions.assertThat(submitted.getSourceRecording()).isEqualTo(recordingDTO); - - // Should be read-only once submitted - List updatedInstructions = List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build()); - createEditRequestDTO.setEditInstructions(updatedInstructions); - String resubmittedRequest = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); - String message = assertThrows( - ResourceInWrongStateException.class, - () -> doPutRequest( - EDIT_ENDPOINT + "/" + recordingId, - resubmittedRequest, - TestingSupportRoles.SUPER_USER - ) - ).getMessage(); - - Assertions.assertThat(message) - .isEqualTo( - "Cannot resubmit edit request {}: submit a new edit request", - createEditRequestDTO.getId() - ); + // TODO: Finish test here when https://tools.hmcts.net/jira/browse/S28-3556 is done + // Response auditResponse = doGetRequest(AUDIT_ENDPOINT...) } @Test - @DisplayName("Should record an audit trail when edit request is submitted") - void editRequestSubmissionAuditLog() throws JsonProcessingException { + @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.SUBMITTED); + createEditRequestDTO.setStatus(EditRequestStatus.REJECTED); + createEditRequestDTO.setRejectionReason("this & is unsafe"); String requestBody = OBJECT_MAPPER.writeValueAsString(createEditRequestDTO); - Response firstResponse = doPutRequest( - EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), - requestBody, - TestingSupportRoles.SUPER_USER + Response putResponse = doPutRequest( + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER ); + assertResponseCode(putResponse, 400); - assertResponseCode(firstResponse, 204); - - // TODO: Finish test here when https://tools.hmcts.net/jira/browse/S28-3556 is done - // Response auditResponse = doGetRequest(AUDIT_ENDPOINT...) + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); } @Test - @DisplayName("When an edit request has been approved, it should be picked up for processing") - void approvedEditRequest() throws JsonProcessingException { + @DisplayName("Should not create an edit request with unsafe data in approved by fields") + void editRequestWithUnsafeDataApprovedBy() throws JsonProcessingException { CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); - // Submit + // 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 + EDIT_ENDPOINT + "/" + createEditRequestDTO.getId(), + requestBody, + TestingSupportRoles.SUPER_USER ); - assertResponseCode(putResponse, 201); - - EditRequest approvedEditRequest = getEditRequest(createEditRequestDTO.getId()); - assertThat(approvedEditRequest.getStatus()).isEqualTo(EditRequestStatus.APPROVED); + assertResponseCode(putResponse, 400); - // Manually trigger cron job: in prod, this is scheduled to run every N minutes - Response triggerPerformEditResponse = doPostRequest( - TRIGGER_TASK_ENDPOINT + "/PerformEditRequest", - "", // Empty body - TestingSupportRoles.SUPER_USER - ); - assertResponseCode(triggerPerformEditResponse, 204); + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); + } - EditRequest processingEditRequest = getEditRequest(createEditRequestDTO.getId()); - assertThat(processingEditRequest.getStatus()).isEqualTo(EditRequestStatus.PROCESSING); + @Test + @DisplayName("Should not create an edit request with unsafe data in reason fields") + void editRequestWithUnsafeDataReason() throws JsonProcessingException { + CreateEditRequestDTO createEditRequestDTO = createEditRequestDTO(recordingId); - // Not a full test as we're not waiting for it to fully process. This is just to check that - // the edit request is picked up for processing once it is approved. - } + // 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); - @Test - @DisplayName("Should not create an edit request with unsafe data in fields") - void editRequestWithUnsafeData() { - // Copy and rewrite the existing test to use the `PUT edits/{id}` endpoint instead of the CSV endpoint + assertThat(putResponse.getBody().asString()).contains("contains potentially malicious content"); } } 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 2f8fb00ce5..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; @@ -26,7 +27,6 @@ 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.entities.EditRequest; import uk.gov.hmcts.reform.preapi.enums.CaseState; import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.ParticipantType; @@ -466,6 +466,22 @@ protected CreateEditRequestDTO createEditRequestDTO(UUID recordingId) { 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());