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 a4c5ca6a4d..7945f629c7 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 @@ -8,8 +8,8 @@ import org.junit.jupiter.params.provider.NullSource; 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.EditCutInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; @@ -47,13 +47,13 @@ void editFromCsvSuccess() throws JsonProcessingException { ).as(EditRequestDTO.class); assertThat(postResponse.getId()).isNotNull(); - assertThat(postResponse.getSourceRecording().getId()).isEqualTo(recordingDetails.recordingId()); + assertThat(postResponse.getSourceRecordingId()).isEqualTo(recordingDetails.recordingId()); assertThat(postResponse.getStatus()).isEqualTo(EditRequestStatus.PENDING); - var instructions = postResponse.getEditInstruction(); - assertThat(postResponse.getEditInstruction()).isNotNull(); + List instructions = postResponse.getEditCutInstructions(); + assertThat(postResponse.getEditCutInstructions()).isNotNull(); - List requestedInstructions = instructions.getRequestedInstructions(); + List requestedInstructions = instructions.getRequestedInstructions(); assertThat(requestedInstructions).isNotEmpty(); assertThat(requestedInstructions.size()).isEqualTo(2); 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..7beead013f 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,13 @@ 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 static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,32 +44,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 +264,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 +296,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); + var 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 +330,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/controllers/EditController.java b/src/main/java/uk/gov/hmcts/reform/preapi/controllers/EditController.java index 84291ef6fa..3e835b2746 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 @@ -30,8 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import uk.gov.hmcts.reform.preapi.controllers.base.PreApiController; 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.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.exception.BadRequestException; import uk.gov.hmcts.reform.preapi.exception.PathPayloadMismatchException; import uk.gov.hmcts.reform.preapi.exception.RequestedPageOutOfRangeException; @@ -114,22 +113,22 @@ public HttpEntity>> searchEdits( @PutMapping("/{id}") @PreAuthorize("hasAnyRole('ROLE_SUPER_USER', 'ROLE_LEVEL_1', 'ROLE_LEVEL_3')") - public ResponseEntity upsertEditRequest( + public ResponseEntity upsertEditRequest( @PathVariable("id") UUID id, - @Valid @RequestBody CreateEditRequestDTO createEditRequestDTO + @Valid @RequestBody EditRequestDTO createEditRequestDTO ) { if (!id.equals(createEditRequestDTO.getId())) { throw new PathPayloadMismatchException("editRequestId", "createEditRequestDTO.id"); } - return getUpsertResponse(editRequestService.upsert(createEditRequestDTO), id); + return ResponseEntity.ok(editRequestService.upsert(createEditRequestDTO)); } @DeleteMapping("/{id}") @PreAuthorize("hasAnyRole('ROLE_SUPER_USER', 'ROLE_LEVEL_1', 'ROLE_LEVEL_3')") public ResponseEntity delete( @PathVariable("id") UUID id, - @Valid @RequestBody CreateEditRequestDTO deleteEditRequestDTO + @Valid @RequestBody EditRequestDTO deleteEditRequestDTO ) { if (!id.equals(deleteEditRequestDTO.getId())) { throw new PathPayloadMismatchException("editRequestId", "deleteEditRequestDTO.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 f845f30c1c..9b124864f8 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 @@ -28,7 +28,7 @@ import uk.gov.hmcts.reform.preapi.batch.repositories.MigrationRecordRepository; import uk.gov.hmcts.reform.preapi.controllers.params.TestingSupportRoles; import uk.gov.hmcts.reform.preapi.dto.BookingDTO; -import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; import uk.gov.hmcts.reform.preapi.dto.migration.VfMigrationRecordDTO; import uk.gov.hmcts.reform.preapi.entities.AppAccess; diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateEditRequestDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateEditRequestDTO.java deleted file mode 100644 index 4172b50ff2..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateEditRequestDTO.java +++ /dev/null @@ -1,53 +0,0 @@ -package uk.gov.hmcts.reform.preapi.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Data; -import lombok.NoArgsConstructor; -import uk.gov.hmcts.reform.preapi.dto.validators.CreateEditRequestStatusConstraint; -import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; - -import java.sql.Timestamp; -import java.util.List; -import java.util.UUID; - -@Data -@NoArgsConstructor -@CreateEditRequestStatusConstraint -@Schema(description = "CreateEditRequestDTO") -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public class CreateEditRequestDTO { - @NotNull - @Schema(description = "CreateEditRequestId") - private UUID id; - - @NotNull - @Schema(description = "CreateEditRequestSourceRecordingId") - private UUID sourceRecordingId; - - @NotNull - @Schema(description = "CreateEditRequestStatus") - private EditRequestStatus status; - - @Valid - @Schema(description = "CreateEditRequestInstructions") - private List editInstructions; - - @Schema(description = "CreateEditRequestJointlyAgreed") - private Boolean jointlyAgreed; - - @Size(max = 512) - @Schema(description = "CreateEditRequestRejectionReason") - private String rejectionReason; - - @Schema(description = "CreateEditRequestApprovedAt") - private Timestamp approvedAt; - - @Size(max = 100) - @Schema(description = "CreateEditRequestApprovedBy") - private String approvedBy; -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTO.java index e9253712ce..03806ecd2c 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTO.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTO.java @@ -9,7 +9,6 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.preapi.dto.base.BaseRecordingDTO; -import uk.gov.hmcts.reform.preapi.entities.Recording; import java.util.UUID; @@ -24,19 +23,6 @@ public class CreateRecordingDTO extends BaseRecordingDTO { @NotNull(message = "capture_session_id is required") protected UUID captureSessionId; - public CreateRecordingDTO(Recording recording) { - super(); - id = recording.getId(); - captureSessionId = recording.getCaptureSession().getId(); - if (recording.getParentRecording() != null) { - parentRecordingId = recording.getParentRecording().getId(); - } - version = recording.getVersion(); - filename = recording.getFilename(); - duration = recording.getDuration(); - editInstructions = recording.getEditInstruction(); - } - public CreateRecordingDTO(RecordingDTO recordingDTO) { super(); id = recordingDTO.getId(); @@ -45,6 +31,27 @@ public CreateRecordingDTO(RecordingDTO recordingDTO) { version = recordingDTO.getVersion(); filename = recordingDTO.getFilename(); duration = recordingDTO.getDuration(); + editRequest = recordingDTO.getEditRequest(); + editStatus = recordingDTO.getEditStatus(); + editInstructions = recordingDTO.getEditInstructions(); + } + + public CreateRecordingDTO(UUID newRecordingId, + String providedFileName, + Integer providedVersionNumber, + RecordingDTO recordingDTO) { + super(); + id = newRecordingId; + version = providedVersionNumber; + filename = providedFileName; + + parentRecordingId = recordingDTO.getParentRecordingId() == null + ? recordingDTO.getId() + : recordingDTO.getParentRecordingId(); + captureSessionId = recordingDTO.getCaptureSession().getId(); + duration = recordingDTO.getDuration(); + editRequest = recordingDTO.getEditRequest(); + editStatus = recordingDTO.getEditStatus(); editInstructions = recordingDTO.getEditInstructions(); } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTO.java deleted file mode 100644 index cfd52e9a2d..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTO.java +++ /dev/null @@ -1,96 +0,0 @@ -package uk.gov.hmcts.reform.preapi.dto; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.opencsv.bean.CsvBindByName; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import uk.gov.hmcts.reform.preapi.exception.BadRequestException; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "EditCutInstructionDTO") -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public class EditCutInstructionDTO { - // CSV also contains these columns: - // - Edit Number - // - Total time removed - - @NotNull - @CsvBindByName(column = "Start time of cut") - @Schema(description = "EditInstructionStart") - @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", message = "must be in format HH:MM:SS") - private String startOfCut; - - @NotNull - @CsvBindByName(column = "End time of cut") - @Schema(description = "EditInstructionEnd") - @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", message = "must be in format HH:MM:SS") - private String endOfCut; - - @CsvBindByName(column = "Reason") - private String reason; - - @Schema(hidden = true) - private Long start; - - @Schema(hidden = true) - private Long end; - - public long getStart() { - if (start != null) { - return start; - } - start = parseTime(startOfCut); - return start; - } - - public long getEnd() { - if (end != null) { - return end; - } - end = parseTime(endOfCut); - return end; - } - - private static long parseTime(String time) { - try { - String[] units = time.split(":"); - int hours = Integer.parseInt(units[0]); - int minutes = Integer.parseInt(units[1]); - int seconds = Integer.parseInt(units[2]); - - return hours * 3600L + minutes * 60L + seconds; - } catch (NullPointerException | IndexOutOfBoundsException | NumberFormatException e) { - throw new BadRequestException("Invalid time format: " + time + ". Must be in the form HH:MM:SS", e); - } - } - - public static String formatTime(long time) { - if (time < 0) { - throw new IllegalArgumentException("Time in seconds cannot be negative: " + time); - } - - long hours = time / 3600; - long minutes = time % 3600 / 60; - long seconds = time % 60; - - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - - public EditCutInstructionDTO(long start, long end, String reason) { - this.start = start; - this.end = end; - this.reason = reason; - - this.startOfCut = formatTime(start); - this.endOfCut = formatTime(end); - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/FfmpegEditInstructionDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/FfmpegEditInstructionDTO.java deleted file mode 100644 index bf8ee28185..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/FfmpegEditInstructionDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package uk.gov.hmcts.reform.preapi.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class FfmpegEditInstructionDTO { - private long start; - private long end; -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTO.java index b228872645..55e994a749 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTO.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTO.java @@ -7,7 +7,10 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import uk.gov.hmcts.reform.preapi.dto.base.BaseRecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.entities.Case; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.Participant; import uk.gov.hmcts.reform.preapi.entities.Recording; @@ -21,6 +24,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static uk.gov.hmcts.reform.preapi.utils.StringTools.isBlankString; + @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @@ -52,12 +57,6 @@ public class RecordingDTO extends BaseRecordingDTO { @Schema(description = "RecordingTotalVersionCount") private int totalVersionCount; - @Schema(description = "RecordingEditRequests") - private List editRequests; - - @Schema(description = "RecordingEditStatus") - private EditRequestStatus editStatus; - public RecordingDTO(Recording recording) { super(); id = recording.getId(); @@ -72,7 +71,6 @@ public RecordingDTO(Recording recording) { version = recording.getVersion(); filename = recording.getFilename(); duration = recording.getDuration(); - editInstructions = recording.getEditInstruction(); deletedAt = recording.getDeletedAt(); createdAt = recording.getCreatedAt(); Case caseEntity = recording.getCaptureSession().getBooking().getCaseId(); @@ -87,16 +85,32 @@ public RecordingDTO(Recording recording) { .sorted(Comparator.comparing(Participant::getFirstName)) .map(ParticipantDTO::new)) .collect(Collectors.toList()); - editRequests = Stream.ofNullable(recording.getEditRequests()) - .flatMap(request -> - request - .stream() - .sorted(Comparator.comparing(EditRequest::getModifiedAt).reversed()) - .map(e -> new EditRequestDTO(e, false))) - .collect(Collectors.toList()); - if (recording.getVersion() == 1 && !editRequests.isEmpty()) { - editStatus = editRequests.getFirst().getStatus(); + if (recording.getEditRequest() != null) { + editInstructions = recording.getEditRequest().getEditCutInstructionsAsJson(); + editRequest = new EditRequestDTO(recording.getEditRequest()); + editStatus = editRequest.getStatus(); + } + + if (recording.getVersion() == 1) { + editStatus = EditRequestStatus.ORIGINAL; } } + + public List getEditCutInstructionsLegacyProof() { + // Default to new-style instructions + if (this.getEditRequest() != null && !this.getEditRequest().getEditCutInstructions().isEmpty()) { + return this.getEditRequest().getEditCutInstructions(); + } + + // For legacy edit instructions + if (!isBlankString(this.getEditInstructions())) { + List editCutInstructionsList = + EditRequest.convertEditCutInstructionsFromJson(this.getEditInstructions()); + + return EditRequestDTO.toDTO(editCutInstructionsList); + } + + return List.of(); + } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/base/BaseRecordingDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/base/BaseRecordingDTO.java index d77b16c0f0..498a773d18 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/base/BaseRecordingDTO.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/base/BaseRecordingDTO.java @@ -7,7 +7,9 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.validators.JsonConstraint; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import java.time.Duration; import java.util.UUID; @@ -43,7 +45,14 @@ public abstract class BaseRecordingDTO { @JsonFormat(shape = JsonFormat.Shape.STRING) protected Duration duration; + @Deprecated @Schema(description = "RecordingEditInstructions") @JsonConstraint protected String editInstructions; + + @Schema(description = "RecordingEditRequest") + protected EditRequestDTO editRequest; + + @Schema(description = "RecordingEditStatus") + protected EditRequestStatus editStatus; } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditCutInstructionsDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditCutInstructionsDTO.java new file mode 100644 index 0000000000..df585e6d9b --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditCutInstructionsDTO.java @@ -0,0 +1,141 @@ +package uk.gov.hmcts.reform.preapi.dto.edit; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.opencsv.bean.CsvBindByName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Builder; +import lombok.Data; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +import uk.gov.hmcts.reform.preapi.exception.BadRequestException; + +import java.time.LocalTime; +import java.util.UUID; + +import static java.lang.String.format; + +@Builder(toBuilder = true) +@Data +@Schema(description = "EditCutInstructionsDTO") +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class EditCutInstructionsDTO { + // CSV also contains these columns: + // - Edit Number + // - Total time removed + + private UUID editRequestId; + + @NotNull + @CsvBindByName(column = "Start time of cut") + @Schema(description = "EditInstructionStart") + @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", message = "must be in format HH:MM:SS") + private LocalTime startOfCut; + + @NotNull + @CsvBindByName(column = "End time of cut") + @Schema(description = "EditInstructionEnd") + @Pattern(regexp = "^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$", message = "must be in format HH:MM:SS") + private LocalTime endOfCut; + + @CsvBindByName(column = "Reason") + private String reason; + + @Schema(hidden = true) + private Integer start; + + @Schema(hidden = true) + private Integer end; + + + // Requested instructions are marshalled as JSON into a formatted, human-readable version + // E.g. "00:01:30" + // They are converted to integers (ffmpeg-style) for storing in the database + + public Integer getStart() { + if (start != null) { + return start; + } + start = formatTimeAsInteger(startOfCut); + return start; + } + + public Integer getEnd() { + if (end != null) { + return end; + } + end = formatTimeAsInteger(endOfCut); + return end; + } + + public static Integer formatTimeAsInteger(LocalTime localTime) { + if (localTime == null) { + return null; + } + return localTime.toSecondOfDay(); + + } + + 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); + } + + public static LocalTime formatTimeAsLocalTime(Integer time) { + String timeString = formatTimeAsString(time); + + return LocalTime.parse(timeString); + } + + public EditCutInstructionsDTO(UUID editRequestId, String startTime, String endTime, String reason) { + try { + this.editRequestId = editRequestId; + this.startOfCut = LocalTime.parse(startTime); + this.endOfCut = LocalTime.parse(endTime); + this.reason = reason; + } catch (java.time.format.DateTimeParseException e) { + throw new BadRequestException(format("Invalid time format: %s. Must be in the form HH:MM:SS", + e.getParsedString())); + } catch (java.lang.NullPointerException e) { + throw new BadRequestException("Invalid time format: null. Must be in the form HH:MM:SS"); + } + } + + public EditCutInstructionsDTO(UUID editRequestId, LocalTime startTime, LocalTime endTime, String reason, + Integer start, Integer end) { + try { + this.editRequestId = editRequestId; + this.startOfCut = startTime; + this.endOfCut = endTime; + this.reason = reason; + this.start = start; + this.end = end; + } catch (java.time.format.DateTimeParseException e) { + throw new BadRequestException(format("Invalid time format: %s. Must be in the form HH:MM:SS", + e.getParsedString())); + } catch (java.lang.NullPointerException e) { + throw new BadRequestException("Invalid time format: null. Must be in the form HH:MM:SS"); + } + } + + public EditCutInstructionsDTO(EditCutInstructions editCutInstructions) { + this.editRequestId = editCutInstructions.getEditRequestId(); + + this.startOfCut = formatTimeAsLocalTime(editCutInstructions.getStart()); + this.endOfCut = formatTimeAsLocalTime(editCutInstructions.getEnd()); + + this.start = editCutInstructions.getStart(); + this.end = editCutInstructions.getEnd(); + + this.reason = editCutInstructions.getReason(); + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTO.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditRequestDTO.java similarity index 64% rename from src/main/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTO.java rename to src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditRequestDTO.java index 247749cc5f..db0c59378f 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTO.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/edit/EditRequestDTO.java @@ -1,20 +1,22 @@ -package uk.gov.hmcts.reform.preapi.dto; +package uk.gov.hmcts.reform.preapi.dto.edit; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; import java.sql.Timestamp; +import java.util.List; import java.util.UUID; - -import static uk.gov.hmcts.reform.preapi.media.edit.EditInstructions.fromJson; +import java.util.stream.Collectors; @Data @Builder @@ -22,14 +24,21 @@ @AllArgsConstructor @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class EditRequestDTO { + + @NotNull @Schema(description = "EditRequestId") private UUID id; + @NotNull @Schema(description = "EditRequestSourceRecording") - private RecordingDTO sourceRecording; + private UUID sourceRecordingId; + + @Schema(description = "EditRequestOutputRecording") + private UUID outputRecordingId; + @NotNull @Schema(description = "EditRequestEditInstruction") - private EditInstructions editInstruction; + private List editCutInstructions; @Schema(description = "EditRequestStatus") private EditRequestStatus status; @@ -55,19 +64,22 @@ public class EditRequestDTO { @Schema(description = "EditRequestJointlyAgreed") private Boolean jointlyAgreed; + @Size(max = 512) @Schema(description = "EditRequestRejectionReason") private String rejectionReason; @Schema(description = "EditRequestApprovedAt") private Timestamp approvedAt; + @Size(max = 100) @Schema(description = "EditRequestApprovedBy") private String approvedBy; public EditRequestDTO(EditRequest editRequest) { this.id = editRequest.getId(); - this.sourceRecording = new RecordingDTO(editRequest.getSourceRecording()); - this.editInstruction = fromJson(editRequest.getEditInstruction()); + this.sourceRecordingId = editRequest.getSourceRecordingId(); + this.outputRecordingId = editRequest.getOutputRecordingId(); + this.editCutInstructions = toDTO(editRequest.getEditCutInstructions()); this.status = editRequest.getStatus(); this.startedAt = editRequest.getStartedAt(); this.finishedAt = editRequest.getFinishedAt(); @@ -81,22 +93,18 @@ public EditRequestDTO(EditRequest editRequest) { this.approvedBy = editRequest.getApprovedBy(); } - public EditRequestDTO(EditRequest editRequest, boolean includeSourceRecording) { - this.id = editRequest.getId(); - if (includeSourceRecording) { - this.sourceRecording = new RecordingDTO(editRequest.getSourceRecording()); + public static List toDTO(List editInstructions) { + if (editInstructions == null) { + return null; } - this.editInstruction = fromJson(editRequest.getEditInstruction()); - this.status = editRequest.getStatus(); - this.startedAt = editRequest.getStartedAt(); - this.finishedAt = editRequest.getFinishedAt(); - this.createdById = editRequest.getCreatedBy().getId(); - this.createdAt = editRequest.getCreatedAt(); - this.createdBy = editRequest.getCreatedBy().getFullName(); - this.modifiedAt = editRequest.getModifiedAt(); - this.jointlyAgreed = editRequest.getJointlyAgreed(); - this.rejectionReason = editRequest.getRejectionReason(); - this.approvedAt = editRequest.getApprovedAt(); - this.approvedBy = editRequest.getApprovedBy(); + return editInstructions.stream().map(EditCutInstructionsDTO::new).collect(Collectors.toList()); } + + public static List fromDTO(List editInstructions) { + if (editInstructions == null) { + return null; + } + return editInstructions.stream().map(EditCutInstructions::new).collect(Collectors.toList()); + } + } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidator.java b/src/main/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidator.java index a7a4bc9653..e55ec76e69 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidator.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidator.java @@ -2,18 +2,18 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; public class CreateEditRequestStatusValidator - implements ConstraintValidator { + implements ConstraintValidator { @Override public void initialize(CreateEditRequestStatusConstraint constraintAnnotation) { } @Override - public boolean isValid(CreateEditRequestDTO dto, ConstraintValidatorContext cxt) { + public boolean isValid(EditRequestDTO dto, ConstraintValidatorContext cxt) { if (dto.getStatus() == null) { return true; } 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/entities/EditCutInstructions.java b/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditCutInstructions.java new file mode 100644 index 0000000000..a9f000426c --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditCutInstructions.java @@ -0,0 +1,49 @@ +package uk.gov.hmcts.reform.preapi.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.entities.base.BaseEntity; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@NoArgsConstructor +@Table(name = "edit_cut_instructions") +public class EditCutInstructions extends BaseEntity { + + @NotNull + @Column(name = "edit_request_id") + UUID editRequestId; + + @Column(name = "start_edit_seconds") + Integer start; + + @Column(name = "end_edit_seconds") + Integer end; + + @Column(name = "reason") + String reason; + + public EditCutInstructions(UUID editRequestId, Integer start, Integer end, String reason) { + this.editRequestId = editRequestId; + this.start = start; + this.end = end; + this.reason = reason; + } + + public EditCutInstructions(EditCutInstructionsDTO editCutInstructions) { + this.editRequestId = editCutInstructions.getEditRequestId(); + this.start = editCutInstructions.getStart(); + this.end = editCutInstructions.getEnd(); + this.reason = editCutInstructions.getReason(); + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditRequest.java b/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditRequest.java index 357d3e592f..7125bc77b8 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditRequest.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/entities/EditRequest.java @@ -1,36 +1,56 @@ package uk.gov.hmcts.reform.preapi.entities; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +import org.jetbrains.annotations.NotNull; import uk.gov.hmcts.reform.preapi.entities.base.CreatedModifiedAtEntity; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import uk.gov.hmcts.reform.preapi.exception.BadRequestException; +import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; @Getter @Setter @Entity @Table(name = "edit_requests") public class EditRequest extends CreatedModifiedAtEntity { - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "source_recording_id", referencedColumnName = "id", nullable = false) - private Recording sourceRecording; - @Column(name = "edit_instruction", nullable = false) - @JdbcTypeCode(SqlTypes.JSON) - private String editInstruction; + @NotNull + @OneToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "edit_cut_instructions", + joinColumns = @JoinColumn(name = "edit_request_id", referencedColumnName = "id", nullable = false)) + private List editCutInstructions; + + @NotNull + @Column(name = "source_recording_id") + private UUID sourceRecordingId; + + @Column(name = "output_recording_id") + private UUID outputRecordingId; @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, columnDefinition = "edit_request_status") @@ -63,9 +83,9 @@ public class EditRequest extends CreatedModifiedAtEntity { public Map getDetailsForAudit() { Map details = new HashMap<>(); details.put("id", getId()); - details.put("sourceRecordingId", sourceRecording.getId()); + details.put("sourceRecordingId", sourceRecordingId); details.put("status", status); - details.put("editInstruction", editInstruction); + details.put("editInstructions", getEditCutInstructionsAsJson()); details.put("startedAt", startedAt); details.put("finishedAt", finishedAt); details.put("createdBy", createdBy.getId()); @@ -75,4 +95,51 @@ public Map getDetailsForAudit() { details.put("approvedBy", approvedBy); return details; } + + public String getEditCutInstructionsAsJson() { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.writeValueAsString(this.editCutInstructions); + } catch (JsonProcessingException e) { + return null; + } + } + + public static List convertEditCutInstructionsFromJson(@NotNull String editCutInstructionsAsJson) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + List editCutInstructions = new ArrayList<>(); + + try { + JsonNode jsonNode = objectMapper.readTree(editCutInstructionsAsJson); + + String editRequestIdAsString = jsonNode.get("editRequestId").asText(); + UUID editRequestId; + + try { + editRequestId = UUID.fromString(editRequestIdAsString); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Invalid edit request UUID: " + editRequestIdAsString); + } + + // Need to pick out requested instructions as that's the important bit + JsonNode requestedInstructionsNode = jsonNode.path("editInstructions") + .path("requestedInstructions"); + + if (requestedInstructionsNode.isArray()) { + for (JsonNode instruction : requestedInstructionsNode) { + Integer start = instruction.path("start").asInt(); + Integer end = instruction.path("end").asInt(); + String reason = instruction.path("reason").asText(); + editCutInstructions.add(new EditCutInstructions(editRequestId, start, end, reason)); + } + } + + Comparator customComparator = Comparator.comparing(EditCutInstructions::getStart); + return editCutInstructions.stream().sorted(customComparator).toList(); + } catch (JsonProcessingException e) { + throw new UnknownServerException("Unable to read edit instructions", e); + } + } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/entities/Recording.java b/src/main/java/uk/gov/hmcts/reform/preapi/entities/Recording.java index 1063808a77..bad8489769 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/entities/Recording.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/entities/Recording.java @@ -8,6 +8,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import lombok.Getter; @@ -64,8 +65,9 @@ public class Recording extends BaseEntity implements ISoftDeletable { @JdbcTypeCode(SqlTypes.JSON) private String editInstruction; - @OneToMany(mappedBy = "sourceRecording") - private Set editRequests; + @OneToOne + @JoinColumn(name = "parent_recording_id") + private EditRequest editRequest; @Column(name = "deleted_at") private Timestamp deletedAt; @@ -80,6 +82,18 @@ public boolean isDeleted() { return deletedAt != null; } + public String getEditInstruction() { + if (editRequest == null) { + // Backwards compatible: we used to store edit instructions as plain JSON + // But prefer to build JSON string from edit cut instructions if present + if (editInstruction != null) { + return editInstruction; + } + return null; + } + return editRequest.getEditCutInstructionsAsJson(); + } + @Override public Map getDetailsForAudit() { Map details = new HashMap<>(); @@ -89,7 +103,7 @@ public Map getDetailsForAudit() { if (duration != null) { details.put("recordingDuration", duration.toString()); } - details.put("recordingEditInstruction", editInstruction); + details.put("recordingEditInstruction", getEditInstruction()); details.put("deleted", isDeleted()); return details; } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/entities/listeners/RecordingListener.java b/src/main/java/uk/gov/hmcts/reform/preapi/entities/listeners/RecordingListener.java index 4488c99a49..4476520320 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/entities/listeners/RecordingListener.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/entities/listeners/RecordingListener.java @@ -51,7 +51,6 @@ public void onRecordingCreated(Recording recording) { } try { - List shares = recording.getCaptureSession().getBooking().getShares() .stream() .filter(s -> !s.isDeleted()) @@ -73,7 +72,7 @@ public void onRecordingCreated(Recording recording) { } ); } catch (Exception e) { - log.error("Failed to notify users of recording ready for recording: " + recording.getId()); + log.error("Failed to notify users of recording ready for recording: {}", recording.getId()); } } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/enums/EditRequestStatus.java b/src/main/java/uk/gov/hmcts/reform/preapi/enums/EditRequestStatus.java index 4ca64c82cf..d59bd35806 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/enums/EditRequestStatus.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/enums/EditRequestStatus.java @@ -8,5 +8,7 @@ public enum EditRequestStatus { PENDING, PROCESSING, COMPLETE, - ERROR + ERROR, + ORIGINAL, + OUTDATED } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructions.java b/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructions.java deleted file mode 100644 index 9b9231d4e9..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructions.java +++ /dev/null @@ -1,33 +0,0 @@ -package uk.gov.hmcts.reform.preapi.media.edit; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.AllArgsConstructor; -import lombok.Getter; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; -import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; - -import java.util.List; - -@Getter -@AllArgsConstructor -public class EditInstructions { - private final List requestedInstructions; - private final List ffmpegInstructions; - - public static EditInstructions fromJson(String editInstructions) { - try { - return new ObjectMapper().readValue(editInstructions, EditInstructions.class); - } catch (Exception e) { - throw new UnknownServerException("Unable to read edit instructions", e); - } - } - - public static EditInstructions tryFromJson(String editInstructions) { - try { - return new ObjectMapper().readValue(editInstructions, EditInstructions.class); - } catch (Exception e) { - return null; - } - } -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/IEditingService.java b/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/IEditingService.java deleted file mode 100644 index fcf830ac26..0000000000 --- a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/IEditingService.java +++ /dev/null @@ -1,10 +0,0 @@ -package uk.gov.hmcts.reform.preapi.media.edit; - -import uk.gov.hmcts.reform.preapi.entities.EditRequest; - -import java.util.UUID; - -@SuppressWarnings("PMD.ImplicitFunctionalInterface") -public interface IEditingService { - void performEdit(UUID newRecordingId, EditRequest request); -} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditCutInstructionsRepository.java b/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditCutInstructionsRepository.java new file mode 100644 index 0000000000..6b895e291d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditCutInstructionsRepository.java @@ -0,0 +1,16 @@ +package uk.gov.hmcts.reform.preapi.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface EditCutInstructionsRepository extends JpaRepository { + + // TODO: Find instructions for draft edit request for recording XX. Refresh. + void refreshInstructionsForDraftEditOnRecording(UUID sourceRecordingId, + List editCutInstructions); +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditRequestRepository.java b/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditRequestRepository.java index 741fcc7b27..5eaf124f40 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditRequestRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/repositories/EditRequestRepository.java @@ -15,6 +15,7 @@ import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -53,9 +54,13 @@ Page searchAllBy( Optional findByIdNotLocked(@NotNull UUID id); @Query(""" - SELECT (COUNT(e) > 0) from EditRequest e - WHERE e.sourceRecording.captureSession.booking.caseId.id = :caseId - AND (e.status != 'COMPLETE') - """) - boolean existsByCaseIdAndIsIncomplete(@Param("caseId") UUID caseId); + SELECT booking.caseId from Booking booking + WHERE booking.id in (select cs.booking from CaptureSession cs + where cs.id in (select r.captureSession from Recording r + where r.id in (select e.sourceRecordingId from EditRequest e + where e.status in ('DRAFT', 'SUBMITTED', 'APPROVED', 'PENDING', 'PROCESSING')))) + """) + List getCaseIdsWithIncompleteEdits(); + + Optional findFirstBySourceRecordingIdIs(UUID sourceRecordingId); } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/security/AuthorisationService.java b/src/main/java/uk/gov/hmcts/reform/preapi/security/AuthorisationService.java index e63e25ad39..cb4e136ec5 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/security/AuthorisationService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/security/AuthorisationService.java @@ -5,7 +5,7 @@ import uk.gov.hmcts.reform.preapi.dto.CreateBookingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCaptureSessionDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCaseDTO; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.CreateParticipantDTO; import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateShareBookingDTO; @@ -146,7 +146,7 @@ public boolean hasEditRequestAccess(UserAuthentication authentication, UUID id) } try { EditRequest request = editRequestRepository.findByIdNotLocked(id).orElse(null); - return request == null || hasRecordingAccess(authentication, request.getSourceRecording().getId()); + return request == null || hasRecordingAccess(authentication, request.getSourceRecordingId()); } catch (Exception e) { return false; } @@ -188,7 +188,7 @@ && hasUpsertAccess(authentication, dto.getParticipants()) && canUpdateCaseState(authentication, dto); } - public boolean hasUpsertAccess(UserAuthentication authentication, CreateEditRequestDTO dto) { + public boolean hasUpsertAccess(UserAuthentication authentication, EditRequestDTO dto) { return hasRecordingAccess(authentication, dto.getSourceRecordingId()); } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/CaseService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/CaseService.java index a4e52de018..25b36ebb1d 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/services/CaseService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/CaseService.java @@ -161,7 +161,8 @@ public UpsertResult upsert(CreateCaseDTO createCaseDTO) { + createCaseDTO.getState()); } - if (editRequestRepository.existsByCaseIdAndIsIncomplete(createCaseDTO.getId())) { + List casesWithIncompleteEdits = editRequestRepository.getCaseIdsWithIncompleteEdits(); + if (!casesWithIncompleteEdits.isEmpty() && casesWithIncompleteEdits.contains(createCaseDTO.getId())) { throw new ResourceInWrongStateException( "Resource Case(" + createCaseDTO.getId() 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..1e632d1425 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.beans.factory.annotation.Value; 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.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,81 @@ 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) { + 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() + )); } - String groupEmail = court.getGroupEmail(); + EditEmailParameters editEmailParameters = getEmailParameters(sourceRecording); try { - IEmailService enabledEmailService = emailServiceFactory.getEnabledEmailService(); - - if (Boolean.TRUE.equals(request.getJointlyAgreed())) { - enabledEmailService.editingJointlyAgreed(groupEmail, request); - } else { - enabledEmailService.editingNotJointlyAgreed(groupEmail, request); - } + 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/services/EditRequestService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/EditRequestService.java index 107793bf49..cf0464a8e7 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 @@ -1,10 +1,5 @@ package uk.gov.hmcts.reform.preapi.services; -import com.azure.resourcemanager.mediaservices.models.JobState; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.opencsv.bean.CsvToBeanBuilder; -import lombok.Cleanup; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; @@ -13,512 +8,87 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; -import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; -import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; -import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetDTO; -import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetResponseDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; 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.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; -import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; -import uk.gov.hmcts.reform.preapi.media.MediaServiceBroker; -import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; -import uk.gov.hmcts.reform.preapi.media.edit.FfmpegService; -import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; -import uk.gov.hmcts.reform.preapi.media.storage.AzureIngestStorageService; -import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; -import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; import uk.gov.hmcts.reform.preapi.security.authentication.UserAuthentication; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestCrudService; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestFromCsv; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestProcessingService; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; - -import static uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO.formatTime; -import static uk.gov.hmcts.reform.preapi.media.edit.EditInstructions.fromJson; @Slf4j @Service -@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.GodClass"}) public class EditRequestService { - private final EditRequestRepository editRequestRepository; - private final RecordingRepository recordingRepository; - private final FfmpegService ffmpegService; - private final RecordingService recordingService; - private final AzureIngestStorageService azureIngestStorageService; - private final AzureFinalStorageService azureFinalStorageService; - private final MediaServiceBroker mediaServiceBroker; - private final EditNotificationService editNotificationService; + + private final EditRequestCrudService editRequestCrudService; + private final EditRequestProcessingService editRequestProcessingService; + private final EditRequestFromCsv editRequestFromCsv; @Autowired - public EditRequestService(final EditRequestRepository editRequestRepository, - final RecordingRepository recordingRepository, - final FfmpegService ffmpegService, - final RecordingService recordingService, - final AzureIngestStorageService azureIngestStorageService, - final AzureFinalStorageService azureFinalStorageService, - final MediaServiceBroker mediaServiceBroker, - final EditNotificationService editNotificationService) { - this.editRequestRepository = editRequestRepository; - this.recordingRepository = recordingRepository; - this.ffmpegService = ffmpegService; - this.recordingService = recordingService; - this.azureIngestStorageService = azureIngestStorageService; - this.azureFinalStorageService = azureFinalStorageService; - this.mediaServiceBroker = mediaServiceBroker; - this.editNotificationService = editNotificationService; + public EditRequestService(final EditRequestCrudService editRequestCrudService, + final EditRequestProcessingService editRequestProcessingService, + final EditRequestFromCsv editRequestFromCsv) { + this.editRequestCrudService = editRequestCrudService; + this.editRequestProcessingService = editRequestProcessingService; + this.editRequestFromCsv = editRequestFromCsv; } - @Transactional @PreAuthorize("@authorisationService.hasEditRequestAccess(authentication, #id)") public EditRequestDTO findById(UUID id) { - return editRequestRepository - .findByIdNotLocked(id) - .map(EditRequestDTO::new) - .orElseThrow(() -> new NotFoundException("Edit Request: " + id)); + return editRequestCrudService.findById(id); } - @Transactional public Page findAll(@NotNull SearchEditRequests params, Pageable pageable) { UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); params.setAuthorisedBookings(auth.isAdmin() || auth.isAppUser() ? null : auth.getSharedBookings()); params.setAuthorisedCourt(auth.isPortalUser() || auth.isAdmin() ? null : auth.getCourtId()); - return editRequestRepository - .searchAllBy(params, pageable) - .map(EditRequestDTO::new); + return editRequestCrudService.findAll(params, pageable); } - @Transactional public Optional getNextPendingEditRequest() { - return editRequestRepository.findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING); - } - - @Transactional - public void updateEditRequestStatus(UUID id, EditRequestStatus status) { - EditRequest request = editRequestRepository.findById(id) - .orElseThrow(() -> new NotFoundException("Edit Request: " + id)); - - request.setStatus(status); - switch (status) { - case PROCESSING -> request.setStartedAt(Timestamp.from(Instant.now())); - case ERROR, COMPLETE -> request.setFinishedAt(Timestamp.from(Instant.now())); - default -> { - } - } - editRequestRepository.save(request); - } - - @Transactional(noRollbackFor = Exception.class) - public EditRequest markAsProcessing(UUID editId) throws InterruptedException { - log.info("Performing Edit Request: {}", editId); - // retrieves locked edit request - EditRequest request = editRequestRepository.findById(editId) - .orElseThrow(() -> new NotFoundException("Edit Request: " + editId)); - - if (request.getStatus() != EditRequestStatus.PENDING) { - throw new ResourceInWrongStateException( - EditRequest.class.getSimpleName(), - request.getId().toString(), - request.getStatus().toString(), - EditRequestStatus.PENDING.toString() - ); - } - updateEditRequestStatus(request.getId(), EditRequestStatus.PROCESSING); - return request; - } - - @Transactional(noRollbackFor = {Exception.class, RuntimeException.class}, propagation = Propagation.REQUIRES_NEW) - public RecordingDTO performEdit(EditRequest request) throws InterruptedException { - UUID newRecordingId = UUID.randomUUID(); - String filename; - try { - ffmpegService.performEdit(newRecordingId, request); - filename = generateAsset(newRecordingId, request); - } catch (Exception e) { - updateEditRequestStatus(request.getId(), EditRequestStatus.ERROR); - throw e; - } - - updateEditRequestStatus(request.getId(), EditRequestStatus.COMPLETE); - - CreateRecordingDTO createDto = createRecordingDto(newRecordingId, filename, request); - recordingService.upsert(createDto); - - return recordingService.findById(newRecordingId); + return editRequestCrudService.getNextPendingEditRequest(); } - @Transactional - public @NotNull CreateRecordingDTO createRecordingDto(UUID newRecordingId, String filename, EditRequest request) { - UUID parentId = request.getSourceRecording().getParentRecording() == null - ? request.getSourceRecording().getId() - : request.getSourceRecording().getParentRecording().getId(); - - CreateRecordingDTO createDto = new CreateRecordingDTO(); - createDto.setId(newRecordingId); - createDto.setParentRecordingId(parentId); - // if edit on edit without original edits saved (legacy edit), - // then these edits will not align with the original timeline - EditInstructionDump dump = new EditInstructionDump(request.getId(), fromJson(request.getEditInstruction())); - createDto.setEditInstructions(toJson(dump)); - createDto.setVersion(recordingService.getNextVersionNumber(parentId)); - createDto.setCaptureSessionId(request.getSourceRecording().getCaptureSession().getId()); - createDto.setFilename(filename); - // duration is auto-generated - return createDto; - } - - @Transactional - public String generateAsset(UUID newRecordingId, EditRequest request) throws InterruptedException { - String sourceContainer = newRecordingId + "-input"; - if (!azureIngestStorageService.doesContainerExist(sourceContainer)) { - throw new NotFoundException("Source Container (" + sourceContainer + ") does not exist"); - } - // throws 404 when doesn't exist - azureIngestStorageService.getMp4FileName(sourceContainer); - azureIngestStorageService.markContainerAsProcessing(sourceContainer); - String assetName = newRecordingId.toString().replace("-", ""); - - azureFinalStorageService.createContainerIfNotExists(newRecordingId.toString()); - - GenerateAssetDTO generateAssetDto = GenerateAssetDTO.builder() - .sourceContainer(sourceContainer) - .destinationContainer(newRecordingId) - .tempAsset(assetName) - .finalAsset(assetName + "_output") - .parentRecordingId(request.getSourceRecording().getId()) - .description("Edit of " + request.getSourceRecording().getId().toString().replace("-", "")) - .build(); - - GenerateAssetResponseDTO result = mediaServiceBroker.getEnabledMediaService() - .importAsset(generateAssetDto, false); - - if (!result.getJobStatus().equals(JobState.FINISHED.toString())) { - throw new UnknownServerException("Failed to generate asset for edit request: " - + request.getSourceRecording().getId() - + ", new recording: " - + newRecordingId); - } - azureIngestStorageService.markContainerAsSafeToDelete(sourceContainer); - return azureFinalStorageService.getMp4FileName(newRecordingId.toString()); - } - - @Transactional @PreAuthorize("@authorisationService.hasUpsertAccess(authentication, #dto)") - public void delete(CreateEditRequestDTO dto) { - Optional req = editRequestRepository.findById(dto.getId()); - - if (req.isEmpty()) { - log.info("Attempt to delete non-existing edit request with id {}", dto.getId()); - return; - } - - editRequestRepository.delete(req.get()); - } - - @Transactional - @PreAuthorize("@authorisationService.hasUpsertAccess(authentication, #dto)") - public UpsertResult upsert(CreateEditRequestDTO dto) { - recordingService.syncRecordingMetadataWithStorage(dto.getSourceRecordingId()); - - Recording sourceRecording = recordingRepository.findByIdAndDeletedAtIsNull(dto.getSourceRecordingId()) - .orElseThrow(() -> new NotFoundException("Source Recording: " + dto.getSourceRecordingId())); - - if (sourceRecording.getDuration() == null) { - throw new ResourceInWrongStateException("Source Recording (" - + dto.getSourceRecordingId() - + ") does not have a valid duration"); - } - - Optional existingEditRequest = editRequestRepository.findById(dto.getId()); - - boolean isUpdate = existingEditRequest.isPresent(); - if (dto.getEditInstructions() == null || dto.getEditInstructions().isEmpty()) { - if (isUpdate) { - log.info( - "Deleting edit request {} for source recording {} as edit instructions are empty", - existingEditRequest.get().getId(), dto.getSourceRecordingId() - ); - delete(dto); - return UpsertResult.UPDATED; - } else { - throw new BadRequestException("Invalid Instruction: Cannot create an edit request with empty" - + " instructions"); - } - } - - EditRequest request = getEditRequestToCreateOrUpdate(dto, sourceRecording, - existingEditRequest.orElse(new EditRequest())); - - if (!isUpdate) { - UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); - User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); - - request.setCreatedBy(user); - } - - if (isUpdate) { - if (dto.getStatus() == EditRequestStatus.SUBMITTED) { - editNotificationService.onEditRequestSubmitted(request); - } else { - editNotificationService.onEditRequestRejected(request); - } - } - - editRequestRepository.save(request); - return isUpdate ? UpsertResult.UPDATED : UpsertResult.CREATED; + public EditRequestDTO upsert(EditRequestDTO dto) { + UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); + return editRequestCrudService.createOrUpsertDraftEditRequestInstructions(dto, user); } - @Transactional + // temporary code for create edit request with csv endpoint + @Deprecated @PreAuthorize("@authorisationService.hasRecordingAccess(authentication, #sourceRecordingId)") public EditRequestDTO upsert(UUID sourceRecordingId, MultipartFile file) { - // temporary code for create edit request with csv endpoint - UUID id = UUID.randomUUID(); - CreateEditRequestDTO dto = new CreateEditRequestDTO(); - dto.setId(id); - dto.setSourceRecordingId(sourceRecordingId); - dto.setEditInstructions(parseCsv(file)); - dto.setStatus(EditRequestStatus.PENDING); - - upsert(dto); - - return editRequestRepository.findById(id) - .map(EditRequestDTO::new) - .orElseThrow(() -> new UnknownServerException("Edit Request failed to create")); - } - - private @NotNull EditRequest getEditRequestToCreateOrUpdate(CreateEditRequestDTO dto, Recording sourceRecording, - EditRequest request) { - boolean isOriginalRecordingEdit = sourceRecording.getParentRecording() == null; - - boolean sourceInstructionsAreNotEmpty = !isOriginalRecordingEdit - && sourceRecording.getEditInstruction() != null - && !sourceRecording.getEditInstruction().isEmpty(); - - EditInstructions prevInstructions = null; - if (sourceInstructionsAreNotEmpty) { - prevInstructions = EditInstructions.tryFromJson(sourceRecording.getEditInstruction()); - } - boolean prevInstructionsAreNotEmpty = prevInstructions != null - && prevInstructions.getFfmpegInstructions() != null - && !prevInstructions.getFfmpegInstructions().isEmpty() - && prevInstructions.getRequestedInstructions() != null - && !prevInstructions.getRequestedInstructions().isEmpty(); - - boolean isInstructionCombination = sourceInstructionsAreNotEmpty && prevInstructionsAreNotEmpty; - - request.setId(dto.getId()); - request.setSourceRecording(!isInstructionCombination - ? sourceRecording - : sourceRecording.getParentRecording()); - request.setStatus(dto.getStatus()); - request.setJointlyAgreed(dto.getJointlyAgreed()); - request.setApprovedAt(dto.getApprovedAt()); - request.setApprovedBy(dto.getApprovedBy()); - request.setRejectionReason(dto.getRejectionReason()); - - List requestedEdits = isInstructionCombination - ? combineCutsOnOriginalTimeline(prevInstructions, dto.getEditInstructions()) - : dto.getEditInstructions(); - - List editInstructions = invertInstructions( - requestedEdits, - isInstructionCombination ? request.getSourceRecording() : sourceRecording - ); - - request.setEditInstruction(toJson(new EditInstructions(requestedEdits, editInstructions))); - return request; - } + UserAuthentication auth = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + User user = auth.isAppUser() ? auth.getAppAccess().getUser() : auth.getPortalAccess().getUser(); + return editRequestFromCsv.upsert(sourceRecordingId, file, user); - private List parseCsv(MultipartFile file) { - try { - @Cleanup BufferedReader reader = new BufferedReader(new InputStreamReader( - file.getInputStream(), - StandardCharsets.UTF_8 - )); - return new CsvToBeanBuilder(reader) - .withType(EditCutInstructionDTO.class) - .build() - .parse(); - } catch (Exception e) { - log.error("Error when reading CSV file: {} ", e.getMessage()); - throw new UnknownServerException("Uploaded CSV file incorrectly formatted", e); - } } - @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.AvoidLiteralsInIfCondition"}) - public List invertInstructions(final List instructions, - final Recording recording) { - long recordingDuration = recording.getDuration().toSeconds(); - if (instructions.size() == 1) { - EditCutInstructionDTO firstInstruction = instructions.getFirst(); - if (firstInstruction.getStart() == 0 && firstInstruction.getEnd() == recordingDuration) { - throw new BadRequestException("Invalid Instruction: Cannot cut an entire recording: Start(" - + formatTime(firstInstruction.getStart()) - + "), End(" - + formatTime(firstInstruction.getEnd()) - + "), Recording Duration(" - + formatTime(recordingDuration) - + ")"); - } - } - - instructions.sort(Comparator.comparing(EditCutInstructionDTO::getStart) - .thenComparing(EditCutInstructionDTO::getEnd)); - - for (int i = 1; i < instructions.size(); i++) { - EditCutInstructionDTO prev = instructions.get(i - 1); - EditCutInstructionDTO curr = instructions.get(i); - if (curr.getStart() < prev.getEnd()) { - throw new BadRequestException("Overlapping instructions: Previous End(" - + formatTime(prev.getEnd()) - + "), Current Start(" - + formatTime(curr.getStart()) - + ")"); - } - } - - long currentTime = 0L; - List invertedInstructions = new ArrayList<>(); - - // invert - for (EditCutInstructionDTO instruction : instructions) { - if (instruction.getStart() == instruction.getEnd()) { - throw new BadRequestException( - "Invalid instruction: Instruction with 0 second duration invalid: Start(" - + formatTime(instruction.getStart()) - + "), End(" - + formatTime(instruction.getEnd()) - + ")"); - } - if (instruction.getEnd() < instruction.getStart()) { - throw new BadRequestException( - "Invalid instruction: Instruction with end time before start time: Start(" - + formatTime(instruction.getStart()) - + "), End(" - + formatTime(instruction.getEnd()) - + ")"); - } - if (instruction.getEnd() > recordingDuration) { - throw new BadRequestException("Invalid instruction: Instruction end time exceeding duration: Start(" - + formatTime(instruction.getStart()) - + "), End(" - + formatTime(instruction.getEnd()) - + "), Recording Duration(" - + formatTime(recordingDuration) - + ")"); - } - if (currentTime < instruction.getStart()) { - invertedInstructions.add(new FfmpegEditInstructionDTO(currentTime, instruction.getStart())); - } - currentTime = Math.max(currentTime, instruction.getEnd()); - } - if (currentTime != recordingDuration) { - invertedInstructions.add(new FfmpegEditInstructionDTO(currentTime, recordingDuration)); - } - - return invertedInstructions; + @PreAuthorize("@authorisationService.hasUpsertAccess(authentication, #dto)") + public void delete(EditRequestDTO dto) { + editRequestCrudService.delete(dto); } - protected List combineCutsOnOriginalTimeline( - final EditInstructions original, - final List newInstructions - ) { - final List keptSegments = original.getFfmpegInstructions(); - - final List editedTimelineMapping = new ArrayList<>(); - long cursor = 0; - for (FfmpegEditInstructionDTO segment : keptSegments) { - long duration = segment.getEnd() - segment.getStart(); - editedTimelineMapping.add(new FfmpegEditInstructionDTO(cursor, cursor + duration)); - cursor += duration; - } - - final List mappedCuts = new ArrayList<>(); - for (EditCutInstructionDTO newCut : newInstructions) { - for (int i = 0; i < keptSegments.size(); i++) { - final FfmpegEditInstructionDTO originalSegment = keptSegments.get(i); - final FfmpegEditInstructionDTO editedSegment = editedTimelineMapping.get(i); - - long start = editedSegment.getStart(); - long end = editedSegment.getEnd(); - - if (newCut.getEnd() <= start || newCut.getStart() >= end) { - continue; - } - - long overlapStart = Math.max(start, newCut.getStart()); - long overlapEnd = Math.min(end, newCut.getEnd()); - - long offsetInSegment = overlapStart - start; - long cutLength = overlapEnd - overlapStart; - - long originalMappedStart = originalSegment.getStart() + offsetInSegment; - long originalMappedEnd = originalMappedStart + cutLength; - - mappedCuts.add(new EditCutInstructionDTO( - originalMappedStart, - originalMappedEnd, - newCut.getReason() - )); - } - } - - mappedCuts.addAll(original.getRequestedInstructions()); - mappedCuts.sort(Comparator.comparing(EditCutInstructionDTO::getStart)); - - return mergeOverlappingCuts(mappedCuts) - .stream() - .map(cut -> new EditCutInstructionDTO(cut.getStart(), cut.getEnd(), cut.getReason())) - .collect(Collectors.toList()); + public EditRequest markAsProcessing(UUID editId) { + return editRequestProcessingService.markAsProcessing(editId); } - protected List mergeOverlappingCuts(final List cuts) { - final List merged = new ArrayList<>(); - EditCutInstructionDTO current = cuts.getFirst(); - - for (int i = 1; i < cuts.size(); i++) { - final EditCutInstructionDTO next = cuts.get(i); - if (next.getStart() <= current.getEnd()) { - current.setEnd(Math.max(current.getEnd(), next.getEnd())); - } else { - merged.add(current); - current = next; - } - } - merged.add(current); - return merged; + public RecordingDTO performEdit(EditRequest request) throws InterruptedException { + return editRequestProcessingService.prepareForAndPerformEdit(request); } - protected String toJson(E instructions) { - try { - return new ObjectMapper().writeValueAsString(instructions); - } catch (JsonProcessingException e) { - throw new UnknownServerException("Something went wrong: " + e.getMessage(), e); - } + public void updateEditRequestStatus(UUID id, EditRequestStatus updatedStatus) { + editRequestProcessingService.updateEditRequestStatus(id, updatedStatus); } - private record EditInstructionDump(UUID editRequestId, EditInstructions editInstructions) { - } } diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationService.java new file mode 100644 index 0000000000..581bebbe88 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationService.java @@ -0,0 +1,71 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import com.azure.resourcemanager.mediaservices.models.JobState; +import groovy.util.logging.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetDTO; +import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetResponseDTO; +import uk.gov.hmcts.reform.preapi.entities.EditRequest; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; +import uk.gov.hmcts.reform.preapi.media.MediaServiceBroker; +import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; +import uk.gov.hmcts.reform.preapi.media.storage.AzureIngestStorageService; + +import java.util.UUID; + +import static java.lang.String.format; + +@Service +@Slf4j +public class AssetGenerationService { + + private final AzureIngestStorageService azureIngestStorageService; + private final AzureFinalStorageService azureFinalStorageService; + private final MediaServiceBroker mediaServiceBroker; + + @Autowired + public AssetGenerationService(final AzureIngestStorageService azureIngestStorageService, + final AzureFinalStorageService azureFinalStorageService, + final MediaServiceBroker mediaServiceBroker) { + this.azureIngestStorageService = azureIngestStorageService; + this.azureFinalStorageService = azureFinalStorageService; + this.mediaServiceBroker = mediaServiceBroker; + } + + public String generateAsset(UUID newRecordingId, UUID sourceRecordingId) throws InterruptedException { + String sourceContainer = newRecordingId + "-input"; + if (!azureIngestStorageService.doesContainerExist(sourceContainer)) { + throw new NotFoundException("Source Container (" + sourceContainer + ") does not exist"); + } + // throws 404 when doesn't exist + azureIngestStorageService.getMp4FileName(sourceContainer); + azureIngestStorageService.markContainerAsProcessing(sourceContainer); + String assetName = newRecordingId.toString().replace("-", ""); + + azureFinalStorageService.createContainerIfNotExists(newRecordingId.toString()); + + GenerateAssetDTO generateAssetDto = GenerateAssetDTO.builder() + .sourceContainer(sourceContainer) + .destinationContainer(newRecordingId) + .tempAsset(assetName) + .finalAsset(assetName + "_output") + .parentRecordingId(sourceRecordingId) + .description("Edit of " + sourceRecordingId.toString().replace("-", "")) + .build(); + + GenerateAssetResponseDTO result = mediaServiceBroker.getEnabledMediaService() + .importAsset(generateAssetDto, false); + + if (!result.getJobStatus().equals(JobState.FINISHED.toString())) { + throw new UnknownServerException(format( + "Failed to generate asset for edit request: " + + "source recording: %s, new recording: %s", + sourceRecordingId, newRecordingId + )); + } + azureIngestStorageService.markContainerAsSafeToDelete(sourceContainer); + return azureFinalStorageService.getMp4FileName(newRecordingId.toString()); + } +} 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 new file mode 100644 index 0000000000..ff5bad6b0a --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudService.java @@ -0,0 +1,156 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +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.BadRequestException; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; +import uk.gov.hmcts.reform.preapi.repositories.EditCutInstructionsRepository; +import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; +import uk.gov.hmcts.reform.preapi.services.RecordingService; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static java.lang.String.format; +import static uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO.fromDTO; + +@Slf4j +@Service +public class EditRequestCrudService { + + private final EditRequestRepository editRequestRepository; + private final RecordingRepository recordingRepository; + private final RecordingService recordingService; + private final EditCutInstructionsRepository editCutInstructionsRepository; + + @Autowired + public EditRequestCrudService(final EditRequestRepository editRequestRepository, + final RecordingRepository recordingRepository, + final RecordingService recordingService, + final EditCutInstructionsRepository editCutInstructionsRepository) { + this.editRequestRepository = editRequestRepository; + this.recordingRepository = recordingRepository; + this.recordingService = recordingService; + this.editCutInstructionsRepository = editCutInstructionsRepository; + } + + @Transactional + public EditRequestDTO findById(UUID id) { + return editRequestRepository + .findByIdNotLocked(id) + .map(EditRequestDTO::new) + .orElseThrow(() -> new NotFoundException("Edit Request: " + id)); + } + + @Transactional + public Page findAll(@NotNull SearchEditRequests params, Pageable pageable) { + return editRequestRepository + .searchAllBy(params, pageable) + .map(EditRequestDTO::new); + } + + @Transactional + public Optional getNextPendingEditRequest() { + return editRequestRepository.findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING); + } + + @Transactional + public void delete(EditRequestDTO dto) { + Optional req = editRequestRepository.findById(dto.getId()); + + if (req.isEmpty()) { + log.info("Attempt to delete non-existing edit request with id {}", dto.getId()); + return; + } + + editRequestRepository.delete(req.get()); + } + + @Transactional + public EditRequestDTO createOrUpsertDraftEditRequestInstructions(EditRequestDTO dto, User user) { + Recording originalRecording = validateSourceRecording(dto.getSourceRecordingId()); + + Optional mostRecentEditRequest = editRequestRepository + .findFirstBySourceRecordingIdIs(originalRecording.getId()); + + if (mostRecentEditRequest.isEmpty()) { + // Deliberately allow new (draft) edit request with empty instructions + // However, edit requests cannot be *submitted* with empty instructions + return createEditRequest(user, originalRecording, EditRequestDTO.fromDTO(dto.getEditCutInstructions())); + } + + // A non-draft edit request exists; create a new one with previous instructions attached + // Non-draft edit requests are read-only and should not be altered + if (mostRecentEditRequest.get().getStatus() != EditRequestStatus.DRAFT) { + return createEditRequest(user, originalRecording, mostRecentEditRequest.get().getEditCutInstructions()); + } + + editCutInstructionsRepository.refreshInstructionsForDraftEditOnRecording( + originalRecording.getId(), + fromDTO(dto.getEditCutInstructions()) + ); + + // Draft edit request exists: delete all current instructions and replace with updated instructions + // In practice these might be identical + // We might prefer to do an actual upsert on these to preserve edit instruction creation time and createdBy info? + editCutInstructionsRepository.deleteAll(mostRecentEditRequest.get().getEditCutInstructions()); + editCutInstructionsRepository.saveAll(fromDTO(dto.getEditCutInstructions())); + + return dto; + } + + private EditRequestDTO createEditRequest(User user, + Recording originalRecording, + List cutInstructions) { + if (originalRecording.getVersion() != 1) { + throw new BadRequestException(format( + "Can only perform edits on original recording (Version 1). " + + "Recording %s is version %d. Perhaps you need parent recording %s?", + originalRecording.getId(), + originalRecording.getVersion(), + originalRecording.getParentRecording().getId() + )); + } + + EditRequest newEditRequest = new EditRequest(); + newEditRequest.setId(UUID.randomUUID()); + newEditRequest.setCreatedBy(user); + newEditRequest.setStatus(EditRequestStatus.DRAFT); + newEditRequest.setSourceRecordingId(originalRecording.getId()); + newEditRequest.setOutputRecordingId(null); // This will be set by the processing service + newEditRequest.setEditCutInstructions(cutInstructions); + + editRequestRepository.save(newEditRequest); + + return new EditRequestDTO(newEditRequest); + } + + private @NotNull Recording validateSourceRecording(UUID sourceRecordingId) { + recordingService.syncRecordingMetadataWithStorage(sourceRecordingId); + + Recording originalRecording = recordingRepository.findByIdAndDeletedAtIsNull(sourceRecordingId) + .orElseThrow(() -> new NotFoundException("Source Recording: " + sourceRecordingId)); + + if (originalRecording.getDuration() == null || originalRecording.getDuration().isZero()) { + throw new ResourceInWrongStateException("Source Recording (" + + sourceRecordingId + + ") does not have a valid duration"); + } + return originalRecording; + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsv.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsv.java new file mode 100644 index 0000000000..8a428903e2 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsv.java @@ -0,0 +1,74 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import com.opencsv.bean.CsvToBeanBuilder; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +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.UnknownServerException; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; + +@Deprecated +@Slf4j +@Service +public class EditRequestFromCsv { + + private final EditRequestCrudService editRequestCrudService; + + @Autowired + public EditRequestFromCsv(EditRequestCrudService editRequestCrudService) { + this.editRequestCrudService = editRequestCrudService; + } + + @Transactional + public EditRequestDTO upsert(UUID sourceRecordingId, MultipartFile file, User user) { + UUID id = UUID.randomUUID(); + EditRequestDTO dto = new EditRequestDTO(); + dto.setId(id); + dto.setSourceRecordingId(sourceRecordingId); + dto.setEditCutInstructions(parseCsv(file)); + dto.setStatus(EditRequestStatus.PENDING); + + if (dto.getEditCutInstructions() == null || dto.getEditCutInstructions().isEmpty()) { + throw new BadRequestException("No cut instructions found"); + } + + editRequestCrudService.createOrUpsertDraftEditRequestInstructions(dto, user); + + try { + return editRequestCrudService.findById(id); + } catch (Exception e) { + throw new UnknownServerException("Edit Request failed to create"); + } + } + + private List parseCsv(MultipartFile file) { + try { + @Cleanup BufferedReader reader = new BufferedReader(new InputStreamReader( + file.getInputStream(), + StandardCharsets.UTF_8 + )); + + return new CsvToBeanBuilder(reader) + .withType(EditCutInstructionsDTO.class) + .build() + .parse(); + } catch (Exception e) { + log.error("Error when reading CSV file: {} ", e.getMessage()); + throw new UnknownServerException("Uploaded CSV file incorrectly formatted", e); + } + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingService.java new file mode 100644 index 0000000000..d6c611c3b5 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingService.java @@ -0,0 +1,202 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +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.exception.ResourceInWrongStateException; +import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.services.EditNotificationService; +import uk.gov.hmcts.reform.preapi.services.RecordingService; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +import static java.lang.String.format; +import static uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO.formatTimeAsString; + +@Service +@Slf4j +public class EditRequestProcessingService { + + private final EditRequestRepository editRequestRepository; + private final IEditingService editingService; + private final RecordingService recordingService; + private final EditNotificationService editNotificationService; + private final AssetGenerationService assetGenerationService; + + @Autowired + public EditRequestProcessingService(final EditRequestRepository editRequestRepository, + final IEditingService editingService, + final RecordingService recordingService, + final EditNotificationService editNotificationService, + final AssetGenerationService assetGenerationService) { + this.editRequestRepository = editRequestRepository; + this.editingService = editingService; + this.recordingService = recordingService; + this.editNotificationService = editNotificationService; + this.assetGenerationService = assetGenerationService; + } + + @Transactional(noRollbackFor = Exception.class) + public EditRequest markAsProcessing(UUID editId) { + log.info("Performing Edit Request: {}", editId); + // retrieves locked edit request + EditRequest request = editRequestRepository.findById(editId) + .orElseThrow(() -> new NotFoundException("Edit Request: " + editId)); + + if (request.getStatus() != EditRequestStatus.PENDING) { + throw new ResourceInWrongStateException( + EditRequest.class.getSimpleName(), + request.getId().toString(), + request.getStatus().toString(), + EditRequestStatus.PENDING.toString() + ); + } + updateEditRequestStatus(request.getId(), EditRequestStatus.PROCESSING); + return request; + } + + @Transactional(noRollbackFor = {Exception.class, RuntimeException.class}, propagation = Propagation.REQUIRES_NEW) + public RecordingDTO prepareForAndPerformEdit(EditRequest request) throws InterruptedException { + RecordingDTO sourceRecording = recordingService.findById(request.getSourceRecordingId()); + + UUID newRecordingId = UUID.randomUUID(); + String filename; + try { + validateEditInstructions(sourceRecording); + editingService.performEdit(newRecordingId, sourceRecording); + filename = assetGenerationService.generateAsset(newRecordingId, request.getSourceRecordingId()); + Integer versionNumber = recordingService.getNextVersionNumber(sourceRecording.getParentRecordingId()); + CreateRecordingDTO createRecordingDTO = new CreateRecordingDTO( + newRecordingId, filename, versionNumber, sourceRecording); + request.setOutputRecordingId(createRecordingDTO.getId()); + editRequestRepository.save(request); + recordingService.upsert(createRecordingDTO); + } catch (Exception e) { + updateEditRequestStatus(request.getId(), EditRequestStatus.ERROR); + throw e; + } + + updateEditRequestStatus(request.getId(), EditRequestStatus.COMPLETE); + + return recordingService.findById(newRecordingId); + } + + @Transactional + public void updateEditRequestStatus(UUID editRequestId, EditRequestStatus updatedStatus) { + EditRequest editRequest = editRequestRepository.findById(editRequestId) + .orElseThrow(() -> new NotFoundException("Edit Request: " + editRequestId)); + + if (editRequest.getStatus() == EditRequestStatus.DRAFT && updatedStatus == EditRequestStatus.SUBMITTED) { + if (editRequest.getEditCutInstructions().isEmpty()) { + throw new BadRequestException(format( + "Cannot submit edit request %s: empty instructions", + editRequest.getId() + )); + } + } + editRequest.setStatus(updatedStatus); + + if (updatedStatus == EditRequestStatus.PROCESSING) { + editRequest.setStartedAt(Timestamp.from(Instant.now())); + } + + if (updatedStatus == EditRequestStatus.COMPLETE || updatedStatus == EditRequestStatus.ERROR) { + editRequest.setFinishedAt(Timestamp.from(Instant.now())); + } + editRequestRepository.save(editRequest); + + // For edited recordings, notification is sent by RecordingListener instead + if (updatedStatus != EditRequestStatus.DRAFT && updatedStatus != EditRequestStatus.COMPLETE) { + editNotificationService.editRequestStatusWasUpdated(editRequest); + } + } + + private void validateEditInstructions(final RecordingDTO recordingDTO) { + if (recordingDTO == null) { + throw new BadRequestException("Cannot perform edit request: recording was null"); + } + + List instructionsList = recordingDTO.getEditCutInstructionsLegacyProof(); + if (instructionsList.isEmpty()) { + throw new BadRequestException("Cannot perform edit request: no instructions were provided"); + } + + instructionsList.sort(Comparator.comparing(EditCutInstructionsDTO::getStart) + .thenComparing(EditCutInstructionsDTO::getEnd)); + + if (recordingDTO.getDuration() == null) { + throw new BadRequestException("Cannot perform edit request: duration was null"); + } + + long recordingDuration = recordingDTO.getDuration().toSeconds(); + + if (instructionsList.getFirst().getStart() == 0 && instructionsList.getFirst().getEnd() == recordingDuration) { + throw new BadRequestException("Invalid Instruction: Cannot cut an entire recording: Start(" + + formatTimeAsString(instructionsList.getFirst().getStart()) + + "), End(" + + formatTimeAsString(instructionsList.getFirst().getEnd()) + + "), Recording Duration(" + + recordingDuration + + " seconds)"); + } + + EditCutInstructionsDTO previous = null; + for (EditCutInstructionsDTO current : instructionsList) { + // To skip the first one + if (previous == null) { + previous = current; + continue; + } + previous = current; + + if (current.getStart() < previous.getEnd()) { + throw new BadRequestException(format( + "Overlapping instructions: Previous End(%s), Current Start(%s)", + formatTimeAsString(previous.getEnd()), + formatTimeAsString(previous.getStart()) + )); + } + + if (current.getStart().equals(current.getEnd())) { + throw new BadRequestException(format( + "Invalid instruction: Instruction with 0 second duration invalid: " + + "Start(%s), End(%s)", + formatTimeAsString(current.getStart()), + formatTimeAsString(current.getEnd()) + )); + } + if (current.getEnd() < current.getStart()) { + throw new BadRequestException(format( + "Invalid instruction: Instruction with end time before start time: " + + "Start(%s), End(%s)", + formatTimeAsString(current.getStart()), + formatTimeAsString(current.getEnd()) + )); + } + if (current.getEnd() > recordingDuration) { + throw new BadRequestException(format( + "Invalid instruction: Instruction end time exceeding duration: " + + "Start(%s), End(%s), Recording Duration(%s)", + formatTimeAsString(current.getStart()), + formatTimeAsString(current.getEnd()), + recordingDuration + )); + } + } + + } + +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegService.java b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java similarity index 62% rename from src/main/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegService.java rename to src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java index a2d98f8ca7..1a999ba2c2 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegService.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/FfmpegService.java @@ -1,12 +1,13 @@ -package uk.gov.hmcts.reform.preapi.media.edit; +package uk.gov.hmcts.reform.preapi.services.edit; import lombok.extern.slf4j.Slf4j; import org.apache.commons.exec.CommandLine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import uk.gov.hmcts.reform.preapi.component.CommandExecutor; -import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; -import uk.gov.hmcts.reform.preapi.entities.EditRequest; +import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; @@ -47,64 +48,68 @@ public FfmpegService(AzureIngestStorageService azureIngestStorageService, this.commandExecutor = commandExecutor; } + // Change this to accept a recording with embedded edit request @Override - public void performEdit(UUID newRecordingId, EditRequest request) { - String inputFileName = request.getSourceRecording().getFilename(); + public void performEdit(UUID newRecordingId, RecordingDTO recording) { + String inputFileName = recording.getFilename(); if (inputFileName == null) { throw new NotFoundException("No file name provided"); } String outputFileName = newRecordingId + ".mp4"; - performCommandsForEdit(request, newRecordingId, inputFileName, outputFileName); - } - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") - private void performCommandsForEdit(final EditRequest request, - final UUID newRecordingId, - final String inputFileName, - final String outputFileName) { final List filesToDelete = new ArrayList<>(); filesToDelete.add(inputFileName); filesToDelete.add(outputFileName); - final Map commands = generateMultiEditCommands(request, inputFileName, outputFileName); - downloadInputFile(request, inputFileName); + final Map commands = generateMultiEditCommands(newRecordingId, outputFileName, recording); + + downloadInputFile(recording.getEditRequest(), inputFileName); try { - // single command - if (commands.size() == 1) { - runFfmpegCommand( - request.getId(), - commands.get(outputFileName), - inputFileName, - outputFileName, - filesToDelete - ); - uploadOutputFile(newRecordingId, outputFileName, filesToDelete); - cleanup(filesToDelete); - return; - } + runFfmpegCommandsWithFilenames( + recording.getEditRequest().getId(), newRecordingId, inputFileName, outputFileName, + commands, filesToDelete + ); + } finally { + cleanup(filesToDelete); + } + } - final long ffmpegStart = System.currentTimeMillis(); + private void runFfmpegCommandsWithFilenames(UUID requestId, UUID newRecordingId, String inputFileName, + String outputFileName, Map commands, + List filesToDelete) { + // single command + if (commands.size() == 1) { + runFfmpegCommand( + requestId, + commands.get(outputFileName), + inputFileName, + outputFileName, + filesToDelete + ); + uploadOutputFile(newRecordingId, outputFileName, filesToDelete); + cleanup(filesToDelete); + return; + } - // multi-command - commands.forEach((segmentFileName, command) -> { - filesToDelete.add(segmentFileName); - runFfmpegCommand(request.getId(), command, inputFileName, segmentFileName, filesToDelete); - }); + final long ffmpegStart = System.currentTimeMillis(); - // concat segments - generateConcatListFile(commands.keySet(), CONCAT_FILENAME); - filesToDelete.add(CONCAT_FILENAME); - final CommandLine concatCommand = generateConcatCommand(CONCAT_FILENAME, outputFileName); - runFfmpegConcatCommand(request.getId(), concatCommand, outputFileName); + // multi-command + commands.forEach((segmentFileName, command) -> { + filesToDelete.add(segmentFileName); + runFfmpegCommand(requestId, command, inputFileName, segmentFileName, filesToDelete); + }); - final long ffmpegEnd = System.currentTimeMillis(); - log.info("All ffmpeg commands completed in {} ms", ffmpegEnd - ffmpegStart); + // concat segments + generateConcatListFile(commands.keySet()); + filesToDelete.add(CONCAT_FILENAME); + final CommandLine concatCommand = generateConcatCommand(CONCAT_FILENAME, outputFileName); + runFfmpegConcatCommand(requestId, concatCommand, outputFileName); - uploadOutputFile(newRecordingId, outputFileName, filesToDelete); - } finally { - cleanup(filesToDelete); - } + final long ffmpegEnd = System.currentTimeMillis(); + log.info("All ffmpeg commands completed in {} ms", ffmpegEnd - ffmpegStart); + + uploadOutputFile(newRecordingId, outputFileName, filesToDelete); } private void runFfmpegCommand(final UUID requestId, @@ -181,13 +186,35 @@ private void deleteFile(String fileName) { } } - protected CommandLine generateSingleEditCommand(final FfmpegEditInstructionDTO instruction, - final String inputFileName, - final String outputFileName) { + private record FfmpegEditInstruction(long start, long end) { + } + + private List invertInstructions(final List instructions, + final long recordingDuration) { + long currentTime = 0L; + List invertedInstructions = new ArrayList<>(); + + for (EditCutInstructionsDTO instruction : instructions) { + if (currentTime < instruction.getStart()) { + invertedInstructions.add(new FfmpegEditInstruction(currentTime, instruction.getStart())); + } + currentTime = Math.max(currentTime, instruction.getEnd()); + } + + if (currentTime != recordingDuration) { + invertedInstructions.add(new FfmpegEditInstruction(currentTime, recordingDuration)); + } + + return invertedInstructions; + } + + private CommandLine generateSingleEditCommand(final FfmpegEditInstruction instruction, + final String inputFileName, + final String outputFileName) { return new CommandLine("ffmpeg") .addArgument("-y") - .addArgument("-ss").addArgument(String.valueOf(instruction.getStart())) - .addArgument("-to").addArgument(String.valueOf(instruction.getEnd())) + .addArgument("-ss").addArgument(String.valueOf(instruction.start())) + .addArgument("-to").addArgument(String.valueOf(instruction.end())) .addArgument("-i").addArgument(inputFileName) .addArgument("-c").addArgument("copy") .addArgument("-avoid_negative_ts").addArgument("1") @@ -207,42 +234,45 @@ protected CommandLine generateConcatCommand(final String concatListFileName, fin .addArgument(outputFileName); } - @SuppressWarnings({"PMD.AvoidLiteralsInIfCondition", "PMD.LooseCoupling"}) - protected LinkedHashMap generateMultiEditCommands(final EditRequest editRequest, - final String inputFileName, - final String outputFileName) { - EditInstructions instructions = EditInstructions.fromJson(editRequest.getEditInstruction()); + private LinkedHashMap generateMultiEditCommands(final UUID newRecordingId, + final String outputFileName, + final RecordingDTO recording) { + List rawEditInstructions = recording.getEditRequest().getEditCutInstructions(); - if (instructions.getFfmpegInstructions() == null - || instructions.getFfmpegInstructions().isEmpty()) { - throw new UnknownServerException("Malformed edit instructions"); - } + long recordingDuration = recording.getDuration().toSeconds(); + List ffmpegEditInstructions = invertInstructions(rawEditInstructions, recordingDuration); - if (instructions.getFfmpegInstructions().size() == 1) { - FfmpegEditInstructionDTO instruction = instructions.getFfmpegInstructions().getFirst(); + if (ffmpegEditInstructions.size() == 1) { + FfmpegEditInstruction instruction = ffmpegEditInstructions.getFirst(); return new LinkedHashMap<>(Map.of( outputFileName, - generateSingleEditCommand(instruction, inputFileName, outputFileName))); + generateSingleEditCommand(instruction, recording.getFilename(), outputFileName) + )); } - return IntStream.range(0, instructions.getFfmpegInstructions().size()) + return IntStream.range(0, ffmpegEditInstructions.size()) .mapToObj(i -> { - FfmpegEditInstructionDTO instruction = instructions.getFfmpegInstructions().get(i); String segmentFileName = String.format("%d_segment.mp4", i); - return Map.entry(segmentFileName, - generateSingleEditCommand(instruction, inputFileName, segmentFileName)); + return Map.entry( + segmentFileName, + generateSingleEditCommand(ffmpegEditInstructions.get(i), recording.getFilename(), segmentFileName) + ); }) - .collect(Collectors.toMap(Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, - LinkedHashMap::new)); + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); } - protected void generateConcatListFile(final Set segmentFiles, final String outputPath) { - try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputPath), - StandardCharsets.UTF_8, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)) { + protected void generateConcatListFile(final Set segmentFiles) { + try (BufferedWriter writer = Files.newBufferedWriter( + Paths.get(FfmpegService.CONCAT_FILENAME), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + )) { for (String file : segmentFiles) { writer.write("file '" + file + "'"); writer.newLine(); @@ -252,11 +282,11 @@ protected void generateConcatListFile(final Set segmentFiles, final Stri } } - private void downloadInputFile(final EditRequest request, + private void downloadInputFile(final EditRequestDTO request, final String inputFileName) { final long downloadStart = System.currentTimeMillis(); if (!azureFinalStorageService - .downloadBlob(request.getSourceRecording().getId().toString(), inputFileName, inputFileName)) { + .downloadBlob(request.getSourceRecordingId().toString(), inputFileName, inputFileName)) { throw new UnknownServerException("Error occurred when attempting to download file: " + inputFileName); } final long downloadEnd = System.currentTimeMillis(); 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 new file mode 100644 index 0000000000..443f9534c5 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/services/edit/IEditingService.java @@ -0,0 +1,9 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; + +import java.util.UUID; + +public interface IEditingService { + void performEdit(UUID newRecordingId, RecordingDTO recording); +} diff --git a/src/main/java/uk/gov/hmcts/reform/preapi/tasks/CleanupNullRecordingDuration.java b/src/main/java/uk/gov/hmcts/reform/preapi/tasks/CleanupNullRecordingDuration.java index f182167bcc..ae997a5fd5 100644 --- a/src/main/java/uk/gov/hmcts/reform/preapi/tasks/CleanupNullRecordingDuration.java +++ b/src/main/java/uk/gov/hmcts/reform/preapi/tasks/CleanupNullRecordingDuration.java @@ -7,7 +7,7 @@ import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; import uk.gov.hmcts.reform.preapi.exception.NotFoundException; -import uk.gov.hmcts.reform.preapi.media.edit.FfmpegService; +import uk.gov.hmcts.reform.preapi.services.edit.FfmpegService; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; import uk.gov.hmcts.reform.preapi.security.service.UserAuthenticationService; import uk.gov.hmcts.reform.preapi.services.RecordingService; 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..9e142d84f0 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/preapi/utils/StringTools.java @@ -0,0 +1,7 @@ +package uk.gov.hmcts.reform.preapi.utils; + +public class StringTools { + public static boolean isBlankString(String string) { + return string == null || string.trim().isEmpty(); + } +} diff --git a/src/main/resources/db/migration/V030__EditRequests.sql b/src/main/resources/db/migration/V030__EditRequests.sql index a58c2992ba..3e02bc6bca 100644 --- a/src/main/resources/db/migration/V030__EditRequests.sql +++ b/src/main/resources/db/migration/V030__EditRequests.sql @@ -8,7 +8,7 @@ CREATE TYPE public.EDIT_REQUEST_STATUS AS ENUM ( CREATE TABLE edit_requests ( id UUID PRIMARY KEY, source_recording_id UUID REFERENCES recordings(id) NOT NULL, - edit_instruction JSON, + edit_instruction JSON, -- deprecated, use edit_cut_instructions status public.EDIT_REQUEST_STATUS NOT NULL, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, diff --git a/src/main/resources/db/migration/V053_EditCutInstructions.sql b/src/main/resources/db/migration/V053_EditCutInstructions.sql new file mode 100644 index 0000000000..4fba91186c --- /dev/null +++ b/src/main/resources/db/migration/V053_EditCutInstructions.sql @@ -0,0 +1,10 @@ +CREATE TABLE edit_cut_instructions +( + id UUID PRIMARY KEY, + edit_request_id UUID references edit_requests(id) NOT NULL, + start_edit_seconds INTEGER not null, + end_edit_seconds INTEGER NOT NULL, + reason VARCHAR(600) NOT NULL +); + +ALTER TABLE public.edit_requests ADD COLUMN output_recording_id uuid references recordings(id); 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 8c41d438fc..f776ab01d1 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 @@ -18,7 +18,7 @@ import org.testcontainers.shaded.com.fasterxml.jackson.databind.PropertyNamingStrategy; import uk.gov.hmcts.reform.preapi.controllers.EditController; import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; @@ -253,7 +253,7 @@ void upsertEditRequestCreated() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -278,7 +278,7 @@ void upsertEditRequestDeleted() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -302,7 +302,7 @@ void deleteEditRequestPathPayloadMismatch() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -326,7 +326,7 @@ void upsertEditRequestUpdated() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -351,7 +351,7 @@ void upsertEditRequestPathPayloadMismatch() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -403,7 +403,7 @@ void validateStartOfCutIsNull() throws Exception { void validateSourceRecordingIdIsNull() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -424,7 +424,7 @@ void validateStatusIsNull() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -444,7 +444,7 @@ void validateRejectedStatusWithoutRejectionReason() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -465,7 +465,7 @@ void validateSubmittedStatusWithoutJointlyAgreed() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -486,7 +486,7 @@ void validateApprovedStatusWithoutApprovedAt() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); @@ -508,7 +508,7 @@ void validateApprovedStatusWithoutApprovedBy() throws Exception { var dto = new CreateEditRequestDTO(); dto.setId(UUID.randomUUID()); dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() + dto.setEditInstructions(List.of(EditCutInstructionsDTO.builder() .startOfCut("00:00:00") .endOfCut("00:00:01") .build())); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTOTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTOTest.java index 152ec98dfb..012b5463bd 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTOTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/dto/CreateRecordingDTOTest.java @@ -1,82 +1,115 @@ package uk.gov.hmcts.reform.preapi.dto; - -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.preapi.entities.CaptureSession; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import java.sql.Timestamp; import java.time.Duration; import java.time.Instant; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; -@SuppressWarnings("PMD.LawOfDemeter") -class CreateRecordingDTOTest { - private static uk.gov.hmcts.reform.preapi.entities.Recording recordingEntity; - - @BeforeAll - static void setUp() { - recordingEntity = new uk.gov.hmcts.reform.preapi.entities.Recording(); - recordingEntity.setId(UUID.randomUUID()); - var captureSession = new CaptureSession(); - captureSession.setId(UUID.randomUUID()); - recordingEntity.setCaptureSession(captureSession); - recordingEntity.setVersion(1); - recordingEntity.setFilename("example-filename.txt"); - recordingEntity.setCreatedAt(Timestamp.from(Instant.now())); - } +@SpringBootTest(classes = CreateRecordingDTO.class) +public class CreateRecordingDTOTest { - @DisplayName("Should create a recording from entity") - @Test - void createCaseFromEntity() { - var parentRecording = new uk.gov.hmcts.reform.preapi.entities.Recording(); - parentRecording.setId(UUID.randomUUID()); - recordingEntity.setParentRecording(parentRecording); - var model = new CreateRecordingDTO(recordingEntity); - - assertThat(model.getId()).isEqualTo(recordingEntity.getId()); - assertThat(model.getCaptureSessionId()).isEqualTo(recordingEntity.getCaptureSession().getId()); - assertThat(model.getParentRecordingId()).isEqualTo(recordingEntity.getParentRecording().getId()); + @Mock + private RecordingDTO recordingDTO; + + @MockitoBean + private CaptureSessionDTO captureSessionDTO; + + private static final UUID recordingId = UUID.randomUUID(); + private static final UUID captureSessionId = UUID.randomUUID(); + + @BeforeEach + public void setUp() { + Timestamp setUpTimestamp = Timestamp.from(Instant.now()); + when(captureSessionDTO.getId()).thenReturn(captureSessionId); + + when(recordingDTO.getId()).thenReturn(recordingId); + when(recordingDTO.getCaptureSession()).thenReturn(captureSessionDTO); + when(recordingDTO.getVersion()).thenReturn(1); + when(recordingDTO.getFilename()).thenReturn("original filename"); + when(recordingDTO.getCreatedAt()).thenReturn(setUpTimestamp); + when(recordingDTO.getDuration()).thenReturn(Duration.ofMinutes(3)); + when(recordingDTO.getEditStatus()).thenReturn(EditRequestStatus.PENDING); + + UUID parentId = UUID.randomUUID(); + when(recordingDTO.getParentRecordingId()).thenReturn(parentId); + + EditRequestDTO editRequest = new EditRequestDTO(); + editRequest.setId(UUID.randomUUID()); + editRequest.setStatus(EditRequestStatus.REJECTED); + editRequest.setRejectionReason("I didn't like it"); + EditCutInstructions instructions = new EditCutInstructions(UUID.randomUUID(), 10, 20, "reason"); + editRequest.setEditCutInstructions(List.of(new EditCutInstructionsDTO(instructions))); + + when(recordingDTO.getEditRequest()).thenReturn(editRequest); } @DisplayName("Should create a recording from entity when parent recording is null") @Test void createCaseFromEntityParentRecordingNull() { - recordingEntity.setParentRecording(null); - var model = new CreateRecordingDTO(recordingEntity); + when(recordingDTO.getParentRecordingId()).thenReturn(null); + var model = new CreateRecordingDTO(recordingDTO); - assertThat(model.getId()).isEqualTo(recordingEntity.getId()); - assertThat(model.getCaptureSessionId()).isEqualTo(recordingEntity.getCaptureSession().getId()); + assertThat(model.getId()).isEqualTo(recordingDTO.getId()); + assertThat(model.getCaptureSessionId()).isEqualTo(recordingDTO.getCaptureSession().getId()); assertThat(model.getParentRecordingId()).isEqualTo(null); } @Test @DisplayName("Should create dto from RecordingDTO") void createDtoFromRecordingDto() { - RecordingDTO recording = new RecordingDTO(); - CaptureSessionDTO captureSession = new CaptureSessionDTO(); - captureSession.setId(UUID.randomUUID()); - - recording.setId(UUID.randomUUID()); - recording.setCaptureSession(captureSession); - recording.setParentRecordingId(UUID.randomUUID()); - recording.setVersion(1); - recording.setFilename("example-filename.txt"); - recording.setCreatedAt(Timestamp.from(Instant.now())); - recording.setDuration(Duration.ofMinutes(3)); - recording.setEditInstructions("{}"); - - CreateRecordingDTO createRecordingDTO = new CreateRecordingDTO(recording); - - assertThat(createRecordingDTO.getId()).isEqualTo(recording.getId()); - assertThat(createRecordingDTO.getCaptureSessionId()).isEqualTo(recording.getCaptureSession().getId()); - assertThat(createRecordingDTO.getParentRecordingId()).isEqualTo(recording.getParentRecordingId()); - assertThat(createRecordingDTO.getVersion()).isEqualTo(recording.getVersion()); - assertThat(createRecordingDTO.getFilename()).isEqualTo(recording.getFilename()); - assertThat(createRecordingDTO.getDuration()).isEqualTo(recording.getDuration()); - assertThat(createRecordingDTO.getEditInstructions()).isEqualTo(recording.getEditInstructions()); + CreateRecordingDTO createRecordingDTO = new CreateRecordingDTO(recordingDTO); + + assertThat(createRecordingDTO.getId()).isEqualTo(recordingId); + assertThat(createRecordingDTO.getCaptureSessionId()).isEqualTo(recordingDTO.getCaptureSession().getId()); + assertThat(createRecordingDTO.getParentRecordingId()).isEqualTo(recordingDTO.getParentRecordingId()); + assertThat(createRecordingDTO.getVersion()).isEqualTo(recordingDTO.getVersion()); + assertThat(createRecordingDTO.getFilename()).isEqualTo(recordingDTO.getFilename()); + assertThat(createRecordingDTO.getDuration()).isEqualTo(recordingDTO.getDuration()); + assertThat(createRecordingDTO.getEditInstructions()).isEqualTo(recordingDTO.getEditInstructions()); + } + + @Test + @DisplayName("Should create dto with edit request details from RecordingDTO") + void createDtoWithEditDetailsFromRecordingDto() { + when(recordingDTO.getEditInstructions()).thenReturn("edit instructions"); + CreateRecordingDTO createRecordingDTO = new CreateRecordingDTO(recordingDTO); + assertThat(createRecordingDTO.getId()).isEqualTo(recordingDTO.getId()); + assertThat(createRecordingDTO.getEditInstructions()).isEqualTo(recordingDTO.getEditInstructions()); + assertThat(createRecordingDTO.getEditRequest()).isEqualTo(recordingDTO.getEditRequest()); + assertThat(createRecordingDTO.getEditStatus()).isEqualTo(recordingDTO.getEditStatus()); + } + + @Test + @DisplayName("Should return new create recording dto with overridden values") + void createRecordingSuccess() { + UUID parentRecordingId = recordingDTO.getParentRecordingId(); + when(recordingDTO.getParentRecordingId()).thenReturn(parentRecordingId); + + UUID newRecordingId = UUID.randomUUID(); + Integer newVersionNumber = 6; + String newFileName = "new_filename.mp4"; + CreateRecordingDTO dto = new CreateRecordingDTO(newRecordingId, newFileName, newVersionNumber, recordingDTO); + + assertThat(dto).isNotNull(); + assertThat(dto.getId()).isEqualTo(newRecordingId); + assertThat(dto.getParentRecordingId()).isEqualTo(parentRecordingId); + assertThat(dto.getVersion()).isEqualTo(newVersionNumber); + assertThat(dto.getFilename()).isEqualTo(newFileName); } + } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTOTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTOTest.java deleted file mode 100644 index 2263c18e4b..0000000000 --- a/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionDTOTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package uk.gov.hmcts.reform.preapi.dto; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.preapi.exception.BadRequestException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThrows; - -public class EditCutInstructionDTOTest { - @Test - @DisplayName("Should successfully parse start and end times from HH:MM:SS input format") - void parseTimeSuccess() { - String startStr = "01:01:01"; // 3661 seconds - String endStr = "10:10:10"; // 36610 seconds - - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut(startStr) - .endOfCut(endStr) - .build(); - - assertThat(dto.getStart()).isEqualTo(3661); - assertThat(dto.getEnd()).isEqualTo(36610); - } - - @Test - @DisplayName("Should throw error when attempting to parse start/end time that is not parsable") - void parseTimeNumberFormatException() { - String startStr = "H1:01:01"; - String endStr = "10:10:1S"; - - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut(startStr) - .endOfCut(endStr) - .build(); - - String message1 = assertThrows(BadRequestException.class, dto::getStart).getMessage(); - assertThat(message1).isEqualTo("Invalid time format: " + startStr + ". Must be in the form HH:MM:SS"); - - String message2 = assertThrows(BadRequestException.class, dto::getEnd).getMessage(); - assertThat(message2).isEqualTo("Invalid time format: " + endStr + ". Must be in the form HH:MM:SS"); - } - - @Test - @DisplayName("Should throw error when attempting to parse start/end time that is not parsable") - void parseTimeIndexOutOfBoundsException() { - String startStr = "0101:01"; - String endStr = "101010"; - - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut(startStr) - .endOfCut(endStr) - .build(); - - String message1 = assertThrows(BadRequestException.class, dto::getStart).getMessage(); - assertThat(message1).isEqualTo("Invalid time format: " + startStr + ". Must be in the form HH:MM:SS"); - - String message2 = assertThrows(BadRequestException.class, dto::getEnd).getMessage(); - assertThat(message2).isEqualTo("Invalid time format: " + endStr + ". Must be in the form HH:MM:SS"); - } - - @Test - @DisplayName("Should return cached start value without parsing again") - void getStartReturnsCachedValue() { - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .start(3600L) - .build(); - - assertThat(dto.getStart()).isEqualTo(3600L); - } - - @Test - @DisplayName("Should correctly parse and calculate start value when accessed for the first time") - void getStartParsesStartValue() { - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut("01:30:00") // 5400 seconds - .build(); - - assertThat(dto.getStart()).isEqualTo(5400L); - } - - @Test - @DisplayName("Should throw BadRequestException for invalid empty start format") - void getStartThrowsForEmptyString() { - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut("") - .build(); - - String message = assertThrows(BadRequestException.class, dto::getStart).getMessage(); - - assertThat(message).isEqualTo("Invalid time format: . Must be in the form HH:MM:SS"); - } - - @Test - @DisplayName("Should throw BadRequestException for null start value") - void getStartThrowsForNullValue() { - EditCutInstructionDTO dto = EditCutInstructionDTO.builder() - .startOfCut(null) - .build(); - - String message = assertThrows(BadRequestException.class, dto::getStart).getMessage(); - - assertThat(message).isEqualTo("Invalid time format: null. Must be in the form HH:MM:SS"); - } -} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionsDTOTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionsDTOTest.java new file mode 100644 index 0000000000..ea7a4d76ba --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditCutInstructionsDTOTest.java @@ -0,0 +1,114 @@ +package uk.gov.hmcts.reform.preapi.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +import uk.gov.hmcts.reform.preapi.exception.BadRequestException; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EditCutInstructionsDTOTest { + + @Test + @DisplayName("Should set edit request ID from split parameter constructor") + void setEditRequestIdFromSplitParameterConstructor() { + UUID editRequestId = UUID.randomUUID(); + String startStr = "01:01:01"; // 3661 seconds + String endStr = "10:10:10"; // 36610 seconds + + EditCutInstructionsDTO dto = new EditCutInstructionsDTO(editRequestId, startStr, endStr, "reason"); + + assertThat(dto.getEditRequestId()).isEqualTo(editRequestId); + } + + @Test + @DisplayName("Should set all fields request ID from entity constructor") + void setEditRequestIdFromEntityConstructor() { + UUID editRequestId = UUID.randomUUID(); + + EditCutInstructions entity = new EditCutInstructions(editRequestId, 10, 20, "reason"); + EditCutInstructionsDTO dto = new EditCutInstructionsDTO(entity); + assertThat(dto.getEditRequestId()).isEqualTo(editRequestId); + assertThat(dto.getStart()).isEqualTo(10); + assertThat(dto.getEnd()).isEqualTo(20); + assertThat(dto.getReason()).isEqualTo("reason"); + assertThat(dto.getStartOfCut()).isEqualTo("00:00:10"); + assertThat(dto.getEndOfCut()).isEqualTo("00:00:20"); + } + + + @Test + @DisplayName("Should successfully parse start and end times from HH:MM:SS input string format") + void parseTimeSuccess() { + UUID editRequestId = UUID.randomUUID(); + String startStr = "01:01:01"; // 3661 seconds + String endStr = "10:10:10"; // 36610 seconds + + EditCutInstructionsDTO dto = new EditCutInstructionsDTO(editRequestId, startStr, endStr, "reason"); + + assertThat(dto.getStart()).isEqualTo(3661); + assertThat(dto.getEnd()).isEqualTo(36610); + } + + @Test + @DisplayName("Should throw error when attempting to parse start/end time that is not parsable") + void parseTimeNumberFormatException() { + try { + new EditCutInstructionsDTO(UUID.randomUUID(), "H1:01:01", "10:10:1S", "reason"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo("Invalid time format: H1:01:01" + + ". Must be in the form HH:MM:SS"); + } + } + + @Test + @DisplayName("Should throw error when attempting to parse start time that is not parsable") + void parseTimeIndexOutOfBoundsException() { + try { + new EditCutInstructionsDTO(UUID.randomUUID(), "0101:01", "00:22:00", "reason"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo("Invalid time format: 0101:01" + + ". Must be in the form HH:MM:SS"); + } + } + + + @Test + @DisplayName("Should correctly parse and calculate start value when accessed for the first time") + void getStartParsesStartValue() { + EditCutInstructionsDTO dto = new EditCutInstructionsDTO( + UUID.randomUUID(), + "01:30:00", "01:30:00", + "reason" + ); + + assertThat(dto.getStart()).isEqualTo(5400); + assertThat(dto.getEnd()).isEqualTo(5400); + assertThat(dto.getStartOfCut()).isEqualTo("01:30:00"); + assertThat(dto.getEndOfCut()).isEqualTo("01:30:00"); + } + + @Test + @DisplayName("Should throw BadRequestException for invalid empty start format") + void getStartThrowsForEmptyString() { + try { + new EditCutInstructionsDTO(UUID.randomUUID(), "", "", "reason"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo("Invalid time format: . Must be in the form HH:MM:SS"); + } + } + + @Test + @DisplayName("Should throw BadRequestException for null start value") + void getStartThrowsForNullValue() { + try { + new EditCutInstructionsDTO(UUID.randomUUID(), null, "00:22:00", "reason"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo("Invalid time format: null" + + ". Must be in the form HH:MM:SS"); + } + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTOTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTOTest.java index df04fb18d3..e23407c878 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTOTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/dto/EditRequestDTOTest.java @@ -3,15 +3,16 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.User; -import uk.gov.hmcts.reform.preapi.enums.CourtType; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; -import uk.gov.hmcts.reform.preapi.enums.RecordingOrigin; -import uk.gov.hmcts.reform.preapi.util.HelperFactory; import java.sql.Timestamp; import java.time.Instant; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -23,7 +24,7 @@ public class EditRequestDTOTest { static void setUp() { editRequest = new EditRequest(); editRequest.setId(UUID.randomUUID()); - editRequest.setEditInstruction("{\"requestedInstructions\": [], \"ffmpegInstructions\": []}"); + editRequest.setEditCutInstructions(List.of()); editRequest.setStatus(EditRequestStatus.COMPLETE); editRequest.setStartedAt(Timestamp.from(Instant.now())); editRequest.setFinishedAt(Timestamp.from(Instant.now())); @@ -33,78 +34,90 @@ static void setUp() { var user = new User(); user.setId(UUID.randomUUID()); editRequest.setCreatedBy(user); - - var court = HelperFactory.createCourt(CourtType.CROWN, "Test Court", "123"); - var aCase = HelperFactory.createCase(court, "reference", false, null); - var booking = HelperFactory.createBooking(aCase, Timestamp.from(Instant.now()), null); - var captureSession = HelperFactory.createCaptureSession( - booking, - RecordingOrigin.PRE, - null, - null, - null, - null, - null, - null, - null, - null - ); - var recording = HelperFactory.createRecording(captureSession, null, 1, "index.mp4", null); - editRequest.setSourceRecording(recording); + editRequest.setSourceRecordingId(UUID.randomUUID()); } @Test @DisplayName("Should create an edit request dto from edit request entity") - void testConstructor() { - var dto = new EditRequestDTO(editRequest); + void testConstructorFromEntity() { + EditCutInstructions firstEditInstructions = new EditCutInstructions(editRequest.getId(), 300, 500, "first edit"); + EditCutInstructions secondEditInstructions = new EditCutInstructions(editRequest.getId(), 600, 750, "second edit"); + editRequest.setEditCutInstructions(List.of(firstEditInstructions, secondEditInstructions)); + var dto = new EditRequestDTO(editRequest); assertThat(dto.getId()).isEqualTo(editRequest.getId()); - assertThat(dto.getEditInstruction().getRequestedInstructions()).isEmpty(); - assertThat(dto.getEditInstruction().getFfmpegInstructions()).isEmpty(); + assertThat(dto.getEditCutInstructions().size()).isEqualTo(2); + + EditCutInstructionsDTO first = dto.getEditCutInstructions().getFirst(); + assertThat(first.getEditRequestId()).isEqualTo(editRequest.getId()); + assertThat(first.getReason()).isEqualTo(firstEditInstructions.getReason()); + assertThat(first.getStart()).isEqualTo(firstEditInstructions.getStart()); + assertThat(first.getEnd()).isEqualTo(firstEditInstructions.getEnd()); + + EditCutInstructionsDTO second = dto.getEditCutInstructions().getLast(); + assertThat(second.getEditRequestId()).isEqualTo(editRequest.getId()); + assertThat(second.getReason()).isEqualTo(secondEditInstructions.getReason()); + assertThat(second.getStart()).isEqualTo(secondEditInstructions.getStart()); + assertThat(second.getEnd()).isEqualTo(secondEditInstructions.getEnd()); + + assertThat(dto.getCreatedAt()).isEqualTo(editRequest.getCreatedAt()); assertThat(dto.getStatus()).isEqualTo(editRequest.getStatus()); assertThat(dto.getStartedAt()).isEqualTo(editRequest.getStartedAt()); assertThat(dto.getFinishedAt()).isEqualTo(editRequest.getFinishedAt()); assertThat(dto.getCreatedAt()).isEqualTo(editRequest.getCreatedAt()); assertThat(dto.getModifiedAt()).isEqualTo(editRequest.getModifiedAt()); assertThat(dto.getCreatedById()).isEqualTo(editRequest.getCreatedBy().getId()); - assertThat(dto.getSourceRecording().getId()).isEqualTo(editRequest.getSourceRecording().getId()); + assertThat(dto.getSourceRecordingId()).isEqualTo(editRequest.getSourceRecordingId()); } @Test - @DisplayName("Should create an edit request dto from edit request entity without source recording") - void testConstructorWithoutSourceRecording() { - var dto = new EditRequestDTO(editRequest, false); + @DisplayName("Should create an edit request dto with empty edit instructions") + void testConstructorFromEntityEmptyEditInstructions() { + editRequest.setEditCutInstructions(List.of()); + var dto = new EditRequestDTO(editRequest); assertThat(dto.getId()).isEqualTo(editRequest.getId()); - assertThat(dto.getSourceRecording()).isNull(); - assertThat(dto.getEditInstruction().getRequestedInstructions()).isEmpty(); - assertThat(dto.getEditInstruction().getFfmpegInstructions()).isEmpty(); + assertThat(dto.getEditCutInstructions()).isEmpty(); assertThat(dto.getStatus()).isEqualTo(editRequest.getStatus()); assertThat(dto.getStartedAt()).isEqualTo(editRequest.getStartedAt()); assertThat(dto.getFinishedAt()).isEqualTo(editRequest.getFinishedAt()); - assertThat(dto.getCreatedById()).isEqualTo(editRequest.getCreatedBy().getId()); assertThat(dto.getCreatedAt()).isEqualTo(editRequest.getCreatedAt()); assertThat(dto.getModifiedAt()).isEqualTo(editRequest.getModifiedAt()); - assertThat(dto.getJointlyAgreed()).isEqualTo(editRequest.getJointlyAgreed()); - assertThat(dto.getRejectionReason()).isEqualTo(editRequest.getRejectionReason()); - assertThat(dto.getApprovedAt()).isEqualTo(editRequest.getApprovedAt()); - assertThat(dto.getApprovedBy()).isEqualTo(editRequest.getApprovedBy()); + assertThat(dto.getCreatedById()).isEqualTo(editRequest.getCreatedBy().getId()); + assertThat(dto.getSourceRecordingId()).isEqualTo(editRequest.getSourceRecordingId()); } @Test - @DisplayName("Should create an edit request dto from edit request entity with source recording") - void testConstructorWithSourceRecording() { - var dto = new EditRequestDTO(editRequest, true); + @DisplayName("Should convert list of instructions to and from DTO") + void testConvertListOfInstructionsToAndFromDTO() { + EditCutInstructions firstEditInstructions = new EditCutInstructions(editRequest.getId(), 300, 500, "first edit"); + EditCutInstructions secondEditInstructions = new EditCutInstructions(editRequest.getId(), 600, 750, "second edit"); + List nonDto = List.of(firstEditInstructions, secondEditInstructions); - assertThat(dto.getId()).isEqualTo(editRequest.getId()); - assertThat(dto.getEditInstruction().getRequestedInstructions()).isEmpty(); - assertThat(dto.getEditInstruction().getFfmpegInstructions()).isEmpty(); - assertThat(dto.getStatus()).isEqualTo(editRequest.getStatus()); - assertThat(dto.getStartedAt()).isEqualTo(editRequest.getStartedAt()); - assertThat(dto.getFinishedAt()).isEqualTo(editRequest.getFinishedAt()); - assertThat(dto.getCreatedAt()).isEqualTo(editRequest.getCreatedAt()); - assertThat(dto.getModifiedAt()).isEqualTo(editRequest.getModifiedAt()); - assertThat(dto.getCreatedById()).isEqualTo(editRequest.getCreatedBy().getId()); - assertThat(dto.getSourceRecording().getId()).isEqualTo(editRequest.getSourceRecording().getId()); + List dtoList = EditRequestDTO.toDTO(nonDto); + + assertThat(dtoList.size()).isEqualTo(nonDto.size()); + + assertThat(dtoList.getFirst().getReason()).isEqualTo(nonDto.getFirst().getReason()); + assertThat(dtoList.getFirst().getStart()).isEqualTo(nonDto.getFirst().getStart()); + assertThat(dtoList.getFirst().getEnd()).isEqualTo(nonDto.getFirst().getEnd()); + + assertThat(dtoList.get(1).getReason()).isEqualTo(nonDto.get(1).getReason()); + assertThat(dtoList.get(1).getStart()).isEqualTo(nonDto.get(1).getStart()); + assertThat(dtoList.get(1).getEnd()).isEqualTo(nonDto.get(1).getEnd()); + + List convertedBackFromDto = EditRequestDTO.fromDTO(dtoList); + + assertThat(convertedBackFromDto.getFirst().getEditRequestId()).isEqualTo(nonDto.getFirst().getEditRequestId()); + assertThat(convertedBackFromDto.getFirst().getReason()).isEqualTo(nonDto.getFirst().getReason()); + assertThat(convertedBackFromDto.getFirst().getStart()).isEqualTo(nonDto.getFirst().getStart()); + assertThat(convertedBackFromDto.getFirst().getEnd()).isEqualTo(nonDto.getFirst().getEnd()); + + assertThat(convertedBackFromDto.getLast().getEditRequestId()).isEqualTo(nonDto.getLast().getEditRequestId()); + assertThat(convertedBackFromDto.getLast().getReason()).isEqualTo(nonDto.getLast().getReason()); + assertThat(convertedBackFromDto.getLast().getStart()).isEqualTo(nonDto.getLast().getStart()); + assertThat(convertedBackFromDto.getLast().getEnd()).isEqualTo(nonDto.getLast().getEnd()); } + } + diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTOTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTOTest.java index 8fbc6e7819..d0e0f0822e 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTOTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/dto/RecordingDTOTest.java @@ -3,11 +3,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; 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.EditCutInstructions; +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.CourtType; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import uk.gov.hmcts.reform.preapi.enums.ParticipantType; import uk.gov.hmcts.reform.preapi.enums.RecordingOrigin; import uk.gov.hmcts.reform.preapi.util.HelperFactory; @@ -15,6 +20,7 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.Comparator; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -25,9 +31,25 @@ public class RecordingDTOTest { private Recording recordingEntity; private Case caseEntity; + private EditRequest editRequestEntity; + private User user; @BeforeEach void setUp() { + user = HelperFactory.createUser( + "first", "second", "email", + null, "phone", null + ); + + editRequestEntity = new EditRequest(); + editRequestEntity.setId(UUID.randomUUID()); + editRequestEntity.setCreatedBy(user); + editRequestEntity.setStatus(EditRequestStatus.PENDING); + editRequestEntity.setEditCutInstructions(List.of( + new EditCutInstructions(editRequestEntity.getId(), 100, 200, "reason") + )); + + caseEntity = HelperFactory.createCase( HelperFactory.createCourt(CourtType.CROWN, "Foo Court", null), "1234567890", @@ -50,10 +72,11 @@ void setUp() { recordingEntity = new Recording(); recordingEntity.setId(UUID.randomUUID()); - recordingEntity.setVersion(1); + recordingEntity.setVersion(2); recordingEntity.setFilename("test.mp4"); recordingEntity.setCaptureSession(captureSession); recordingEntity.setRecordings(Set.of()); + recordingEntity.setEditRequest(editRequestEntity); } @DisplayName("Should create a recording from entity") @@ -73,6 +96,20 @@ void createRecordingFromEntity() { .toList(); assertThat(sortedList.get(0).getFirstName()).isEqualTo("Jane"); assertThat(sortedList.get(1).getFirstName()).isEqualTo("John"); + + assertThat(model.getEditRequest().getId()).isEqualTo(editRequestEntity.getId()); + assertThat(model.getEditRequest().getStatus()).isEqualTo(editRequestEntity.getStatus()); + assertThat(model.getEditStatus()).isEqualTo(editRequestEntity.getStatus()); + + EditCutInstructions firstEditInstrEntity = editRequestEntity.getEditCutInstructions().getFirst(); + EditCutInstructionsDTO firstEditInstrDto = model.getEditRequest().getEditCutInstructions().getFirst(); + + assertThat(firstEditInstrDto.getEditRequestId()) + .isEqualTo(firstEditInstrEntity.getEditRequestId()); + assertThat(firstEditInstrDto.getStart()) + .isEqualTo(firstEditInstrEntity.getStart()); + assertThat(firstEditInstrDto.getEnd()) + .isEqualTo(firstEditInstrEntity.getEnd()); } @Test @@ -138,4 +175,14 @@ public void testParticipantSorting() { assertEquals("BBB", participants.get(1).getFirstName()); assertEquals("CCC", participants.get(2).getFirstName()); } + + + @DisplayName("Should set recording edit status to ORIGINAL for version 1") + @Test + void shouldSetRecordingEditStatusToORIGINALForVersion1() { + recordingEntity.setVersion(1); + var model = new RecordingDTO(recordingEntity); + assertThat(model.getEditStatus()).isEqualTo(EditRequestStatus.ORIGINAL); + } + } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidatorTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidatorTest.java index 39d2402bec..668269bd61 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidatorTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/dto/validators/CreateEditRequestStatusValidatorTest.java @@ -3,7 +3,7 @@ import jakarta.validation.ConstraintValidatorContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; import java.sql.Timestamp; @@ -27,7 +27,7 @@ public void setUp() { @Test public void isValidStatusNullTrue() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(null); var context = mock(ConstraintValidatorContext.class); @@ -36,7 +36,7 @@ public void isValidStatusNullTrue() { @Test public void isValidRejectedRejectionReasonNullFalse() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.REJECTED); dto.setRejectionReason(null); var context = mock(ConstraintValidatorContext.class); @@ -56,7 +56,7 @@ public void isValidRejectedRejectionReasonNullFalse() { @Test public void isValidRejectedRejectionReasonNotNullTrue() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.REJECTED); dto.setRejectionReason("Reason"); var context = mock(ConstraintValidatorContext.class); @@ -66,7 +66,7 @@ public void isValidRejectedRejectionReasonNotNullTrue() { @Test public void isValidSubmittedJointlyAgreedNullFalse() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.SUBMITTED); dto.setJointlyAgreed(null); var context = mock(ConstraintValidatorContext.class); @@ -86,7 +86,7 @@ public void isValidSubmittedJointlyAgreedNullFalse() { @Test public void isValidSubmittedJointlyAgreedNotNullTrue() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.SUBMITTED); dto.setJointlyAgreed(true); var context = mock(ConstraintValidatorContext.class); @@ -96,7 +96,7 @@ public void isValidSubmittedJointlyAgreedNotNullTrue() { @Test public void isValidApprovedApprovedAtNullFalse() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.APPROVED); dto.setApprovedAt(null); var context = mock(ConstraintValidatorContext.class); @@ -116,7 +116,7 @@ public void isValidApprovedApprovedAtNullFalse() { @Test public void isValidApprovedApprovedByNullFalse() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.APPROVED); dto.setApprovedAt(Timestamp.from(Instant.now())); dto.setApprovedBy(null); @@ -137,7 +137,7 @@ public void isValidApprovedApprovedByNullFalse() { @Test public void isValidApprovedAllFieldsNotNullTrue() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setStatus(EditRequestStatus.APPROVED); dto.setApprovedAt(Timestamp.from(Instant.now())); dto.setApprovedBy("Someone"); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java index 37321e79a9..d17ffe7fca 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/email/GovNotifyTest.java @@ -9,6 +9,7 @@ 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.EditEmailParameters; 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; @@ -164,32 +165,24 @@ private Case getCase() { 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; + private EditEmailParameters createEditEmailParameters() { + return EditEmailParameters.builder() + .toEmailAddress("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(); } @DisplayName(("Should send recording ready email")) @@ -282,17 +275,18 @@ 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); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setToEmailAddress("group-email@example.com"); + editEmailParameters.setEditRequestStatus(EditRequestStatus.REJECTED); + editEmailParameters.setRejectionReason("REJECTION REASON"); + editEmailParameters.setJointlyAgreed(true); var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingRejected("group-email@example.com", request); + var response = govNotify.sendEmailAboutEditingRequest(editEmailParameters); - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); + assertThat(response.get().getFromEmail()).isEqualTo("SENDER EMAIL"); + assertThat(response.get().getSubject()).isEqualTo("SUBJECT TEXT"); + assertThat(response.get().getBody()).isEqualTo("MESSAGE TEXT"); } @Test @@ -301,16 +295,17 @@ 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); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setToEmailAddress("group-email@example.com"); + editEmailParameters.setEditRequestStatus(EditRequestStatus.SUBMITTED); + editEmailParameters.setJointlyAgreed(true); var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingJointlyAgreed("group-email@example.com", request); + var response = govNotify.sendEmailAboutEditingRequest(editEmailParameters); - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); + assertThat(response.get().getFromEmail()).isEqualTo("SENDER EMAIL"); + assertThat(response.get().getSubject()).isEqualTo("SUBJECT TEXT"); + assertThat(response.get().getBody()).isEqualTo("MESSAGE TEXT"); } @Test @@ -319,16 +314,17 @@ 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); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setToEmailAddress("group-email@example.com"); + editEmailParameters.setEditRequestStatus(EditRequestStatus.SUBMITTED); + editEmailParameters.setJointlyAgreed(false); var govNotify = new GovNotify("http://localhost:8080", mockGovNotifyClient); - var response = govNotify.editingNotJointlyAgreed("group-email@example.com", request); + var response = govNotify.sendEmailAboutEditingRequest(editEmailParameters); - assertThat(response.getFromEmail()).isEqualTo("SENDER EMAIL"); - assertThat(response.getSubject()).isEqualTo("SUBJECT TEXT"); - assertThat(response.getBody()).isEqualTo("MESSAGE TEXT"); + assertThat(response.get().getFromEmail()).isEqualTo("SENDER EMAIL"); + assertThat(response.get().getSubject()).isEqualTo("SUBJECT TEXT"); + assertThat(response.get().getBody()).isEqualTo("MESSAGE TEXT"); } @DisplayName(("Should fail to send recording ready email")) @@ -454,55 +450,14 @@ 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); + EditEmailParameters editEmailParameters = createEditEmailParameters(); + editEmailParameters.setToEmailAddress("group-email@example.com"); + editEmailParameters.setEditRequestStatus(EditRequestStatus.REJECTED); + editEmailParameters.setRejectionReason("REJECTION REASON"); 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 - ) - ) + () -> govNotify.sendEmailAboutEditingRequest(editEmailParameters)) .getMessage(); assertThat(message).isEqualTo("Failed to send email to: " + getCase().getCourt().getGroupEmail()); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditCutInstructionsTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditCutInstructionsTest.java new file mode 100644 index 0000000000..3558afb96b --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditCutInstructionsTest.java @@ -0,0 +1,36 @@ +package uk.gov.hmcts.reform.preapi.entities; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; + +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class EditCutInstructionsTest { + + @Test + @DisplayName("Should construct from DTO") + void constructFromDTO() { + UUID editRequestId = UUID.randomUUID(); + EditCutInstructionsDTO dto = new EditCutInstructionsDTO(editRequestId, "01:01:01", "01:02:02", "reason"); + EditCutInstructions entity = new EditCutInstructions(dto); + + assertThat(entity.getEditRequestId()).isEqualTo(editRequestId); + assertThat(entity.getStart()).isEqualTo(3661); + assertThat(entity.getEnd()).isEqualTo(3722); + assertThat(entity.getReason()).isEqualTo("reason"); + } + + @Test + @DisplayName("Should construct from split parameters") + void constructFromSplitParameters() { + UUID editRequestId = UUID.randomUUID(); + EditCutInstructions entity = new EditCutInstructions(editRequestId, 3, 5, "reason"); + assertThat(entity.getEditRequestId()).isEqualTo(editRequestId); + assertThat(entity.getStart()).isEqualTo(3); + assertThat(entity.getEnd()).isEqualTo(5); + assertThat(entity.getReason()).isEqualTo("reason"); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditRequestTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditRequestTest.java new file mode 100644 index 0000000000..df4eefe150 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/entities/EditRequestTest.java @@ -0,0 +1,91 @@ +package uk.gov.hmcts.reform.preapi.entities; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.reform.preapi.enums.EditRequestStatus; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static uk.gov.hmcts.reform.preapi.entities.EditRequest.convertEditCutInstructionsFromJson; + +public class EditRequestTest { + + private static final Path sampleEditRequestDeprecatedJsonPath = + Path.of("src/test/resources/edit-requests/existing/sample-for-edit-request-test.json"); + + @Test + @DisplayName("Should be able to deserialize from old-style JSON into ordered edit cut requests") + void deserializeDeprecatedJson() throws IOException { + var dto = new EditRequest(); + + String content = Files.readString(sampleEditRequestDeprecatedJsonPath, StandardCharsets.UTF_8); + + List editCutInstructionsList = convertEditCutInstructionsFromJson(content); + dto.setEditCutInstructions(editCutInstructionsList); + assertThat(dto.getEditCutInstructions().size()).isEqualTo(2); + + EditCutInstructions first = dto.getEditCutInstructions().getFirst(); + assertThat(first.getEditRequestId()).isEqualTo(UUID.fromString("f264f9cb-0203-4fa6-9234-b6efec06819e")); + assertThat(first.getStart()).isEqualTo(180); + assertThat(first.getEnd()).isEqualTo(300); + assertThat(first.getReason()).isEqualTo("Removing 2 minutes"); + + EditCutInstructions second = dto.getEditCutInstructions().get(1); + assertThat(second.getEditRequestId()).isEqualTo(UUID.fromString("f264f9cb-0203-4fa6-9234-b6efec06819e")); + assertThat(second.getStart()).isEqualTo(480); + assertThat(second.getEnd()).isEqualTo(490); + assertThat(second.getReason()).isEqualTo("Removing 10 seconds"); + } + + @Test + @DisplayName("Should get details for audit") + void getDetailsForAudit() { + EditRequest editRequest = new EditRequest(); + editRequest.setId(UUID.randomUUID()); + editRequest.setSourceRecordingId(UUID.randomUUID()); + editRequest.setStatus(EditRequestStatus.COMPLETE); + editRequest.setEditCutInstructions(List.of( + new EditCutInstructions(editRequest.getId(), 100, 200, "reason") + )); + editRequest.setStartedAt(Timestamp.from(Instant.now())); + editRequest.setFinishedAt(Timestamp.from(Instant.now())); + editRequest.setCreatedAt(Timestamp.from(Instant.now())); + editRequest.setModifiedAt(Timestamp.from(Instant.now())); + editRequest.setJointlyAgreed(false); + editRequest.setRejectionReason("Test rejection"); + editRequest.setApprovedAt(Timestamp.from(Instant.now())); + editRequest.setApprovedBy("approver"); + + User user = new User(); + user.setId(UUID.randomUUID()); + editRequest.setCreatedBy(user); + + Map detailsForAudit = editRequest.getDetailsForAudit(); + + assertThat(detailsForAudit.get("id")).isEqualTo(editRequest.getId()); + assertThat(detailsForAudit.get("editInstructions")) + .isEqualTo(format( + "[{\"id\":null,\"editRequestId\":\"%s\",\"start\":100,\"end\":200,\"reason\":\"reason\",\"detailsForAudit\":{}}]", + editRequest.getId() + )); + assertThat(detailsForAudit.get("status")).isEqualTo(editRequest.getStatus()); + assertThat(detailsForAudit.get("startedAt")).isEqualTo(editRequest.getStartedAt()); + assertThat(detailsForAudit.get("finishedAt")).isEqualTo(editRequest.getFinishedAt()); + assertThat(detailsForAudit.get("createdBy")).isEqualTo(editRequest.getCreatedBy().getId()); + assertThat(detailsForAudit.get("rejectionReason")).isEqualTo(editRequest.getRejectionReason()); + assertThat(detailsForAudit.get("jointlyAgreed")).isEqualTo(editRequest.getJointlyAgreed()); + assertThat(detailsForAudit.get("approvedAt")).isEqualTo(editRequest.getApprovedAt()); + assertThat(detailsForAudit.get("approvedBy")).isEqualTo(editRequest.getApprovedBy()); + assertThat(detailsForAudit.get("sourceRecordingId")).isEqualTo(editRequest.getSourceRecordingId()); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/entities/RecordingTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/entities/RecordingTest.java new file mode 100644 index 0000000000..5493d085ce --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/entities/RecordingTest.java @@ -0,0 +1,71 @@ +package uk.gov.hmcts.reform.preapi.entities; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; + +public class RecordingTest { + + @Test + @DisplayName("Should get recording details for audit") + void getRecordingDetailsForAudit() { + Recording parentRecording = new Recording(); + parentRecording.setId(UUID.randomUUID()); + + Recording recording = new Recording(); + recording.setParentRecording(parentRecording); + recording.setVersion(3); + recording.setFilename("test filename"); + recording.setDuration(Duration.ofHours(3)); + recording.setDeletedAt(Timestamp.from(Instant.now())); + recording.setEditInstruction("edit instruction"); + + var audit = recording.getDetailsForAudit(); + assertThat(audit.get("parentRecordingId")).isEqualTo(parentRecording.getId()); + assertThat(audit.get("recordingVersion")).isEqualTo(3); + assertThat(audit.get("recordingFilename")).isEqualTo("test filename"); + assertThat(audit.get("recordingDuration")).isEqualTo("PT3H"); + assertThat(audit.get("recordingEditInstruction")).isEqualTo("edit instruction"); + assertThat(audit.get("deleted")).isEqualTo(true); + } + + @Test + @DisplayName("Should build edit instructions from Java object if available") + void shouldBuildEditInstructionsFromJavaObjectIfAvailable() { + + EditRequest editRequest = new EditRequest(); + editRequest.setId(UUID.randomUUID()); + + EditCutInstructions first = new EditCutInstructions( + editRequest.getId(), 30, 50, + "first edit" + ); + EditCutInstructions second = new EditCutInstructions( + editRequest.getId(), 60, 70, + "second edit" + ); + editRequest.setEditCutInstructions(List.of(first, second)); + + Recording recording = new Recording(); + recording.setEditInstruction("should not be this string: deprecated"); + recording.setEditRequest(editRequest); + + var audit = recording.getDetailsForAudit(); + assertThat(audit.get("recordingEditInstruction")).isEqualTo(format( + "[{\"id\":null," + + "\"editRequestId\":\"%s\",\"start\":30,\"end\":50," + + "\"reason\":\"first edit\",\"detailsForAudit\":{}},{\"id\":null," + + "\"editRequestId\":\"%s\",\"start\":60,\"end\":70," + + "\"reason\":\"second edit\",\"detailsForAudit\":{}}]", editRequest.getId(), editRequest.getId() + )); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructionsTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructionsTest.java deleted file mode 100644 index 3a1fcf5ab3..0000000000 --- a/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/EditInstructionsTest.java +++ /dev/null @@ -1,161 +0,0 @@ -package uk.gov.hmcts.reform.preapi.media.edit; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; -import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class EditInstructionsTest { - @Test - @DisplayName("Should successfully deserialize valid JSON into EditInstructions") - void fromJson_shouldSuccessfullyDeserializeValidJson() { - String validJson = """ - { - "requestedInstructions": [ - { - "start_of_cut": "00:00:00", - "end_of_cut": "00:00:10", - "start": 0, - "end": 10, - "reason": "Some reason" - } - ], - "ffmpegInstructions": [ - { - "start": 10, - "end": 20 - } - ] - } - """; - - EditInstructions result = EditInstructions.fromJson(validJson); - - assertNotNull(result); - assertThat(result.getRequestedInstructions()).hasSize(1); - assertThat(result.getFfmpegInstructions()).hasSize(1); - - EditCutInstructionDTO requestedInstruction = result.getRequestedInstructions().getFirst(); - assertThat(requestedInstruction.getStartOfCut()).isEqualTo("00:00:00"); - assertThat(requestedInstruction.getEndOfCut()).isEqualTo("00:00:10"); - assertThat(requestedInstruction.getStart()).isEqualTo(0L); - assertThat(requestedInstruction.getEnd()).isEqualTo(10L); - assertThat(requestedInstruction.getReason()).isEqualTo("Some reason"); - - FfmpegEditInstructionDTO ffmpegInstruction = result.getFfmpegInstructions().getFirst(); - assertThat(ffmpegInstruction.getStart()).isEqualTo(10L); - assertThat(ffmpegInstruction.getEnd()).isEqualTo(20L); - } - - @Test - @DisplayName("Should throw exception when JSON has invalid structure") - void fromJson_shouldThrowExceptionForInvalidJsonStructure() { - String invalidJson = """ - { - "invalidField": "value" - } - """; - - assertUnknownServerExceptionOnParse(invalidJson); - } - - @Test - @DisplayName("Should throw exception when JSON is malformed") - void fromJson_shouldThrowExceptionForMalformedJson() { - String malformedJson = """ - { - "requestedInstructions": [ - // syntax error: missing closing bracket - } - """; - - assertUnknownServerExceptionOnParse(malformedJson); - } - - @Test - @DisplayName("Should throw exception when JSON is null") - void fromJson_shouldThrowExceptionWhenJsonIsNull() { - assertUnknownServerExceptionOnParse(null); - } - - @Test - @DisplayName("Should throw exception when JSON is empty") - void fromJson_shouldThrowExceptionWhenJsonIsEmpty() { - String emptyJson = ""; - - assertUnknownServerExceptionOnParse(emptyJson); - } - - @Test - @DisplayName("Should return empty lists when JSON has no instructions") - void fromJson_shouldHandleEmptyJsonObject() { - String emptyJson2 = "{}"; - - EditInstructions result = EditInstructions.fromJson(emptyJson2); - - assertNull(result.getRequestedInstructions()); - assertNull(result.getFfmpegInstructions()); - } - - @Test - @DisplayName("Should successfully deserialize valid JSON into EditInstructions without error") - void tryFromJson_shouldSuccessfullyDeserializeValidJson() { - String validJson = """ - { - "requestedInstructions": [ - { - "start_of_cut": "00:00:00", - "end_of_cut": "00:00:10", - "start": 0, - "end": 10, - "reason": "Some reason" - } - ], - "ffmpegInstructions": [ - { - "start": 10, - "end": 20 - } - ] - } - """; - - EditInstructions result = EditInstructions.tryFromJson(validJson); - - assertNotNull(result); - } - - @Test - @DisplayName("Should return null when encountering a parsing error") - void tryFromJson_ReturnsNullOnError() { - assertNull(EditInstructions.tryFromJson(""" - { - "invalidField": "value" - } - """)); - assertNull(EditInstructions.tryFromJson(""" - { - "requestedInstructions": [ - // syntax error: missing closing bracket - } - """)); - assertNull(EditInstructions.tryFromJson(null)); - assertNull(EditInstructions.tryFromJson("")); - } - - private static void assertUnknownServerExceptionOnParse(String json) { - String message = assertThrows( - UnknownServerException.class, - () -> EditInstructions.fromJson(json) - ).getMessage(); - - assertEquals("Unknown Server Exception: Unable to read edit instructions", message); - } -} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegServiceTest.java deleted file mode 100644 index e247636399..0000000000 --- a/src/test/java/uk/gov/hmcts/reform/preapi/media/edit/FfmpegServiceTest.java +++ /dev/null @@ -1,450 +0,0 @@ -package uk.gov.hmcts.reform.preapi.media.edit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.exec.CommandLine; -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.component.CommandExecutor; -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.NotFoundException; -import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; -import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; -import uk.gov.hmcts.reform.preapi.media.storage.AzureIngestStorageService; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.time.Duration; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -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.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@SpringBootTest(classes = FfmpegService.class) -public class FfmpegServiceTest { - @MockitoBean - private AzureIngestStorageService azureIngestStorageService; - - @MockitoBean - private AzureFinalStorageService azureFinalStorageService; - - @MockitoBean - private CommandExecutor commandExecutor; - - @Autowired - private FfmpegService ffmpegService; - - private Recording recording; - private EditRequest editRequest; - - @BeforeEach - void setUp() { - recording = new Recording(); - recording.setId(UUID.randomUUID()); - recording.setFilename("input.mp4"); - recording.setDuration(Duration.ofMinutes(3)); - - editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setSourceRecording(recording); - editRequest.setEditInstruction("{}"); - editRequest.setStatus(EditRequestStatus.PENDING); - } - - @Test - @DisplayName("Should throw error when source recording does not have a filename set") - void performEditFileNameNull() { - recording.setFilename(null); - var newRecordingId = UUID.randomUUID(); - - var message = assertThrows( - NotFoundException.class, - () -> ffmpegService.performEdit(newRecordingId, editRequest) - ).getMessage(); - - assertThat(message).isEqualTo("Not found: No file name provided"); - - verify(azureFinalStorageService,never()).downloadBlob(any(), any(), any()); - verify(commandExecutor, never()).execute(any()); - verify(azureIngestStorageService, never()).uploadBlob(any(), any(), any()); - } - - @Test - @DisplayName("Should throw error when source recording does not have a filename set") - void performEditDownloadFailure() throws JsonProcessingException { - setRandomEditInstructions(); - var newRecordingId = UUID.randomUUID(); - var inputFileName = editRequest.getSourceRecording().getFilename(); - var containerName = editRequest.getSourceRecording().getId().toString(); - - when(azureFinalStorageService.downloadBlob(containerName, inputFileName, inputFileName)) - .thenReturn(false); - - var message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.performEdit(newRecordingId, editRequest) - ).getMessage(); - - assertThat(message) - .isEqualTo("Unknown Server Exception: Error occurred when attempting to download file: " - + editRequest.getSourceRecording().getFilename()); - - verify(azureFinalStorageService, times(1)).downloadBlob(containerName, inputFileName, inputFileName); - verify(commandExecutor, never()).execute(any()); - verify(azureIngestStorageService, never()).uploadBlob(any(), any(), any()); - } - - @Test - @DisplayName("Should throw error when ffmpeg execution encounters and error") - void performEditExecuteFailure() throws JsonProcessingException { - setRandomEditInstructions(); - var newRecordingId = UUID.randomUUID(); - var inputFileName = editRequest.getSourceRecording().getFilename(); - var containerName = editRequest.getSourceRecording().getId().toString(); - - when(azureFinalStorageService.downloadBlob(containerName, inputFileName, inputFileName)) - .thenReturn(true); - when(commandExecutor.execute(any(CommandLine.class))).thenReturn(false); - - var message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.performEdit(newRecordingId, editRequest) - ).getMessage(); - - assertThat(message) - .isEqualTo("Unknown Server Exception: Error occurred when attempting to process edit request: " - + editRequest.getId()); - - verify(azureFinalStorageService, times(1)).downloadBlob(containerName, inputFileName, inputFileName); - verify(commandExecutor, times(1)).execute(any(CommandLine.class)); - verify(azureIngestStorageService, never()).uploadBlob(any(), any(), any()); - } - - @Test - @DisplayName("Should return correct duration when valid output is provided") - void getDurationFromMp4Success() { - String containerName = "container-name"; - String fileName = "file-name"; - String mockOutput = "123.456"; - - when(azureFinalStorageService.downloadBlob(containerName, fileName, fileName)) - .thenReturn(true); - when(commandExecutor.executeAndGetOutput(any(CommandLine.class))).thenReturn(mockOutput); - - Duration duration = ffmpegService.getDurationFromMp4(containerName, fileName); - - assertThat(duration).isNotNull(); - assertThat(duration.toMillis()).isEqualTo(123456); - } - - @Test - @DisplayName("Should return null when ffprobe output is non-numeric") - void getDurationFromMp4InvalidOutput() { - String containerName = "container-name"; - String fileName = "file-name"; - when(azureFinalStorageService.downloadBlob(containerName, fileName, fileName)) - .thenReturn(true); - when(commandExecutor.executeAndGetOutput(any(CommandLine.class))).thenReturn("not_a_number"); - - Duration duration = ffmpegService.getDurationFromMp4(containerName, fileName); - - assertThat(duration).isNull(); - } - - @Test - @DisplayName("Should return null when exception is thrown during command execution") - void getDurationFromMp4ThrowsException() { - String containerName = "container-name"; - String fileName = "file-name"; - when(azureFinalStorageService.downloadBlob(containerName, fileName, fileName)) - .thenReturn(true); - when(commandExecutor.executeAndGetOutput(any(CommandLine.class))) - .thenThrow(new RuntimeException("An error occurred")); - - Duration duration = ffmpegService.getDurationFromMp4(containerName, fileName); - - assertThat(duration).isNull(); - } - - @Test - @DisplayName("Should return null when ffprobe returns empty output") - void getDurationFromMp4EmptyOutput() { - String containerName = "container-name"; - String fileName = "file-name"; - when(azureFinalStorageService.downloadBlob(containerName, fileName, fileName)) - .thenReturn(true); - when(commandExecutor.executeAndGetOutput(any(CommandLine.class))).thenReturn(" "); - - Duration duration = ffmpegService.getDurationFromMp4(containerName, fileName); - - assertThat(duration).isNull(); - } - - @Test - @DisplayName("Should throw error when unable to read edit instructions") - void generateCommandFromJsonError() { - editRequest.setEditInstruction(""); - - String message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.generateMultiEditCommands(editRequest, "", "") - ).getMessage(); - - assertThat(message).isEqualTo("Unknown Server Exception: Unable to read edit instructions"); - } - - @Test - @DisplayName("Should throw error when instructions are null") - void generateCommandFromJsonEmpty() { - String message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.generateMultiEditCommands(editRequest, "", "") - ).getMessage(); - - assertThat(message).isEqualTo("Unknown Server Exception: Malformed edit instructions"); - } - - @Test - @DisplayName("Should throw error when instructions are empty") - void generateCommandEditInstructionsEmpty() { - editRequest.setEditInstruction("{\"ffmpegInstructions\": []}"); - String message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.generateMultiEditCommands(editRequest, "", "") - ).getMessage(); - - assertThat(message).isEqualTo("Unknown Server Exception: Malformed edit instructions"); - } - - @Test - @DisplayName("Should successfully generate ffmpeg commands") - void generateCommandSuccess() throws JsonProcessingException { - List instructions = setRandomEditInstructions(); - - LinkedHashMap - commands = ffmpegService.generateMultiEditCommands(editRequest, "input.mp4", "output.mp4"); - - assertThat(commands).isNotNull(); - assertThat(commands).hasSize(2); - Map.Entry firstCommandEntry = commands.pollFirstEntry(); - assertThat(firstCommandEntry.getKey()).isEqualTo("0_segment.mp4"); - CommandLine firstCommand = firstCommandEntry.getValue(); - assertThat(firstCommand.getExecutable()).isEqualTo("ffmpeg"); - - String[] args1 = firstCommand.getArguments(); - assertThat(args1).hasSize(12); - assertThat(args1[0]).isEqualTo("-y"); - assertThat(args1[1]).isEqualTo("-ss"); - assertThat(args1[2]).isEqualTo(String.valueOf(instructions.getFirst().getStart())); - assertThat(args1[3]).isEqualTo("-to"); - assertThat(args1[4]).isEqualTo(String.valueOf(instructions.getFirst().getEnd())); - assertThat(args1[5]).isEqualTo("-i"); - assertThat(args1[6]).isEqualTo("input.mp4"); - assertThat(args1[7]).isEqualTo("-c"); - assertThat(args1[8]).isEqualTo("copy"); - assertThat(args1[9]).isEqualTo("-avoid_negative_ts"); - assertThat(args1[10]).isEqualTo("1"); - assertThat(args1[11]).isEqualTo("0_segment.mp4"); - - Map.Entry secondCommandEntry = commands.pollFirstEntry(); - assertThat(secondCommandEntry.getKey()).isEqualTo("1_segment.mp4"); - CommandLine secondCommand = secondCommandEntry.getValue(); - assertThat(secondCommand.getExecutable()).isEqualTo("ffmpeg"); - - String[] args2 = secondCommand.getArguments(); - assertThat(args2).hasSize(12); - assertThat(args2[0]).isEqualTo("-y"); - assertThat(args2[1]).isEqualTo("-ss"); - assertThat(args2[2]).isEqualTo(String.valueOf(instructions.getLast().getStart())); - assertThat(args2[3]).isEqualTo("-to"); - assertThat(args2[4]).isEqualTo(String.valueOf(instructions.getLast().getEnd())); - assertThat(args2[5]).isEqualTo("-i"); - assertThat(args2[6]).isEqualTo("input.mp4"); - assertThat(args2[7]).isEqualTo("-c"); - assertThat(args2[8]).isEqualTo("copy"); - assertThat(args2[9]).isEqualTo("-avoid_negative_ts"); - assertThat(args2[10]).isEqualTo("1"); - assertThat(args2[11]).isEqualTo("1_segment.mp4"); - } - - @Test - @DisplayName("Should generate a single edit command in multi-command mode") - void generateSingleEditCommandInMultiCommandMode() { - editRequest.setEditInstruction("{\"ffmpegInstructions\": [{\"start\": 10, \"end\": 20}]}"); - LinkedHashMap commands = - ffmpegService.generateMultiEditCommands(editRequest, "input.mp4", "output.mp4"); - - assertThat(commands).hasSize(1); - assertThat(commands).containsKey("output.mp4"); - CommandLine command = commands.get("output.mp4"); - assertThat(command.toString()).contains("-ss", "10", "-to", "20", "-i", "input.mp4", "output.mp4"); - } - - @Test - @DisplayName("Should generate multiple edit commands and concatenate segments") - void generateMultipleEditCommandsAndConcatenateSegments() throws JsonProcessingException { - setRandomEditInstructions(); - LinkedHashMap commands = - ffmpegService.generateMultiEditCommands(editRequest, "input.mp4", "output.mp4"); - - assertThat(commands).hasSizeGreaterThan(1); - - commands.forEach((fileName, command) -> { - assertThat(fileName).endsWith("_segment.mp4"); - assertThat(command.toString()).contains("-ss", "-to", "-i", "input.mp4", fileName); - }); - } - - @Test - @DisplayName("Should fail when execution of a generated command fails") - void failWhenExecutionOfGeneratedCommandFails() throws JsonProcessingException { - setRandomEditInstructions(); - UUID newRecordingId = UUID.randomUUID(); - - when(azureFinalStorageService.downloadBlob(any(), any(), any())).thenReturn(true); - when(commandExecutor.execute(any())).thenReturn(false); - - String message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.performEdit(newRecordingId, editRequest) - ).getMessage(); - - assertThat(message).isEqualTo( - "Unknown Server Exception: Error occurred when attempting to process edit request: " + editRequest.getId()); - - verify(commandExecutor, times(1)).execute(any()); - } - - @Test - @DisplayName("Should successfully generate the concat list file") - void successfullyGenerateConcatListFile() throws IOException { - Set segmentFiles = Set.of("segment1.mp4", "segment2.mp4"); - ffmpegService.generateConcatListFile(segmentFiles, "concat-list.txt"); - - File outputFile = new File("concat-list.txt"); - assertThat(outputFile).exists(); - assertThat(outputFile.length()).isGreaterThan(0); - outputFile.delete(); - } - - @Test - @DisplayName("Should successfully execute multi-command ffmpeg flow and upload result") - void shouldExecuteFullMultiCommandFlowSuccessfully() throws JsonProcessingException { - setRandomEditInstructions(); - - when(azureFinalStorageService.downloadBlob(any(), any(), any())).thenReturn(true); - when(commandExecutor.execute(any())).thenReturn(true); - when(azureIngestStorageService.uploadBlob(any(), any(), any())).thenReturn(true); - - UUID newRecordingId = UUID.randomUUID(); - ffmpegService.performEdit(newRecordingId, editRequest); - - // 2 segment commands + 1 concat command = 3 ffmpeg executions - verify(commandExecutor, times(3)).execute(any(CommandLine.class)); - verify(azureFinalStorageService).downloadBlob(any(), any(), any()); - verify(azureIngestStorageService).uploadBlob(any(), any(), any()); - } - - @Test - @DisplayName("Should throw when concat command fails after successful segment commands") - void shouldFailWhenConcatCommandFails() throws JsonProcessingException { - setRandomEditInstructions(); - UUID newRecordingId = UUID.randomUUID(); - - when(azureFinalStorageService.downloadBlob(any(), any(), any())).thenReturn(true); - - when(commandExecutor.execute(any())) - .thenReturn(true) // segment 0 - .thenReturn(true) // segment 1 - .thenReturn(false); // concat - - String message = assertThrows( - UnknownServerException.class, - () -> ffmpegService.performEdit(newRecordingId, editRequest) - ).getMessage(); - - assertThat(message) - .isEqualTo("Unknown Server Exception: Error occurred when attempting to process edit request: " - + editRequest.getId()); - - verify(commandExecutor, times(3)).execute(any()); // 2 segments + 1 concat - verify(azureIngestStorageService, never()).uploadBlob(any(), any(), any()); - } - - @Test - @DisplayName("Should pass correct segment file names to concat list generator") - void shouldPassCorrectFilesToConcatListFile() throws IOException { - var instructions = setRandomEditInstructions(); - var commands = ffmpegService.generateMultiEditCommands(editRequest, "input.mp4", "output.mp4"); - - var concatListFile = new File("test-concat-list.txt"); - ffmpegService.generateConcatListFile(commands.keySet(), concatListFile.getName()); - - var lines = Files.readAllLines(concatListFile.toPath()); - assertThat(lines).hasSize(instructions.size()); - lines.forEach(line -> assertThat(line).startsWith("file '").endsWith(".mp4'")); - - concatListFile.delete(); - } - - @Test - @DisplayName("Should run single command and upload when only one edit instruction is provided") - void shouldRunSingleCommandWhenOneInstructionProvided() { - editRequest.setEditInstruction("{\"ffmpegInstructions\": [{\"start\": 5, \"end\": 15}]}"); - - when(azureFinalStorageService.downloadBlob(any(), any(), any())).thenReturn(true); - when(commandExecutor.execute(any())).thenReturn(true); - when(azureIngestStorageService.uploadBlob(any(), any(), any())).thenReturn(true); - - UUID newRecordingId = UUID.randomUUID(); - ffmpegService.performEdit(newRecordingId, editRequest); - - // Only one command executed (no concat) - verify(commandExecutor, times(1)).execute(any()); - - verify(azureFinalStorageService, times(1)).downloadBlob(any(), any(), any()); - verify(azureIngestStorageService, times(1)).uploadBlob(any(), any(), any()); - } - - private String generateEditInstructionsJson(List ffmpegEditInstructions) - throws JsonProcessingException { - var objectMapper = new ObjectMapper(); - var instructions = new EditInstructions(List.of(), ffmpegEditInstructions); - - return objectMapper.writeValueAsString(instructions); - } - - private List setRandomEditInstructions() throws JsonProcessingException { - var random = new Random(); - var instructions = List.of( - FfmpegEditInstructionDTO.builder() - .start(random.nextInt(0, 10)) - .end(random.nextInt(60, 70)) - .build(), - FfmpegEditInstructionDTO.builder() - .start(random.nextInt(120, 130)) - .end(180) - .build() - ); - editRequest.setEditInstruction(generateEditInstructionsJson(instructions)); - return instructions; - } -} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/security/AuthorisationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/security/AuthorisationServiceTest.java index 325d097cae..bc426330c5 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/security/AuthorisationServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/security/AuthorisationServiceTest.java @@ -10,7 +10,7 @@ import uk.gov.hmcts.reform.preapi.dto.CreateBookingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCaptureSessionDTO; import uk.gov.hmcts.reform.preapi.dto.CreateCaseDTO; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; import uk.gov.hmcts.reform.preapi.dto.CreateParticipantDTO; import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; import uk.gov.hmcts.reform.preapi.dto.CreateShareBookingDTO; @@ -857,7 +857,7 @@ void canSearchByCaseClosedCaseOpenFalseIsNotAdminOrLevel2() { @Test @DisplayName("Should grant upsert access when the user has access to recording for edit requests") void hasUpsertAccessEditRequest() { - var dto = new CreateEditRequestDTO(); + var dto = new EditRequestDTO(); dto.setSourceRecordingId(UUID.randomUUID()); when(authenticationUser.isAdmin()).thenReturn(true); @@ -893,9 +893,7 @@ void hasEditRequestAccessNotFound() { @DisplayName("Should grant access edit request access when has access to source recording") void hasEditRequestAccessHasRecordingAccess() { var id = UUID.randomUUID(); - var recording = new Recording(); var editRequest = new EditRequest(); - editRequest.setSourceRecording(recording); when(authenticationUser.isAdmin()).thenReturn(false); when(editRequestRepository.findByIdNotLocked(id)).thenReturn(Optional.of(editRequest)); diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/CaseServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/CaseServiceTest.java index 0ba7acd042..2de52deffa 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/services/CaseServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/CaseServiceTest.java @@ -604,7 +604,7 @@ void updateCasePendingClosureWithIncompleteEdits() { caseDTOModel.setState(CaseState.PENDING_CLOSURE); caseDTOModel.setClosedAt(Timestamp.from(Instant.now().plusSeconds(86400))); // +1 day - when(editRequestRepository.existsByCaseIdAndIsIncomplete(caseDTOModel.getId())).thenReturn(true); + when(editRequestRepository.getCaseIdsWithIncompleteEdits()).thenReturn(List.of(caseDTOModel.getId())); when(caseRepository.findById(caseDTOModel.getId())).thenReturn(Optional.of(caseEntity)); when(bookingRepository.findAllByCaseIdAndDeletedAtIsNull(caseEntity)).thenReturn(List.of()); @@ -617,7 +617,7 @@ void updateCasePendingClosureWithIncompleteEdits() { + caseDTOModel.getId() + ") has incomplete edits which must be completed before updating state to PENDING_CLOSURE"); - verify(editRequestRepository, times(1)).existsByCaseIdAndIsIncomplete(caseDTOModel.getId()); + verify(editRequestRepository, times(1)).getCaseIdsWithIncompleteEdits(); verify(caseRepository, never()).saveAndFlush(any()); } 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..9ab250e276 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; @@ -10,20 +11,33 @@ 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.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.EditCutInstructions; 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.exception.BadRequestException; +import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; import uk.gov.hmcts.reform.preapi.util.HelperFactory; import java.sql.Timestamp; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.UUID; +import static java.lang.String.format; +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.times; import static org.mockito.Mockito.verify; @@ -37,6 +51,9 @@ class EditNotificationServiceTest { @MockitoBean private EmailServiceFactory emailServiceFactory; + @MockitoBean + private RecordingRepository recordingRepository; + @Autowired private EditNotificationService underTest; @@ -58,6 +75,9 @@ class EditNotificationServiceTest { @MockitoBean private LoggingService loggingService; + private Participant witnessParticipant; + private Participant defendantParticipant; + private User shareWith1; private User shareWith2; private User sharedBy; @@ -71,34 +91,81 @@ 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 = HelperFactory.createCourt(CourtType.CROWN, "Test Court", "TC", 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())); - - shareBooking2 = HelperFactory.createShareBooking(shareWith2, sharedBy, booking, - new Timestamp(System.currentTimeMillis())); + witnessParticipant = HelperFactory.createParticipant( + testCase, + ParticipantType.WITNESS, + "Witness first name", + "Witness last name", + null + ); + defendantParticipant = HelperFactory.createParticipant( + testCase, + ParticipantType.DEFENDANT, + "Defendant first name", + "Defendant last name", + null + ); + + booking = HelperFactory.createBooking( + testCase, new Timestamp(System.currentTimeMillis()), null, + Set.of(this.witnessParticipant, this.defendantParticipant) + ); + + shareBooking1 = HelperFactory.createShareBooking( + shareWith1, 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); + + // Recording fields + UUID recordingId = UUID.randomUUID(); + when(mockRecording.getId()).thenReturn(recordingId); when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); + when(mockRecording.getEditRequest()).thenReturn(mockEditRequest); + + // Capture session fields when(mockCaptureSession.getBooking()).thenReturn(booking); + + // Edit request fields + final List editInstructions = List.of( + new EditCutInstructions(UUID.randomUUID(), 0, 30, "first thirty seconds reason"), + new EditCutInstructions(UUID.randomUUID(), 45, 50, "first thirty seconds reason"), + new EditCutInstructions(UUID.randomUUID(), 61, 120, "") + ); + + when(mockEditRequest.getSourceRecordingId()).thenReturn(recordingId); + when(mockEditRequest.getEditCutInstructions()).thenReturn(editInstructions); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + when(recordingRepository.findById(recordingId)).thenReturn(Optional.of(mockRecording)); } @DisplayName("Should be able to send notifications") @@ -115,44 +182,155 @@ void testSendNotifications() { @Test void testEditRequestSubmittedJointlyAgreed() { when(mockEditRequest.getJointlyAgreed()).thenReturn(true); - court.setGroupEmail(testEmail); - underTest.onEditRequestSubmitted(mockEditRequest); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.SUBMITTED); + + underTest.editRequestStatusWasUpdated(mockEditRequest); - verify(emailService, times(1)).editingJointlyAgreed(testEmail, mockEditRequest); + ArgumentCaptor captor = ArgumentCaptor.forClass(EditEmailParameters.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(captor.capture()); verifyNoMoreInteractions(emailService); + + EditEmailParameters emailParameters = captor.getValue(); + + assertThat(emailParameters.getEditRequestStatus()).isEqualTo(EditRequestStatus.SUBMITTED); + assertThat(emailParameters.getToEmailAddress()).isEqualTo(testEmail); + assertThat(emailParameters.getWitnessName()).isEqualTo(witnessParticipant.getFirstName()); + assertThat(emailParameters.getDefendantName()) + .isEqualTo(format("%s %s", defendantParticipant.getFirstName(), defendantParticipant.getLastName())); + assertThat(emailParameters.getCaseReference()).isEqualTo(booking.getCaseId().getReference()); + assertThat(emailParameters.getNumberOfRequestedEditInstructions()) + .isEqualTo(mockEditRequest.getEditCutInstructions().size()); + assertThat(emailParameters.getCourtName()).isEqualTo(booking.getCaseId().getCourt().getName()); + assertThat(emailParameters.getEditSummary()).isEqualTo(""" + Edit 1:\s + Start time: 00:00 + End time: 00:00:30 + Time Removed: 00:00:30 + Reason: first thirty seconds reason + + Edit 2:\s + Start time: 00:00:45 + End time: 00:00:50 + Time Removed: 00:00:05 + Reason: first thirty seconds reason + + Edit 3:\s + Start time: 00:01:01 + End time: 00:02 + Time Removed: 00:00:59 + Reason:\s + + """); + assertThat(emailParameters.getRejectionReason()).isEqualTo(null); + assertThat(emailParameters.getJointlyAgreed()).isEqualTo(true); } @DisplayName("Should be able to notify appropriately when edit request is submitted not jointly agreed") @Test void testEditRequestSubmittedNotJointlyAgreed() { when(mockEditRequest.getJointlyAgreed()).thenReturn(false); - court.setGroupEmail(testEmail); - underTest.onEditRequestSubmitted(mockEditRequest); - verify(emailService, times(1)).editingNotJointlyAgreed(testEmail, mockEditRequest); + underTest.editRequestStatusWasUpdated(mockEditRequest); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditEmailParameters.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(captor.capture()); verifyNoMoreInteractions(emailService); + + EditEmailParameters emailParameters = captor.getValue(); + assertThat(emailParameters.getJointlyAgreed()).isEqualTo(false); + + assertThat(emailParameters.getEditRequestStatus()).isEqualTo(EditRequestStatus.SUBMITTED); + assertThat(emailParameters.getToEmailAddress()).isEqualTo(testEmail); + assertThat(emailParameters.getWitnessName()).isEqualTo(witnessParticipant.getFirstName()); + assertThat(emailParameters.getDefendantName()) + .isEqualTo(format("%s %s", defendantParticipant.getFirstName(), defendantParticipant.getLastName())); + assertThat(emailParameters.getCaseReference()).isEqualTo(booking.getCaseId().getReference()); + assertThat(emailParameters.getNumberOfRequestedEditInstructions()) + .isEqualTo(mockEditRequest.getEditCutInstructions().size()); + assertThat(emailParameters.getCourtName()).isEqualTo(booking.getCaseId().getCourt().getName()); + assertThat(emailParameters.getEditSummary()).isEqualTo(""" + Edit 1:\s + Start time: 00:00 + End time: 00:00:30 + Time Removed: 00:00:30 + Reason: first thirty seconds reason + + Edit 2:\s + Start time: 00:00:45 + End time: 00:00:50 + Time Removed: 00:00:05 + Reason: first thirty seconds reason + + Edit 3:\s + Start time: 00:01:01 + End time: 00:02 + Time Removed: 00:00:59 + Reason:\s + + """); + assertThat(emailParameters.getRejectionReason()).isEqualTo(null); } @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); + when(mockEditRequest.getRejectionReason()).thenReturn("rejected reason"); - verify(emailService, times(1)).editingRejected(testEmail, mockEditRequest); + underTest.editRequestStatusWasUpdated(mockEditRequest); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditEmailParameters.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(captor.capture()); verifyNoMoreInteractions(emailService); + + EditEmailParameters emailParameters = captor.getValue(); + + assertThat(emailParameters.getEditRequestStatus()).isEqualTo(EditRequestStatus.REJECTED); + assertThat(emailParameters.getToEmailAddress()).isEqualTo(testEmail); + assertThat(emailParameters.getWitnessName()).isEqualTo(witnessParticipant.getFirstName()); + assertThat(emailParameters.getDefendantName()) + .isEqualTo(format("%s %s", defendantParticipant.getFirstName(), defendantParticipant.getLastName())); + assertThat(emailParameters.getCaseReference()).isEqualTo(booking.getCaseId().getReference()); + assertThat(emailParameters.getNumberOfRequestedEditInstructions()) + .isEqualTo(mockEditRequest.getEditCutInstructions().size()); + assertThat(emailParameters.getCourtName()).isEqualTo(booking.getCaseId().getCourt().getName()); + assertThat(emailParameters.getEditSummary()).isEqualTo(""" + Edit 1:\s + Start time: 00:00 + End time: 00:00:30 + Time Removed: 00:00:30 + Reason: first thirty seconds reason + + Edit 2:\s + Start time: 00:00:45 + End time: 00:00:50 + Time Removed: 00:00:05 + Reason: first thirty seconds reason + + Edit 3:\s + Start time: 00:01:01 + End time: 00:02 + Time Removed: 00:00:59 + Reason:\s + + """); + assertThat(emailParameters.getRejectionReason()).isEqualTo("rejected reason"); + assertThat(emailParameters.getJointlyAgreed()).isEqualTo(false); } - @DisplayName("Should not attempt to email if court email address is null") + @DisplayName("Pass on attempt to email if court email address is null") @Test void testEditRequestNotificationsWhenNoCourtEmail() { court.setGroupEmail(null); - underTest.onEditRequestSubmitted(mockEditRequest); - verifyNoInteractions(emailService); + underTest.editRequestStatusWasUpdated(mockEditRequest); // Exception handled downstream + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditEmailParameters.class); + verify(emailService, times(1)).sendEmailAboutEditingRequest(captor.capture()); + verifyNoMoreInteractions(emailService); + EditEmailParameters emailParameters = captor.getValue(); - underTest.onEditRequestRejected(mockEditRequest); - verifyNoInteractions(emailService); + assertThat(emailParameters.getToEmailAddress()).isEqualTo(null); } } 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 4b2433947d..a9d0b09030 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 @@ -1,117 +1,43 @@ package uk.gov.hmcts.reform.preapi.services; -import com.azure.resourcemanager.mediaservices.models.JobState; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.dao.PessimisticLockingFailureException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import uk.gov.hmcts.reform.preapi.controllers.base.PreApiController; import uk.gov.hmcts.reform.preapi.controllers.params.SearchEditRequests; -import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO; -import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; -import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO; -import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO; -import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; -import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetDTO; -import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetResponseDTO; import uk.gov.hmcts.reform.preapi.entities.AppAccess; -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.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.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; -import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; -import uk.gov.hmcts.reform.preapi.media.IMediaService; -import uk.gov.hmcts.reform.preapi.media.MediaServiceBroker; -import uk.gov.hmcts.reform.preapi.media.edit.EditInstructions; -import uk.gov.hmcts.reform.preapi.media.edit.FfmpegService; -import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; -import uk.gov.hmcts.reform.preapi.media.storage.AzureIngestStorageService; -import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; -import uk.gov.hmcts.reform.preapi.repositories.RecordingRepository; import uk.gov.hmcts.reform.preapi.security.authentication.UserAuthentication; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestCrudService; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestFromCsv; +import uk.gov.hmcts.reform.preapi.services.edit.EditRequestProcessingService; import uk.gov.hmcts.reform.preapi.util.HelperFactory; import java.sql.Timestamp; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.Set; import java.util.UUID; -import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @SpringBootTest(classes = EditRequestService.class) public class EditRequestServiceTest { @MockitoBean - private EditRequestRepository editRequestRepository; + private EditRequestCrudService editRequestCrudService; @MockitoBean - private RecordingRepository recordingRepository; + private EditRequestProcessingService editRequestProcessingService; @MockitoBean - private FfmpegService ffmpegService; - - @MockitoBean - private RecordingService recordingService; - - @MockitoBean - private AzureIngestStorageService azureIngestStorageService; - - @MockitoBean - private AzureFinalStorageService azureFinalStorageService; - - @MockitoBean - private MediaServiceBroker mediaServiceBroker; - - @MockitoBean - private IMediaService mediaService; - - @MockitoBean - private EditNotificationService editNotificationService; - - @MockitoBean - private Recording mockRecording; - - @MockitoBean - private Recording mockParentRecording; + private EditRequestFromCsv csvService; @MockitoBean private UserAuthentication mockAuth; @@ -120,832 +46,55 @@ public class EditRequestServiceTest { private AppAccess mockAppAccess; @MockitoBean - private CaptureSession mockCaptureSession; + private SearchEditRequests searchEditRequests; @MockitoBean - private EditRequest mockEditRequest; - - @MockitoBean - private CaptureSession captureSession; - - @Autowired - private EditRequestService underTest; + private Pageable pageable; private User courtClerkUser; - private Booking booking; - private static final UUID mockRecordingId = UUID.randomUUID(); - private static final UUID mockParentRecId = UUID.randomUUID(); - private static final UUID mockCaptureSessionId = UUID.randomUUID(); + private static final UUID editRequestId = UUID.randomUUID(); + private static final UUID recordingId = UUID.randomUUID(); + + @Autowired + private EditRequestService editRequestService; @BeforeEach void setup() { - User shareWith1 = HelperFactory.createUser( - "First", "User", "example1@example.com", - new Timestamp(System.currentTimeMillis()), null, null - ); - - User shareWith2 = HelperFactory.createUser( - "Second", "User", "example2@example.com", + courtClerkUser = HelperFactory.createUser( + "Court", "Clerk", "court.clerk@example.com", new Timestamp(System.currentTimeMillis()), null, null ); - courtClerkUser = HelperFactory.createUser("Court", "Clerk", "court.clerk@example.com", - new Timestamp(System.currentTimeMillis()), null, null); - - Court court = HelperFactory.createCourt(CourtType.CROWN, "Test Court", "TC"); - - Case testCase = HelperFactory.createCase(court, "Test Case", false, null); - - booking = HelperFactory.createBooking(testCase, new Timestamp(System.currentTimeMillis()), null); - - ShareBooking shareBooking1 = HelperFactory.createShareBooking( - shareWith1, courtClerkUser, booking, - new Timestamp(System.currentTimeMillis()) - ); - - ShareBooking shareBooking2 = HelperFactory.createShareBooking( - shareWith2, courtClerkUser, booking, - new Timestamp(System.currentTimeMillis()) - ); - - booking.setShares(Set.of(shareBooking1, shareBooking2)); - mockAppAccess.setUser(courtClerkUser); when(mockAuth.getAppAccess()).thenReturn(mockAppAccess); when(mockAuth.isAppUser()).thenReturn(true); SecurityContextHolder.getContext().setAuthentication(mockAuth); - - when(mockCaptureSession.getBooking()).thenReturn(booking); - when(mockCaptureSession.getId()).thenReturn(mockCaptureSessionId); - - when(mockRecording.getId()).thenReturn(mockRecordingId); - when(mockRecording.getCaptureSession()).thenReturn(mockCaptureSession); - when(mockRecording.getParentRecording()).thenReturn(mockParentRecording); - when(mockRecording.getDuration()).thenReturn(Duration.ofMinutes(3)); - when(mockRecording.getFilename()).thenReturn("filename"); - - when(mockParentRecording.getId()).thenReturn(mockParentRecId); - when(mockParentRecording.getCaptureSession()).thenReturn(mockCaptureSession); - - when(mediaServiceBroker.getEnabledMediaService()).thenReturn(mediaService); - - when(azureFinalStorageService.getRecordingDuration(mockRecordingId)).thenReturn(Duration.ofMinutes(3)); - when(azureFinalStorageService.getMp4FileName(mockRecordingId.toString())).thenReturn("filename"); - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)).thenReturn(Optional.of(mockRecording)); - when(mockEditRequest.getId()).thenReturn(UUID.randomUUID()); - } - - @Test - @DisplayName("Should return all pending edit requests") - void getPendingEditRequestsSuccess() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.PENDING); - - when(editRequestRepository.findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING)) - .thenReturn(Optional.of(editRequest)); - - var res = underTest.getNextPendingEditRequest(); - - assertThat(res).isPresent(); - assertThat(res.get().getId()).isEqualTo(editRequest.getId()); - assertThat(res.get().getStatus()).isEqualTo(EditRequestStatus.PENDING); - - verify(editRequestRepository, times(1)).findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING); - } - - @Test - @DisplayName("Should attempt to perform edit request and return error on ffmpeg service error") - void performEditFfmpegError() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.PENDING); - editRequest.setSourceRecording(mockRecording); - - when(editRequestRepository.findById(any())).thenReturn(Optional.of(editRequest)); - - doThrow(UnknownServerException.class) - .when(ffmpegService).performEdit(any(UUID.class), eq(editRequest)); - - assertThrows( - Exception.class, - () -> underTest.performEdit(editRequest) - ); - - verify(editRequestRepository, times(1)).findById(editRequest.getId()); - - ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EditRequest.class); - verify(editRequestRepository, times(1)).save(saveCaptor.capture()); - EditRequest updatedEditRequest = saveCaptor.getValue(); - assertThat(updatedEditRequest.getId()).isEqualTo(editRequest.getId()); - assertThat(updatedEditRequest.getStatus()).isEqualTo(EditRequestStatus.ERROR); - - ArgumentCaptor performEditCaptor = ArgumentCaptor.forClass(EditRequest.class); - verify(ffmpegService, times(1)).performEdit(any(UUID.class), performEditCaptor.capture()); - EditRequest performedEditRequest = performEditCaptor.getValue(); - assertThat(performedEditRequest.getId()).isEqualTo(editRequest.getId()); - - verify(recordingService, never()).upsert(any()); - } - - @Test - @DisplayName("Should perform edit request and return created recording") - void performEditSuccess() throws InterruptedException { - EditRequest editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.PENDING); - editRequest.setStartedAt(Timestamp.from(Instant.now())); - editRequest.setSourceRecording(mockRecording); - editRequest.setEditInstruction("{}"); - - when(editRequestRepository.findById(any())).thenReturn(Optional.of(editRequest)); - when(editRequestRepository.findByIdNotLocked(editRequest.getId())).thenReturn(Optional.of(editRequest)); - when(recordingService.getNextVersionNumber(mockRecording.getId())).thenReturn(2); - when(recordingService.upsert(any(CreateRecordingDTO.class))).thenReturn(UpsertResult.CREATED); - - var newRecordingDto = new RecordingDTO(); - newRecordingDto.setParentRecordingId(mockRecording.getId()); - when(recordingService.findById(any(UUID.class))).thenReturn(newRecordingDto); - when(azureIngestStorageService.doesContainerExist(anyString())).thenReturn(true); - var importResponse = new GenerateAssetResponseDTO(); - importResponse.setJobStatus(JobState.FINISHED.toString()); - when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(importResponse); - when(azureFinalStorageService.getMp4FileName(anyString())).thenReturn("index.mp4"); - - var res = underTest.performEdit(editRequest); - assertThat(res).isNotNull(); - assertThat(res.getParentRecordingId()).isEqualTo(mockRecording.getId()); - - verify(editRequestRepository, times(1)).findById(editRequest.getId()); - - ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EditRequest.class); - verify(editRequestRepository, times(1)).save(saveCaptor.capture()); - EditRequest updatedEditRequest = saveCaptor.getValue(); - assertThat(updatedEditRequest.getId()).isEqualTo(editRequest.getId()); - assertThat(updatedEditRequest.getStatus()).isEqualTo(EditRequestStatus.COMPLETE); - - ArgumentCaptor performEditCaptor = ArgumentCaptor.forClass(EditRequest.class); - verify(ffmpegService, times(1)).performEdit(any(UUID.class), performEditCaptor.capture()); - EditRequest performedEditRequest = performEditCaptor.getValue(); - assertThat(performedEditRequest.getId()).isEqualTo(editRequest.getId()); - - verify(recordingService, times(1)).upsert(any(CreateRecordingDTO.class)); - verify(recordingService, times(1)).findById(any(UUID.class)); - verify(azureIngestStorageService, times(1)).doesContainerExist(anyString()); - verify(azureIngestStorageService, times(1)).getMp4FileName(anyString()); - verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); - verify(azureFinalStorageService, times(1)).getMp4FileName(anyString()); - - // Notification is sent by RecordingListener instead - verify(editNotificationService, times(0)).sendNotifications(booking); - } - - @Test - @DisplayName("Should throw not found error when edit request cannot be found with specified id") - void performEditNotFound() { - var id = UUID.randomUUID(); - - when(editRequestRepository.findById(id)).thenReturn(Optional.empty()); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.markAsProcessing(id) - ).getMessage(); - - assertThat(message).isEqualTo("Not found: Edit Request: " + id); - - verify(editRequestRepository, times(1)).findById(id); - verify(editRequestRepository, never()).save(any(EditRequest.class)); - verify(editRequestRepository, never()).saveAndFlush(any(EditRequest.class)); - verify(ffmpegService, never()).performEdit(any(UUID.class), any(EditRequest.class)); - verify(recordingService, never()).upsert(any(CreateRecordingDTO.class)); - verify(recordingService, never()).findById(any(UUID.class)); - } - - @Test - @DisplayName("Should not perform edit and return null when status of edit request is not PENDING") - void performEditStatusNotPending() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.PROCESSING); - - when(editRequestRepository.findById(editRequest.getId())).thenReturn(Optional.of(editRequest)); - - var message = assertThrows( - ResourceInWrongStateException.class, - () -> underTest.markAsProcessing(editRequest.getId()) - ).getMessage(); - - assertThat(message) - .isEqualTo("Resource EditRequest(" - + editRequest.getId() - + ") is in a PROCESSING state. Expected state is PENDING."); - - verify(editRequestRepository, times(1)).findById(editRequest.getId()); - verify(editRequestRepository, never()).save(any(EditRequest.class)); - verify(ffmpegService, never()).performEdit(any(UUID.class), any(EditRequest.class)); - verify(recordingService, never()).upsert(any(CreateRecordingDTO.class)); - verify(recordingService, never()).findById(any(UUID.class)); - } - - @Test - @DisplayName("Should throw lock error when encounters locked edit request") - void performEditLocked() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.PENDING); - - doThrow(PessimisticLockingFailureException.class) - .when(editRequestRepository).findById(editRequest.getId()); - - assertThrows( - PessimisticLockingFailureException.class, - () -> underTest.markAsProcessing(editRequest.getId()) - ); - - verify(editRequestRepository, times(1)).findById(editRequest.getId()); - verify(editRequestRepository, never()).saveAndFlush(any(EditRequest.class)); - } - - @Test - @DisplayName("Should create a new edit request") - void createEditRequestSuccess() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecording.getId()); - dto.setStatus(EditRequestStatus.PENDING); - dto.setEditInstructions(instructions); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecording.getId())) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); - - var response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.CREATED); - - verify(recordingService, times(1)).syncRecordingMetadataWithStorage(mockRecording.getId()); - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecording.getId()); - verify(editRequestRepository, times(1)).findById(dto.getId()); - verify(mockAuth, times(1)).getAppAccess(); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); - } - - @Test - @DisplayName("Should update an edit request") - void updateEditRequestSuccess() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecording.getId()); - dto.setStatus(EditRequestStatus.PENDING); - dto.setEditInstructions(instructions); - - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecording.getId())) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(editRequest)); - - var response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - assertThat(editRequest.getId()).isEqualTo(dto.getId()); - assertThat(editRequest.getStatus()).isEqualTo(EditRequestStatus.PENDING); - assertThat(editRequest.getSourceRecording().getId()).isEqualTo(mockRecording.getId()); - assertThat(editRequest.getEditInstruction()) - .contains("\"ffmpegInstructions\":[{\"start\":0,\"end\":60},{\"start\":120,\"end\":180}]"); - - verify(recordingService, times(1)).syncRecordingMetadataWithStorage(mockRecording.getId()); - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecording.getId()); - verify(editRequestRepository, times(1)).findById(dto.getId()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); } @Test - @DisplayName("Should throw not found when source recording does not exist") - void createEditRequestSourceRecordingNotFound() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setStatus(EditRequestStatus.PENDING); - dto.setEditInstructions(instructions); - - when(recordingRepository.findByIdAndDeletedAtIsNull(dto.getSourceRecordingId())) - .thenReturn(Optional.empty()); + @DisplayName("Should pass on request for edit request by ID") + void findEditRequestByIdSuccess() { + editRequestService.findById(editRequestId); - var message = assertThrows( - NotFoundException.class, - () -> underTest.upsert(dto) - ).getMessage(); - assertThat(message).isEqualTo("Not found: Source Recording: " + dto.getSourceRecordingId()); + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(UUID.class); + verify(editRequestCrudService, times(1)).findById(saveCaptor.capture()); - verify(recordingService, times(1)).syncRecordingMetadataWithStorage(dto.getSourceRecordingId()); - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(dto.getSourceRecordingId()); - verify(editRequestRepository, never()).findById(any()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, never()).save(any(EditRequest.class)); + UUID editRequestId = saveCaptor.getValue(); + assertThat(editRequestId).isEqualTo(recordingId); } - @Test - @DisplayName("Should throw error when source recording does not have a duration") - void createEditRequestDurationIsNullError() { - when(mockRecording.getDuration()).thenReturn(null); - - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setStatus(EditRequestStatus.PENDING); - dto.setEditInstructions(instructions); - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - var message = assertThrows( - ResourceInWrongStateException.class, - () -> underTest.upsert(dto) - ).getMessage(); - assertThat(message) - .isEqualTo("Source Recording (" + mockRecordingId + ") does not have a valid duration"); - - verify(recordingService, times(1)).syncRecordingMetadataWithStorage(mockRecordingId); - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(editRequestRepository, never()).findById(any()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, never()).save(any(EditRequest.class)); - } @Test - @DisplayName("Should throw bad request when instruction cuts entire recording") - void invertInstructionsBadRequestCutToZeroDuration() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(0L) - .end(180L) - .build()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.invertInstructions(instructions, mockRecording) - ).getMessage(); + @DisplayName("Should pass on request for all edit requests") + void findAllEditRequestsSuccess() { + editRequestService.findAll(searchEditRequests, pageable); - assertThat(message) - .isEqualTo("Invalid Instruction: Cannot cut an entire recording: " - + "Start(00:00:00), End(00:03:00), " - + "Recording Duration(00:03:00)"); - } - - @Test - @DisplayName("Should delete edit request when upserting with empty instructions") - void deleteEmptyInstructions() { - UUID sourceRecordingId = UUID.randomUUID(); - Recording sourceRecording = new Recording(); - sourceRecording.setId(sourceRecordingId); - sourceRecording.setDuration(Duration.ofSeconds(30)); - - CreateEditRequestDTO request = new CreateEditRequestDTO(); - request.setId(UUID.randomUUID()); - request.setSourceRecordingId(sourceRecordingId); - request.setEditInstructions(new ArrayList<>()); - - when(recordingRepository.findByIdAndDeletedAtIsNull(sourceRecordingId)) - .thenReturn(Optional.of(sourceRecording)); - when(editRequestRepository.findById(request.getId())).thenReturn(Optional.of(new EditRequest())); - - UpsertResult result = underTest.upsert(request); - assertThat(result).isEqualTo(UpsertResult.UPDATED); - } - - @Test - @DisplayName("Should ignore attempt to delete non-existent edit request") - void deleteNonExistentEditRequestSuccess() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(UUID.randomUUID()); - dto.setEditInstructions(List.of(EditCutInstructionDTO.builder() - .startOfCut("00:00:00") - .endOfCut("00:00:01") - .build())); - dto.setStatus(EditRequestStatus.DRAFT); - - when(recordingRepository.findByIdAndDeletedAtIsNull(dto.getSourceRecordingId())) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); - - underTest.delete(dto); - - verify(editRequestRepository, times(0)).delete(any(EditRequest.class)); - } - - @Test - @DisplayName("Should throw bad request when trying to create new edit request with empty instructions") - void badRequestEmptyInstructions() { - - UUID sourceRecordingId = UUID.randomUUID(); - var sourceRecording = new Recording(); - sourceRecording.setId(sourceRecordingId); - sourceRecording.setDuration(Duration.ofSeconds(30)); - - CreateEditRequestDTO originalRequest = new CreateEditRequestDTO(); - originalRequest.setId(UUID.randomUUID()); - originalRequest.setSourceRecordingId(sourceRecordingId); - originalRequest.setEditInstructions(new ArrayList<>()); - - when(recordingRepository.findByIdAndDeletedAtIsNull(sourceRecordingId)) - .thenReturn(Optional.of(sourceRecording)); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.upsert(originalRequest) - ).getMessage(); - - assertThat(message) - .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); - } - - @Test - @DisplayName("Should throw bad request when instruction has same value for start and end") - void invertInstructionsBadRequestStartEndEqual() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(60L) - .build()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.invertInstructions(instructions, mockRecording) - ).getMessage(); - - assertThat(message) - .isEqualTo("Invalid instruction: Instruction with 0 second duration invalid: " - + "Start(00:01:00), End(00:01:00)"); - } - - @Test - @DisplayName("Should throw bad request when instruction end time is less than start time") - void invertInstructionsBadRequestEndLTStart() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(50L) - .build()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.invertInstructions(instructions, mockRecording) - ).getMessage(); - - assertThat(message) - .isEqualTo("Invalid instruction: Instruction with end time before start time: " - + "Start(00:01:00), End(00:00:50)"); - } - - @Test - @DisplayName("Should throw bad request when instruction end time exceeds duration") - void invertInstructionsBadRequestEndTimeExceedsDuration() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(200L) // duration is 180 - .build()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.invertInstructions(instructions, mockRecording) - ).getMessage(); - - assertThat(message) - .isEqualTo("Invalid instruction: Instruction end time exceeding duration: " - + "Start(00:01:00), End(00:03:20), " - + "Recording Duration(00:03:00)"); - } - - @Test - @DisplayName("Should throw bad request when instructions overlap") - void invertInstructionsOverlap() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(10L) - .end(30L) - .build()); - instructions.add(EditCutInstructionDTO.builder() - .start(20L) - .end(40L) - .build()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.invertInstructions(instructions, mockRecording) - ).getMessage(); - - assertThat(message).isEqualTo("Overlapping instructions: " - + "Previous End(00:00:30), Current Start(00:00:20)"); - } - - @Test - @DisplayName("Should return inverted instructions (ordered correctly)") - void invertInstructionsSuccess() { - List instructions1 = new ArrayList<>(); - instructions1.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - instructions1.add(EditCutInstructionDTO.builder() - .start(150L) - .end(180L) - .build()); - - var expectedInvertedInstructions = List.of( - FfmpegEditInstructionDTO.builder() - .start(0) - .end(60) - .build(), - FfmpegEditInstructionDTO.builder() - .start(120) - .end(150) - .build() - ); - - assertEditInstructionsEq(expectedInvertedInstructions, - underTest.invertInstructions(instructions1, mockRecording)); - } - - @Test - @DisplayName("Should return inverted instructions (ordered correctly) when not cutting the end") - void invertInstructionsNotCuttingEndSuccess() { - List instructions1 = new ArrayList<>(); - instructions1.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - instructions1.add(EditCutInstructionDTO.builder() - .start(150L) - .end(160L) - .build()); - - var expectedInvertedInstructions = List.of( - FfmpegEditInstructionDTO.builder() - .start(0) - .end(60) - .build(), - FfmpegEditInstructionDTO.builder() - .start(120) - .end(150) - .build(), - FfmpegEditInstructionDTO.builder() - .start(160) - .end(180) - .build() - ); - - assertEditInstructionsEq(expectedInvertedInstructions, - underTest.invertInstructions(instructions1, mockRecording)); - } - - @Test - @DisplayName("Should return edit request when it exists") - void findByIdSuccess() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setSourceRecording(mockRecording); - editRequest.setCreatedBy(courtClerkUser); - editRequest.setStatus(EditRequestStatus.PENDING); - editRequest.setEditInstruction("{}"); - - when(editRequestRepository.findByIdNotLocked(editRequest.getId())).thenReturn(Optional.of(editRequest)); - - var res = underTest.findById(editRequest.getId()); - assertThat(res).isNotNull(); - assertThat(res.getId()).isEqualTo(editRequest.getId()); - assertThat(res.getStatus()).isEqualTo(EditRequestStatus.PENDING); - - verify(editRequestRepository, times(1)).findByIdNotLocked(editRequest.getId()); - } - - @Test - @DisplayName("Should throw error when requested request does not exist") - void findByIdNotFound() { - var id = UUID.randomUUID(); - when(editRequestRepository.findByIdNotLocked(id)).thenReturn(Optional.empty()); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.findById(id) - ).getMessage(); - assertThat(message).isEqualTo("Not found: Edit Request: " + id); - - verify(editRequestRepository, times(1)).findByIdNotLocked(id); - } - - @Test - @DisplayName("Should return new create recording dto for the edit request") - void createRecordingSuccess() { - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.COMPLETE); - editRequest.setEditInstruction("{}"); - editRequest.setSourceRecording(mockRecording); - - when(mockRecording.getFilename()).thenReturn("index.mp4"); - when(recordingService.getNextVersionNumber(mockParentRecId)).thenReturn(2); - - var dto = underTest.createRecordingDto(mockRecordingId, "index.mp4", editRequest); - - assertThat(dto).isNotNull(); - assertThat(dto.getId()).isEqualTo(mockRecordingId); - assertThat(dto.getParentRecordingId()).isEqualTo(mockParentRecId); - assertThat(dto.getVersion()).isEqualTo(2); - assertThat(dto.getEditInstructions()) - .isEqualTo(format("{\"editRequestId\":\"%s\",\"editInstructions\":{\"requestedInstructions\":null," - + "\"ffmpegInstructions\":null}}", editRequest.getId())); - - assertThat(dto.getCaptureSessionId()).isEqualTo(mockCaptureSessionId); - assertThat(dto.getFilename()).isEqualTo("index.mp4"); - - verify(recordingService, times(1)).getNextVersionNumber(mockParentRecId); - } - - @Test - @DisplayName("Should return create recording dto with parent recording") - void createRecordingDtoWithParentRecording() { - when(mockRecording.getFilename()).thenReturn("source.mp4"); - - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - editRequest.setEditInstruction(""" - { - "requestedInstructions": [ ], - "ffmpegInstructions": [ ] - } - """); - - var newRecordingId = UUID.randomUUID(); - when(recordingService.getNextVersionNumber(mockParentRecId)).thenReturn(3); - - var dto = underTest.createRecordingDto(newRecordingId, "newFile.mp4", editRequest); - assertThat(dto.getId()).isEqualTo(newRecordingId); - assertThat(dto.getParentRecordingId()).isEqualTo(mockParentRecId); - assertThat(dto.getFilename()).isEqualTo("newFile.mp4"); - assertThat(dto.getVersion()).isEqualTo(3); - assertThat(dto.getEditInstructions()) - .isEqualTo("{\"editRequestId\":null,\"editInstructions\":{\"requestedInstructions\":[]," - + "\"ffmpegInstructions\":[]}}"); - } - - @Test - @DisplayName("Should throw not found when generate asset cannot find source container") - void generateAssetSourceContainerNotFound() { - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - var newRecordingId = UUID.randomUUID(); - var sourceContainer = newRecordingId + "-input"; - - when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(false); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.generateAsset(newRecordingId, editRequest) - ).getMessage(); - assertThat(message).isEqualTo("Not found: Source Container (" + sourceContainer + ") does not exist"); - - verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); - verifyNoMoreInteractions(azureIngestStorageService); - } - - @Test - @DisplayName("Should throw not found when generate asset cannot find source container's mp4") - void generateAssetSourceContainerMp4NotFound() { - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - var newRecordingId = UUID.randomUUID(); - var sourceContainer = newRecordingId + "-input"; - - when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); - doThrow(new NotFoundException("MP4 file not found in container " + sourceContainer)) - .when(azureIngestStorageService).getMp4FileName(sourceContainer); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.generateAsset(newRecordingId, editRequest) - ).getMessage(); - assertThat(message).isEqualTo("Not found: MP4 file not found in container " + sourceContainer); - - verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); - verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); - verifyNoMoreInteractions(azureIngestStorageService); - } - - @Test - @DisplayName("Should throw error when import asset fails when generating asset") - void generateAssetImportAssetError() throws InterruptedException { - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - var newRecordingId = UUID.randomUUID(); - var sourceContainer = newRecordingId + "-input"; - - when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); - doThrow(new NotFoundException("Something went wrong")).when(mediaService) - .importAsset(any(GenerateAssetDTO.class), eq(false)); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.generateAsset(newRecordingId, editRequest) - ).getMessage(); - assertThat(message).isEqualTo("Not found: Something went wrong"); - - verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); - verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); - verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); - verify(azureIngestStorageService, never()).markContainerAsSafeToDelete(sourceContainer); - verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); - } - - @Test - @DisplayName("Should throw error when import asset fails (returning error) when generating asset") - void generateAssetImportAssetReturnsError() throws InterruptedException { - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - var newRecordingId = UUID.randomUUID(); - var sourceContainer = newRecordingId + "-input"; - - when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); - var generateResponse = new GenerateAssetResponseDTO(); - generateResponse.setJobStatus(JobState.ERROR.toString()); - when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(generateResponse); - - var message = assertThrows( - UnknownServerException.class, - () -> underTest.generateAsset(newRecordingId, editRequest) - ).getMessage(); - assertThat(message) - .isEqualTo("Unknown Server Exception: Failed to generate asset for edit request: " - + editRequest.getSourceRecording().getId() - + ", new recording: " - + newRecordingId); - - verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); - verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); - verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); - verify(azureIngestStorageService, never()).markContainerAsSafeToDelete(sourceContainer); - verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); - verify(azureFinalStorageService, never()).getMp4FileName(any()); - } - - @Test - @DisplayName("Should throw error when generating asset if get mp4 from final fails") - void generateAssetGetMp4FinalNotFound() throws InterruptedException { - var editRequest = new EditRequest(); - editRequest.setSourceRecording(mockRecording); - var newRecordingId = UUID.randomUUID(); - var sourceContainer = newRecordingId + "-input"; - - when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); - var generateResponse = new GenerateAssetResponseDTO(); - generateResponse.setJobStatus(JobState.FINISHED.toString()); - when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(generateResponse); - doThrow(new NotFoundException("MP4 file not found in container " + newRecordingId)) - .when(azureFinalStorageService) - .getMp4FileName(newRecordingId.toString()); - - var message = assertThrows( - NotFoundException.class, - () -> underTest.generateAsset(newRecordingId, editRequest) - ).getMessage(); - assertThat(message).isEqualTo("Not found: MP4 file not found in container " + newRecordingId); - - verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); - verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); - verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); - verify(azureIngestStorageService, times(1)).markContainerAsSafeToDelete(sourceContainer); - verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); - verify(azureFinalStorageService, times(1)).getMp4FileName(any()); + verify(editRequestCrudService, times(1)).findAll(any(), any()); } @Test @@ -955,27 +104,14 @@ void findAllAsAdminUseSetsNullFilters() { when(mockAuth.isAppUser()).thenReturn(false); when(mockAuth.isPortalUser()).thenReturn(false); - EditRequest editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setSourceRecording(mockRecording); - EditInstructions originalEdits = new EditInstructions( - List.of(createCut(10, 20, "some original reason")), - List.of(createSegment(0, 10), createSegment(20, 30))); - editRequest.setEditInstruction(underTest.toJson(originalEdits)); - editRequest.setCreatedBy(courtClerkUser); + ArgumentCaptor pageableArgCaptor = ArgumentCaptor.forClass(Pageable.class); + ArgumentCaptor paramsArg = ArgumentCaptor.forClass(SearchEditRequests.class); - SearchEditRequests params = new SearchEditRequests(); - when(editRequestRepository.searchAllBy(any(), any())).thenReturn(new PageImpl<>(List.of(editRequest))); + verify(editRequestCrudService, times(1)) + .findAll(paramsArg.capture(), pageableArgCaptor.capture()); - Page result = underTest.findAll(params, Pageable.unpaged()); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - verify(editRequestRepository).searchAllBy( - argThat(search -> - search.getAuthorisedBookings() == null - && search.getAuthorisedCourt() == null), - any(Pageable.class)); + assertThat(paramsArg.getAllValues()).isEmpty(); + assertThat(pageableArgCaptor.getAllValues()).isEqualTo(Pageable.unpaged()); } @Test @@ -986,16 +122,14 @@ void findAllAsAppUserSetsCourtFilterOnly() { when(mockAuth.isPortalUser()).thenReturn(false); when(mockAuth.getCourtId()).thenReturn(UUID.randomUUID()); - SearchEditRequests params = new SearchEditRequests(); - when(editRequestRepository.searchAllBy(any(), any())).thenReturn(Page.empty()); + ArgumentCaptor pageableArgCaptor = ArgumentCaptor.forClass(Pageable.class); + ArgumentCaptor paramsArg = ArgumentCaptor.forClass(SearchEditRequests.class); - underTest.findAll(params, Pageable.unpaged()); + verify(editRequestCrudService, times(1)) + .findAll(paramsArg.capture(), pageableArgCaptor.capture()); - verify(editRequestRepository).searchAllBy( - argThat(p -> - p.getAuthorisedBookings() == null - && p.getAuthorisedCourt().equals(mockAuth.getCourtId())), - any(Pageable.class)); + assertThat(paramsArg.getValue().getAuthorisedBookings()).isEmpty(); + assertThat(pageableArgCaptor.getAllValues()).isEqualTo(Pageable.unpaged()); } @Test @@ -1005,417 +139,16 @@ void findAllAsPortalUserSetsAuthedBookingFilterOnly() { when(mockAuth.isAppUser()).thenReturn(false); when(mockAuth.isPortalUser()).thenReturn(true); when(mockAuth.getSharedBookings()).thenReturn(List.of(UUID.randomUUID(), UUID.randomUUID())); - when(editRequestRepository.searchAllBy(any(), any())).thenReturn(Page.empty()); - - SearchEditRequests params = new SearchEditRequests(); - underTest.findAll(params, Pageable.unpaged()); - - verify(editRequestRepository).searchAllBy( - argThat(p -> - p.getAuthorisedBookings().containsAll(mockAuth.getSharedBookings()) - && p.getAuthorisedCourt() == null), - any(Pageable.class)); - } - - @Test - @DisplayName("Should combine instructions correctly with single new command") - void combineCutsOnOriginalTimelineSingleCommand() { - var originallyKeptSegments = List.of(createSegment(0, 10), createSegment(20, 30)); - var originalCutSegments = List.of(createCut(10, 20, "some reason")); - var originalInstructions = new EditInstructions(originalCutSegments, originallyKeptSegments); - - // Cut at 5–8 in the edited timeline mapping to 5–8 in the original timeline - var newCut = createCut(5, 8, "test"); - - var result = underTest.combineCutsOnOriginalTimeline(originalInstructions, List.of(newCut)); - assertThat(result).hasSize(2); + ArgumentCaptor pageableArgCaptor = ArgumentCaptor.forClass(Pageable.class); + ArgumentCaptor paramsArg = ArgumentCaptor.forClass(SearchEditRequests.class); - assertThat(result.getFirst().getStart()).isEqualTo(5); - assertThat(result.getFirst().getEnd()).isEqualTo(8); - assertThat(result.getFirst().getReason()).isEqualTo("test"); + verify(editRequestCrudService, times(1)) + .findAll(paramsArg.capture(), pageableArgCaptor.capture()); - assertThat(result.getLast().getStart()).isEqualTo(10); - assertThat(result.getLast().getEnd()).isEqualTo(20); - assertThat(result.getLast().getReason()).isEqualTo("some reason"); + assertThat(paramsArg.getValue().getAuthorisedBookings()).containsAll(mockAuth.getSharedBookings()); + assertThat(paramsArg.getValue().getAuthorisedCourt()).isEqualTo(null); + assertThat(pageableArgCaptor.getAllValues()).isEqualTo(Pageable.unpaged()); } - @Test - @DisplayName("Should combine instructions correctly with cuts across multiple segments") - void combineCutsOnOriginalTimelineMapsCutThatSpanningMultipleSegments() { - var originallyKeptSegments = List.of(createSegment(0, 10), createSegment(20, 30)); - var originalCutSegments = List.of(createCut(10, 20, "some reason")); - var originalInstructions = new EditInstructions(originalCutSegments, originallyKeptSegments); - - // Cut at 8–12 in the edited timeline: - // - 8–10 -> 8–10 in original (segment 1) - // - 10–12 -> 20–22 in original (segment 2) - var newCut = createCut(8, 12, "test"); - - var result = underTest.combineCutsOnOriginalTimeline(originalInstructions, List.of(newCut)); - - assertThat(result).hasSize(1); - - assertThat(result.getFirst().getStart()).isEqualTo(8); - assertThat(result.getFirst().getEnd()).isEqualTo(22); - assertThat(result.getFirst().getReason()).isEqualTo("test"); - } - - @Test - @DisplayName("Should upsert when isOriginalRecordingEdit is false and isInstructionCombination false") - void upsertIsOriginalRecordingEditFalseIsInstructionCombinationFalse() { - // when editing an edit from legacy editing - when(mockRecording.getFilename()).thenReturn("filename.mp4"); - when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(30)); - - CreateEditRequestDTO request = new CreateEditRequestDTO(); - request.setId(UUID.randomUUID()); - request.setSourceRecordingId(mockRecordingId); - request.setStatus(EditRequestStatus.PENDING); - request.setEditInstructions(new ArrayList<>(List.of(createCut(10, 20, "some reason")))); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(request.getId())).thenReturn(Optional.of(new EditRequest())); - - UpsertResult result = underTest.upsert(request); - assertThat(result).isEqualTo(UpsertResult.UPDATED); - - ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); - - verify(editRequestRepository, times(1)).save(captor.capture()); - - EditRequest editRequest = captor.getValue(); - assertThat(editRequest.getId()).isEqualTo(request.getId()); - assertThat(editRequest.getSourceRecording().getId()).isEqualTo(mockRecordingId); - assertThat(editRequest.getStatus()).isEqualTo(EditRequestStatus.PENDING); - assertThat(editRequest.getEditInstruction()).isNotNull(); - - EditInstructions editInstructions = EditInstructions.tryFromJson(editRequest.getEditInstruction()); - assertThat(editInstructions).isNotNull(); - assertThat(editInstructions.getRequestedInstructions()).isNotNull(); - assertThat(editInstructions.getRequestedInstructions()).hasSize(1); - assertThat(editInstructions.getRequestedInstructions().getFirst().getStart()).isEqualTo(10); - assertThat(editInstructions.getRequestedInstructions().getFirst().getEnd()).isEqualTo(20); - - assertThat(editInstructions.getFfmpegInstructions()).isNotNull(); - - assertEditInstructionsEq(List.of(createSegment(0, 10), createSegment(20, 30)), - editInstructions.getFfmpegInstructions()); - } - - @Test - @DisplayName("Should throw exception when editInstructions is null for *new* edit request") - void validateEditInstructionsIsEmptyForNewEditRequest() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setStatus(EditRequestStatus.DRAFT); - dto.setEditInstructions(new ArrayList<>()); - - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.empty()); - - var message = assertThrows( - BadRequestException.class, - () -> underTest.upsert(dto) - ).getMessage(); - - assertThat(message) - .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); - } - - @Test - @DisplayName("Should delete edit instruction when editInstructions is empty for *existing* edit request") - void validateEditInstructionsIsEmpty() throws Exception { - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setEditInstructions(List.of()); - dto.setStatus(EditRequestStatus.DRAFT); - dto.setEditInstructions(new ArrayList<>()); - - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(mockEditRequest)); - - UpsertResult upsertResult = underTest.upsert(dto); - - assertThat(upsertResult).isEqualTo(UpsertResult.UPDATED); - - verify(editRequestRepository, times(1)).delete(mockEditRequest); - } - - @DisplayName("Should be able to upsert edit instructions with CSV file") - void upsertEditInstructionsWithCSVFile() { - - final String fileContents = """ - Edit Number,Start time of cut,End time of cut,Total time removed,Reason - 1,00:00:00,00:00:30,00:30:00,first thirty seconds reason - 2,00:01:01,00:02:00,00:00:59, - """; - - final String expectedEditInstructions = """ - { - "requestedInstructions": [ - { - "start_of_cut": "00:00:00", - "end_of_cut": "00:00:30", - "reason": "first thirty seconds reason", - "start": 0, - "end": 30 - }, - { - "start_of_cut": "00:01:01", - "end_of_cut": "00:02:00", - "reason": "", - "start": 61, - "end": 120 - } - ], - "ffmpegInstructions": [ - { - "start": 30, - "end": 61 - }, - { - "start": 120, - "end": 180 - } - ] - } - """; - - final MockMultipartFile file = new MockMultipartFile( - "file", "edit_instructions.csv", - PreApiController.CSV_FILE_TYPE, fileContents.getBytes() - ); - - EditRequest returnedByDb = new EditRequest(); - returnedByDb.setCreatedBy(courtClerkUser); - - when(editRequestRepository.findById(any())).thenReturn(Optional.of(returnedByDb)); - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)).thenReturn(Optional.of(mockRecording)); - - underTest.upsert(mockRecordingId, file); - - ArgumentCaptor savedEditRequest = ArgumentCaptor.forClass(EditRequest.class); - verify(editRequestRepository, times(1)).save(savedEditRequest.capture()); - - assertThat(savedEditRequest.getValue().getId()).isNotNull(); - assertThat(savedEditRequest.getValue().getSourceRecording()).isEqualTo(mockRecording); - assertThat(savedEditRequest.getValue().getStatus()).isEqualTo(EditRequestStatus.PENDING); - assertThat(savedEditRequest.getValue().getCreatedBy()).isEqualTo(courtClerkUser); - - JSONAssert.assertEquals( - expectedEditInstructions, savedEditRequest.getValue().getEditInstruction(), JSONCompareMode.LENIENT); - } - - - @DisplayName("Should throw an exception if updating edit instructions with non-CSV") - @Test - void upsertEditInstructionsWithNotCSVFile() { - final String fileContents = """ -Region,Court,PRE Inbox Address -South East,Example Court,PRE.Edits.Example@justice.gov.uk - """; - - MockMultipartFile file = new MockMultipartFile( - "file", "edits.csv", - "text/xml", fileContents.getBytes() - ); - - assertThrows( - BadRequestException.class, - () -> underTest.upsert(mockRecordingId, file) - ); - } - - @DisplayName("Should throw an exception if updating edit instructions with empty file") - @Test - void upsertEditInstructionsWithEmptyFile() { - final String fileContents = ""; - - MockMultipartFile file = new MockMultipartFile( - "file", "edits.csv", - PreApiController.CSV_FILE_TYPE, fileContents.getBytes() - ); - - assertThrows( - BadRequestException.class, - () -> underTest.upsert(mockRecordingId, file) - ); - } - - @Test - @DisplayName("Should upsert when isOriginalRecordingEdit is false and isInstructionCombination true") - void upsertIsOriginalRecordingEditFalseIsInstructionCombinationTrue() { - // when editing an edit from the new editing process - final EditInstructions originalEdits = new EditInstructions( - List.of(createCut(10, 20, "some original reason")), - List.of(createSegment(0, 10), createSegment(20, 30))); - - CreateEditRequestDTO request = new CreateEditRequestDTO(); - request.setId(UUID.randomUUID()); - request.setSourceRecordingId(mockRecordingId); - request.setStatus(EditRequestStatus.PENDING); - request.setEditInstructions(new ArrayList<>(List.of(createCut(5, 8, "some new reason")))); - - when(mockRecording.getFilename()).thenReturn("filename.mp4"); - when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(27)); - when(mockParentRecording.getDuration()).thenReturn(Duration.ofSeconds(30)); - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - when(mockRecording.getEditInstruction()).thenReturn(underTest.toJson(originalEdits)); - when(editRequestRepository.findById(request.getId())).thenReturn(Optional.of(new EditRequest())); - when(mockAuth.isAppUser()).thenReturn(true); - - UpsertResult result = underTest.upsert(request); - - assertThat(result).isEqualTo(UpsertResult.UPDATED); - - ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); - - verify(editRequestRepository, times(1)).save(captor.capture()); - - EditRequest editRequest = captor.getValue(); - assertThat(editRequest.getId()).isEqualTo(request.getId()); - assertThat(editRequest.getSourceRecording().getId()).isEqualTo(mockParentRecId); - assertThat(editRequest.getStatus()).isEqualTo(EditRequestStatus.PENDING); - assertThat(editRequest.getEditInstruction()).isNotNull(); - - EditInstructions editInstructions = EditInstructions.tryFromJson(editRequest.getEditInstruction()); - assertThat(editInstructions).isNotNull(); - assertThat(editInstructions.getRequestedInstructions()).isNotNull(); - assertThat(editInstructions.getRequestedInstructions()).hasSize(2); - assertThat(editInstructions.getRequestedInstructions().getFirst().getStart()).isEqualTo(5); - assertThat(editInstructions.getRequestedInstructions().getFirst().getEnd()).isEqualTo(8); - assertThat(editInstructions.getRequestedInstructions().getFirst().getReason()).isEqualTo("some new reason"); - assertThat(editInstructions.getRequestedInstructions().getLast().getStart()).isEqualTo(10); - assertThat(editInstructions.getRequestedInstructions().getLast().getEnd()).isEqualTo(20); - assertThat(editInstructions.getRequestedInstructions().getLast().getReason()).isEqualTo("some original reason"); - - assertThat(editInstructions.getFfmpegInstructions()).isNotNull(); - - assertEditInstructionsEq(List.of(createSegment(0, 5), createSegment(8, 10), createSegment(20, 30)), - editInstructions.getFfmpegInstructions()); - } - - @Test - @DisplayName("Should trigger request submission jointly agreed email on submission") - void upsertOnSubmittedJointlyAgreed() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setStatus(EditRequestStatus.SUBMITTED); - dto.setEditInstructions(instructions); - dto.setJointlyAgreed(true); - - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.DRAFT); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(editRequest)); - - var response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(editRequestRepository, times(1)).findById(dto.getId()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); - verify(editNotificationService, times(1)).onEditRequestSubmitted(editRequest); - } - - @Test - @DisplayName("Should trigger request submission not jointly agreed email on submission") - void upsertOnSubmittedNotJointlyAgreed() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setStatus(EditRequestStatus.SUBMITTED); - dto.setEditInstructions(instructions); - dto.setJointlyAgreed(false); - - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.DRAFT); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(editRequest)); - - var response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(editRequestRepository, times(1)).findById(dto.getId()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); - verify(editNotificationService, times(1)).onEditRequestSubmitted(editRequest); - } - - @Test - @DisplayName("Should trigger request rejection email on edit request rejection") - void upsertOnRejected() { - List instructions = new ArrayList<>(); - instructions.add(EditCutInstructionDTO.builder() - .start(60L) - .end(120L) - .build()); - - var dto = new CreateEditRequestDTO(); - dto.setId(UUID.randomUUID()); - dto.setSourceRecordingId(mockRecordingId); - dto.setStatus(EditRequestStatus.REJECTED); - dto.setEditInstructions(instructions); - dto.setJointlyAgreed(false); - - var editRequest = new EditRequest(); - editRequest.setId(UUID.randomUUID()); - editRequest.setStatus(EditRequestStatus.SUBMITTED); - - when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)) - .thenReturn(Optional.of(mockRecording)); - when(editRequestRepository.findById(dto.getId())).thenReturn(Optional.of(editRequest)); - - var response = underTest.upsert(dto); - assertThat(response).isEqualTo(UpsertResult.UPDATED); - - verify(recordingRepository, times(1)).findByIdAndDeletedAtIsNull(mockRecordingId); - verify(editRequestRepository, times(1)).findById(dto.getId()); - verify(mockAuth, never()).getAppAccess(); - verify(editRequestRepository, times(1)).save(any(EditRequest.class)); - verify(editNotificationService, times(1)).onEditRequestRejected(editRequest); - } - - - private static void assertEditInstructionsEq(List expected, - List actual) { - assertThat(actual.size()).isEqualTo(expected.size()); - - for (int i = 0; i < expected.size(); i++) { - assertThat(actual.get(i).getStart()).isEqualTo(expected.get(i).getStart()); - assertThat(actual.get(i).getEnd()).isEqualTo(expected.get(i).getEnd()); - } - } - - private static FfmpegEditInstructionDTO createSegment(long start, long end) { - return new FfmpegEditInstructionDTO(start, end); - } - - private static EditCutInstructionDTO createCut(long start, long end, String reason) { - return new EditCutInstructionDTO(start, end, reason); - } } diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationServiceTest.java new file mode 100644 index 0000000000..d4ff26c467 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/AssetGenerationServiceTest.java @@ -0,0 +1,182 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import com.azure.resourcemanager.mediaservices.models.JobState; +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.media.GenerateAssetDTO; +import uk.gov.hmcts.reform.preapi.dto.media.GenerateAssetResponseDTO; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.exception.UnknownServerException; +import uk.gov.hmcts.reform.preapi.media.IMediaService; +import uk.gov.hmcts.reform.preapi.media.MediaServiceBroker; +import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; +import uk.gov.hmcts.reform.preapi.media.storage.AzureIngestStorageService; + +import java.time.Duration; +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.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = AssetGenerationService.class) +public class AssetGenerationServiceTest { + + @MockitoBean + private AzureIngestStorageService azureIngestStorageService; + + @MockitoBean + private AzureFinalStorageService azureFinalStorageService; + + @MockitoBean + private MediaServiceBroker mediaServiceBroker; + + @MockitoBean + private IMediaService mediaService; + + @Autowired + private AssetGenerationService underTest; + + private static final UUID newRecordingId = UUID.randomUUID(); + private static final UUID originalRecordingId = UUID.randomUUID(); + + @BeforeEach + void setup() throws InterruptedException { + when(azureFinalStorageService.getRecordingDuration(originalRecordingId)).thenReturn(Duration.ofMinutes(3)); + when(azureFinalStorageService.getMp4FileName(originalRecordingId.toString())).thenReturn("index.mp4"); + + when(azureIngestStorageService.doesContainerExist(anyString())).thenReturn(true); + + when(mediaServiceBroker.getEnabledMediaService()).thenReturn(mediaService); + + GenerateAssetResponseDTO importResponse = new GenerateAssetResponseDTO(); + importResponse.setJobStatus(JobState.FINISHED.toString()); + when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(importResponse); + } + + @Test + @DisplayName("Should throw not found when generate asset cannot find source container") + void generateAssetSourceContainerNotFound() { + var sourceContainer = newRecordingId + "-input"; + + when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(false); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.generateAsset(newRecordingId, originalRecordingId) + ).getMessage(); + assertThat(message).isEqualTo("Not found: Source Container (" + sourceContainer + ") does not exist"); + + verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); + verifyNoMoreInteractions(azureIngestStorageService); + } + + @Test + @DisplayName("Should throw not found when generate asset cannot find source container's mp4") + void generateAssetSourceContainerMp4NotFound() { + var sourceContainer = newRecordingId + "-input"; + + when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); + doThrow(new NotFoundException("MP4 file not found in container " + sourceContainer)) + .when(azureIngestStorageService).getMp4FileName(sourceContainer); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.generateAsset(newRecordingId, originalRecordingId) + ).getMessage(); + assertThat(message).isEqualTo("Not found: MP4 file not found in container " + sourceContainer); + + verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); + verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); + verifyNoMoreInteractions(azureIngestStorageService); + } + + @Test + @DisplayName("Should throw error when import asset fails when generating asset") + void generateAssetImportAssetError() throws InterruptedException { + var sourceContainer = newRecordingId + "-input"; + + when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); + doThrow(new NotFoundException("Something went wrong")).when(mediaService) + .importAsset(any(GenerateAssetDTO.class), eq(false)); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.generateAsset(newRecordingId, originalRecordingId) + ).getMessage(); + assertThat(message).isEqualTo("Not found: Something went wrong"); + + verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); + verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); + verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); + verify(azureIngestStorageService, never()).markContainerAsSafeToDelete(sourceContainer); + + verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); + } + + @Test + @DisplayName("Should throw error when import asset fails (returning error) when generating asset") + void generateAssetImportAssetReturnsError() throws InterruptedException { + var sourceContainer = newRecordingId + "-input"; + + when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); + var generateResponse = new GenerateAssetResponseDTO(); + generateResponse.setJobStatus(JobState.ERROR.toString()); + when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(generateResponse); + + var message = assertThrows( + UnknownServerException.class, + () -> underTest.generateAsset(newRecordingId, originalRecordingId) + ).getMessage(); + assertThat(message) + .isEqualTo("Unknown Server Exception: Failed to generate asset for edit request: " + + "source recording: " + originalRecordingId + + ", new recording: " + newRecordingId); + + verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); + verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); + verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); + verify(azureIngestStorageService, never()).markContainerAsSafeToDelete(sourceContainer); + verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); + verify(azureFinalStorageService, never()).getMp4FileName(any()); + } + + @Test + @DisplayName("Should throw error when generating asset if get mp4 from final fails") + void generateAssetGetMp4FinalNotFound() throws InterruptedException { + var sourceContainer = newRecordingId + "-input"; + + when(azureIngestStorageService.doesContainerExist(sourceContainer)).thenReturn(true); + var generateResponse = new GenerateAssetResponseDTO(); + generateResponse.setJobStatus(JobState.FINISHED.toString()); + when(mediaService.importAsset(any(GenerateAssetDTO.class), eq(false))).thenReturn(generateResponse); + doThrow(new NotFoundException("MP4 file not found in container " + newRecordingId)) + .when(azureFinalStorageService) + .getMp4FileName(newRecordingId.toString()); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.generateAsset(newRecordingId, originalRecordingId) + ).getMessage(); + assertThat(message).isEqualTo("Not found: MP4 file not found in container " + newRecordingId); + + verify(azureIngestStorageService, times(1)).doesContainerExist(sourceContainer); + verify(azureIngestStorageService, times(1)).getMp4FileName(sourceContainer); + verify(azureIngestStorageService, times(1)).markContainerAsProcessing(sourceContainer); + verify(azureIngestStorageService, times(1)).markContainerAsSafeToDelete(sourceContainer); + verify(mediaService, times(1)).importAsset(any(GenerateAssetDTO.class), eq(false)); + verify(azureFinalStorageService, times(1)).getMp4FileName(any()); + } +} 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 new file mode 100644 index 0000000000..98473fd911 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestCrudServiceTest.java @@ -0,0 +1,336 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +import org.junit.Before; +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.mockito.MockitoAnnotations; +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.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +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.BadRequestException; +import uk.gov.hmcts.reform.preapi.exception.NotFoundException; +import uk.gov.hmcts.reform.preapi.exception.ResourceInWrongStateException; +import uk.gov.hmcts.reform.preapi.repositories.EditCutInstructionsRepository; +import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +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 java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = EditRequestCrudService.class) +public class EditRequestCrudServiceTest { + + @MockitoBean + private EditRequestRepository editRequestRepository; + + @MockitoBean + private RecordingRepository recordingRepository; + + @MockitoBean + private RecordingService recordingService; + + @MockitoBean + private EditCutInstructionsRepository editCutInstructionsRepository; + + @MockitoBean + private Recording mockRecording; + + @MockitoBean + private EditRequest mockEditRequest; + + @MockitoBean + private EditRequestDTO mockEditRequestDTO; + + @MockitoBean + private User mockUser; + + @Captor + private ArgumentCaptor> instructionsCaptor; + + @Autowired + private EditRequestCrudService underTest; + + private static final UUID mockEditRequestId = UUID.randomUUID(); + private static final UUID mockRecordingId = UUID.randomUUID(); + + private static final EditCutInstructions editInstructions = new EditCutInstructions( + UUID.randomUUID(), 10, 20, "reason"); + + @BeforeEach + void setup() { + when(mockUser.getId()).thenReturn(UUID.randomUUID()); + + // recording + when(mockRecording.getId()).thenReturn(mockRecordingId); + when(mockRecording.getDuration()).thenReturn(Duration.ofMinutes(3)); + when(mockRecording.getFilename()).thenReturn("filename"); + when(mockRecording.getVersion()).thenReturn(1); + when(mockRecording.getEditRequest()).thenReturn(mockEditRequest); + when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)).thenReturn(Optional.of(mockRecording)); + + // edit request + when(mockEditRequest.getId()).thenReturn(mockEditRequestId); + when(mockEditRequest.getCreatedBy()).thenReturn(mockUser); + when(mockEditRequest.getSourceRecordingId()).thenReturn(mockRecordingId); + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.of(mockEditRequest)); + when(editRequestRepository.findByIdNotLocked(mockEditRequestId)).thenReturn(Optional.of(mockEditRequest)); + when(editRequestRepository.findFirstBySourceRecordingIdIs(mockRecordingId)) + .thenReturn(Optional.of(mockEditRequest)); + + + when(mockEditRequest.getEditCutInstructions()).thenReturn(List.of(editInstructions)); + + // dto + when(mockEditRequestDTO.getId()).thenReturn(mockEditRequestId); + when(mockEditRequestDTO.getSourceRecordingId()).thenReturn(mockRecordingId); + + List dtoList = EditRequestDTO.toDTO(List.of(editInstructions)); + when(mockEditRequestDTO.getEditCutInstructions()).thenReturn(dtoList); + }/**/ + + @Test + @DisplayName("Should return edit request when it exists") + void findByIdSuccess() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + + EditRequestDTO res = underTest.findById(mockEditRequestId); + assertThat(res).isNotNull(); + assertThat(res.getId()).isEqualTo(mockEditRequestId); + assertThat(res.getStatus()).isEqualTo(EditRequestStatus.PENDING); + + verify(editRequestRepository, times(1)).findByIdNotLocked(mockEditRequestId); + } + + @Test + @DisplayName("Should throw error when requested request does not exist") + void findByIdNotFound() { + var id = UUID.randomUUID(); + when(editRequestRepository.findByIdNotLocked(id)).thenReturn(Optional.empty()); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.findById(id) + ).getMessage(); + assertThat(message).isEqualTo("Not found: Edit Request: " + id); + + verify(editRequestRepository, times(1)).findByIdNotLocked(id); + } + + @Test + @DisplayName("Should return all pending edit requests") + void getPendingEditRequestsSuccess() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + when(editRequestRepository.findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING)) + .thenReturn(Optional.of(mockEditRequest)); + + Optional res = underTest.getNextPendingEditRequest(); + + assertThat(res).isPresent(); + assertThat(res.get().getId()).isEqualTo(mockEditRequestId); + assertThat(res.get().getStatus()).isEqualTo(EditRequestStatus.PENDING); + + verify(editRequestRepository, times(1)) + .findFirstByStatusIsOrderByCreatedAt(EditRequestStatus.PENDING); + } + + @Test + @DisplayName("Should be able to delete edit request") + void deleteEditRequestSuccess() { + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.of(mockEditRequest)); + + underTest.delete(mockEditRequestDTO); + + verify(editRequestRepository, times(1)).delete(mockEditRequest); + } + + @Test + @DisplayName("Should ignore attempt to delete non-existent edit request") + void deleteNonExistentEditRequestSuccess() { + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.empty()); + + underTest.delete(mockEditRequestDTO); + + verify(editRequestRepository, times(0)).delete(any(EditRequest.class)); + } + + @Test + @DisplayName("Should verify that recording exists before creating edit request") + void verifyThatRecordingExistsBeforeCreatingEditRequestSuccess() { + when(recordingRepository.findByIdAndDeletedAtIsNull(mockRecordingId)).thenReturn(Optional.empty()); + + String message = assertThrows( + NotFoundException.class, + () -> underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser) + ).getMessage(); + + assertThat(message) + .isEqualTo("Not found: Source Recording: " + mockRecordingId); + } + + @Test + @DisplayName("Should verify that recording has non-zero duration before creating edit request") + void verifyThatRecordingHasNonZeroDurationBeforeCreatingEditRequestSuccess() { + when(mockRecording.getDuration()).thenReturn(Duration.ZERO); + + String message = assertThrows( + ResourceInWrongStateException.class, + () -> underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser) + ).getMessage(); + + assertThat(message) + .isEqualTo("Source Recording (" + mockRecordingId + ") does not have a valid duration"); + } + + @Test + @DisplayName("Can create a brand new edit request") + void createNewEditRequestSuccess() { + when(editRequestRepository.findFirstBySourceRecordingIdIs(mockRecordingId)).thenReturn(Optional.empty()); + + underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); + verify(editRequestRepository, times(1)).save(captor.capture()); + + assertThat(captor.getValue().getCreatedBy()).isEqualTo(mockUser); + assertThat(captor.getValue().getStatus()).isEqualTo(EditRequestStatus.DRAFT); + assertThat(captor.getValue().getSourceRecordingId()).isEqualTo(mockRecordingId); + + EditCutInstructions firstInsertedInstruction = captor.getValue().getEditCutInstructions().getFirst(); + EditCutInstructions firstExpectedInstruction = mockEditRequest.getEditCutInstructions().getFirst(); + assertThat(firstInsertedInstruction.getEditRequestId()).isEqualTo(firstExpectedInstruction.getEditRequestId()); + assertThat(firstInsertedInstruction.getReason()).isEqualTo(firstExpectedInstruction.getReason()); + assertThat(firstInsertedInstruction.getEnd()).isEqualTo(firstExpectedInstruction.getEnd()); + assertThat(firstInsertedInstruction.getStart()).isEqualTo(firstExpectedInstruction.getStart()); + } + + @Test + @DisplayName("Inserts a new edit request if previous edit requests are not in draft status") + void insertNewEditRequestIfNoExistingDraftSuccess() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + + underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); + verify(editRequestRepository, times(1)).save(captor.capture()); + + assertThat(captor.getValue().getCreatedBy()).isEqualTo(mockUser); + assertThat(captor.getValue().getStatus()).isEqualTo(EditRequestStatus.DRAFT); + assertThat(captor.getValue().getSourceRecordingId()).isEqualTo(mockRecordingId); + assertThat(captor.getValue().getEditCutInstructions()).isEqualTo(mockEditRequest.getEditCutInstructions()); + } + + @Test + @DisplayName("Upserts existing edit request if a draft exists") + void upsertExistingDraftSuccess() { + underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EditRequest.class); + verify(editRequestRepository, times(1)).save(captor.capture()); + + assertThat(captor.getValue().getCreatedBy()).isEqualTo(mockUser); + assertThat(captor.getValue().getStatus()).isEqualTo(EditRequestStatus.DRAFT); + assertThat(captor.getValue().getSourceRecordingId()).isEqualTo(mockRecordingId); + assertThat(captor.getValue().getEditCutInstructions()).isEqualTo(mockEditRequest.getEditCutInstructions()); + } + + @Test + @DisplayName("Will not create an edit request from a non-original recording") + void willNotCreateEditRequestFromNonOriginalRecordingSuccess() { + when(editRequestRepository.findFirstBySourceRecordingIdIs(mockRecordingId)).thenReturn(Optional.empty()); + when(mockRecording.getVersion()).thenReturn(3); + + UUID parentId = UUID.randomUUID(); + Recording mockParentRecording = mock(Recording.class); + when(mockRecording.getParentRecording()).thenReturn(mockParentRecording); + when(mockParentRecording.getId()).thenReturn(parentId); + + String message = assertThrows( + BadRequestException.class, + () -> underTest.createOrUpsertDraftEditRequestInstructions(mockEditRequestDTO, mockUser) + ).getMessage(); + + assertThat(message).isEqualTo( + "Can only perform edits on original recording (Version 1). " + + "Recording %s is version %d. Perhaps you need parent recording %s?", + mockRecordingId, 3, parentId); + } + + @Test + @DisplayName("Should be able to delete edit instructions for *existing* draft edit request") + void validateEditInstructionsIsEmpty() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(mockEditRequest.getEditCutInstructions()).thenReturn(List.of()); + + underTest.createOrUpsertDraftEditRequestInstructions( + mockEditRequestDTO, + mockUser + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UUID.class); + verify(editCutInstructionsRepository, times(1)) + .refreshInstructionsForDraftEditOnRecording(captor.capture(), instructionsCaptor.capture()); + + assertThat(captor.getValue()).isEqualTo(mockRecordingId); + + List inserted = instructionsCaptor.getValue(); + assertThat(inserted).hasSize(1); + assertThat(inserted.getFirst().getEditRequestId()).isEqualTo(editInstructions.getEditRequestId()); + assertThat(inserted.getFirst().getStart()).isEqualTo(editInstructions.getStart()); + assertThat(inserted.getFirst().getEnd()).isEqualTo(editInstructions.getEnd()); + assertThat(inserted.getFirst().getReason()).isEqualTo(editInstructions.getReason()); + + } + + @Test + @DisplayName("Should be able to update edit instructions for *existing* draft edit request") + void validateEditInstructionsForDraftSuccess() { + when(mockEditRequestDTO.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + when(editRequestRepository.findFirstBySourceRecordingIdIs(mockRecordingId)) + .thenReturn(Optional.of(mockEditRequest)); + + underTest.createOrUpsertDraftEditRequestInstructions( + mockEditRequestDTO, + mockUser + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UUID.class); + verify(editCutInstructionsRepository, times(1)) + .refreshInstructionsForDraftEditOnRecording(captor.capture(), instructionsCaptor.capture()); + + assertThat(captor.getValue()).isEqualTo(mockRecordingId); + + List inserted = instructionsCaptor.getValue(); + assertThat(inserted).hasSize(1); + assertThat(inserted.getFirst().getEditRequestId()).isEqualTo(editInstructions.getEditRequestId()); + assertThat(inserted.getFirst().getStart()).isEqualTo(editInstructions.getStart()); + assertThat(inserted.getFirst().getEnd()).isEqualTo(editInstructions.getEnd()); + assertThat(inserted.getFirst().getReason()).isEqualTo(editInstructions.getReason()); + } + + + + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsvTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsvTest.java new file mode 100644 index 0000000000..c910068f32 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestFromCsvTest.java @@ -0,0 +1,116 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + +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.mock.web.MockMultipartFile; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import uk.gov.hmcts.reform.preapi.controllers.base.PreApiController; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +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 java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = EditRequestFromCsv.class) +public class EditRequestFromCsvTest { + + @MockitoBean + private User mockUser; + + @MockitoBean + private EditRequestCrudService editRequestCrudService; + + @Autowired + private EditRequestFromCsv underTest; + + private static final UUID sourceRecordingId = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(mockUser.getId()).thenReturn(UUID.randomUUID()); + } + + @Test + @DisplayName("Should be able to upsert edit instructions with CSV file") + void upsertEditInstructionsWithCSVFile() { + final String fileContents = """ + Edit Number,Start time of cut,End time of cut,Total time removed,Reason + 1,00:00:00,00:00:30,00:30:00,first thirty seconds reason + 2,00:01:01,00:02:00,00:00:59, + """; + + final List expectedInstructions = Stream.of( + new EditCutInstructions(UUID.randomUUID(), 0, 30, "first thirty seconds reason"), + new EditCutInstructions(UUID.randomUUID(), 61, 120, "") + ).map(EditCutInstructionsDTO::new).toList(); + + final MockMultipartFile file = new MockMultipartFile( + "file", "edit_instructions.csv", + PreApiController.CSV_FILE_TYPE, fileContents.getBytes() + ); + + underTest.upsert(sourceRecordingId, file, mockUser); + + ArgumentCaptor savedEditRequest = ArgumentCaptor.forClass(EditRequestDTO.class); + verify(editRequestCrudService, times(1)) + .createOrUpsertDraftEditRequestInstructions(savedEditRequest.capture(), mockUser); + + assertThat(savedEditRequest.getValue().getId()).isNotNull(); + assertThat(savedEditRequest.getValue().getSourceRecordingId()).isEqualTo(sourceRecordingId); + // TODO: Check what the status should be + assertThat(savedEditRequest.getValue().getStatus()).isEqualTo(EditRequestStatus.PENDING); + assertThat(savedEditRequest.getValue().getCreatedBy()).isEqualTo(mockUser.getId().toString()); + assertThat(savedEditRequest.getValue().getEditCutInstructions()).isEqualTo(expectedInstructions); + } + + @DisplayName("Should throw an exception if updating edit instructions with non-CSV") + @Test + void upsertEditInstructionsWithNotCSVFile() { + final String fileContents = """ +Region,Court,PRE Inbox Address +South East,Example Court,PRE.Edits.Example@justice.gov.uk + """; + + MockMultipartFile file = new MockMultipartFile( + "file", "edits.csv", + "text/xml", fileContents.getBytes() + ); + + assertThrows( + BadRequestException.class, + () -> underTest.upsert(sourceRecordingId, file, mockUser) + ); + } + + @DisplayName("Should throw an exception if updating edit instructions with empty file") + @Test + void upsertEditInstructionsWithEmptyFile() { + final String fileContents = ""; + + MockMultipartFile file = new MockMultipartFile( + "file", "edits.csv", + PreApiController.CSV_FILE_TYPE, fileContents.getBytes() + ); + + assertThrows( + BadRequestException.class, + () -> underTest.upsert(sourceRecordingId, file, mockUser) + ); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingServiceTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingServiceTest.java new file mode 100644 index 0000000000..1993e7ade4 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/preapi/services/edit/EditRequestProcessingServiceTest.java @@ -0,0 +1,501 @@ +package uk.gov.hmcts.reform.preapi.services.edit; + + +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.dao.PessimisticLockingFailureException; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import uk.gov.hmcts.reform.preapi.dto.CreateRecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.RecordingDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditCutInstructionsDTO; +import uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO; +import uk.gov.hmcts.reform.preapi.entities.EditCutInstructions; +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.exception.ResourceInWrongStateException; +import uk.gov.hmcts.reform.preapi.repositories.EditRequestRepository; +import uk.gov.hmcts.reform.preapi.services.EditNotificationService; +import uk.gov.hmcts.reform.preapi.services.RecordingService; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static java.lang.String.format; +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.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static uk.gov.hmcts.reform.preapi.dto.edit.EditRequestDTO.toDTO; + +@SpringBootTest(classes = EditRequestProcessingService.class) +public class EditRequestProcessingServiceTest { + + @MockitoBean + private EditRequestRepository editRequestRepository; + + @MockitoBean + private IEditingService editingService; + + @MockitoBean + private RecordingService recordingService; + + @MockitoBean + private EditNotificationService editNotificationService; + + @MockitoBean + private AssetGenerationService assetGenerationService; + + @MockitoBean + private Recording mockRecording; + + @MockitoBean + private Recording mockParentRecording; + + @MockitoBean + private RecordingDTO mockRecordingDTO; + + @MockitoBean + private EditRequest mockEditRequest; + + @MockitoBean + private EditRequestDTO mockEditRequestDto; + + @Autowired + private EditRequestProcessingService underTest; + + private static final UUID mockEditRequestId = UUID.randomUUID(); + private static final UUID mockRecordingId = UUID.randomUUID(); + private static final UUID mockParentRecordingId = UUID.randomUUID(); + @Autowired + private EditRequestProcessingService editRequestProcessingService; + + @BeforeEach + void setup() { + when(mockRecording.getId()).thenReturn(mockRecordingId); + when(mockRecording.getDuration()).thenReturn(Duration.ofMinutes(3)); + when(mockRecording.getFilename()).thenReturn("filename"); + when(mockRecording.getParentRecording()).thenReturn(mockParentRecording); + when(mockRecording.getEditRequest()).thenReturn(mockEditRequest); + when(mockParentRecording.getId()).thenReturn(mockParentRecordingId); + + when(recordingService.findById(mockRecordingId)).thenReturn(mockRecordingDTO); + when(recordingService.getNextVersionNumber(mockParentRecordingId)).thenReturn(2); + + when(mockEditRequest.getId()).thenReturn(mockEditRequestId); + when(mockEditRequest.getSourceRecordingId()).thenReturn(mockRecordingId); + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 10, 20, "reason")); + when(mockEditRequest.getEditCutInstructions()).thenReturn(instructions); + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.of(mockEditRequest)); + + when(mockEditRequestDto.getId()).thenReturn(mockEditRequestId); + when(mockEditRequestDto.getSourceRecordingId()).thenReturn(mockRecordingId); + when(mockEditRequestDto.getEditCutInstructions()).thenReturn(toDTO(instructions)); + when(mockRecordingDTO.getEditRequest()).thenReturn(mockEditRequestDto); + } + + @Test + @DisplayName("Should update edit request status") + void updateEditRequestProcessing() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + underTest.markAsProcessing(mockEditRequestId); + + verify(editRequestRepository, times(1)).save(mockEditRequest); + verify(editNotificationService, times(1)).editRequestStatusWasUpdated(mockEditRequest); + + verifyNoMoreInteractions(editRequestRepository); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not update edit request status to SUBMITTED if edit instructions are empty") + void shouldNotUpdateEditRequestProcessingIfEditInstructionsAreEmpty() { + when(mockEditRequest.getEditCutInstructions()).thenReturn(List.of()); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.updateEditRequestStatus(mockEditRequest.getId(), EditRequestStatus.SUBMITTED) + ).getMessage(); + + assertThat(message).isEqualTo(format( + "Cannot submit edit request %s: empty instructions", + mockEditRequestId)); + + verifyNoMoreInteractions(editRequestRepository); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw not found error when edit request cannot be found with specified id") + void performEditNotFound() { + when(editRequestRepository.findById(mockEditRequestId)).thenReturn(Optional.empty()); + + var message = assertThrows( + NotFoundException.class, + () -> underTest.markAsProcessing(mockEditRequestId) + ).getMessage(); + + assertThat(message).isEqualTo("Not found: Edit Request: " + mockEditRequestId); + + verify(editRequestRepository, times(1)).findById(mockEditRequestId); + verifyNoMoreInteractions(editRequestRepository); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not perform edit and return null when status of edit request is not PENDING") + void performEditStatusNotPending() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PROCESSING); + + String message = assertThrows( + ResourceInWrongStateException.class, + () -> underTest.markAsProcessing(mockEditRequestId) + ).getMessage(); + + assertThat(message) + .isEqualTo("Resource EditRequest(" + + mockEditRequestId + + ") is in a PROCESSING state. Expected state is PENDING."); + + verify(editRequestRepository, times(1)).findById(mockEditRequestId); + verifyNoMoreInteractions(editRequestRepository); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw lock error when encounters locked edit request") + void performEditLocked() { + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.PENDING); + + doThrow(PessimisticLockingFailureException.class) + .when(editRequestRepository).findById(mockEditRequestId); + + assertThrows( + PessimisticLockingFailureException.class, + () -> underTest.markAsProcessing(mockEditRequestId) + ); + + verify(editRequestRepository, times(1)).findById(mockEditRequestId); + verifyNoMoreInteractions(editRequestRepository); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should prepare for and perform edit") + void prepareForAndPerformEditSuccess() throws InterruptedException { + Integer nextVersionNumber = 4; + when(recordingService.getNextVersionNumber(mockParentRecordingId)).thenReturn(nextVersionNumber); + + try { + underTest.prepareForAndPerformEdit(mockEditRequest); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(recordingService.findById(mockRecordingId)); + verify(editingService, times(1)).performEdit(any(UUID.class), mockRecordingDTO); + verify(recordingService.getNextVersionNumber(mockParentRecordingId)); + verify(assetGenerationService, times(1)).generateAsset(any(UUID.class), mockRecordingId); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateRecordingDTO.class); + verify(recordingService.upsert(captor.capture())); + + CreateRecordingDTO upsertedDto = captor.getValue(); + assertThat(upsertedDto.getVersion()).isEqualTo(nextVersionNumber); + assertThat(upsertedDto.getParentRecordingId()).isEqualTo(mockParentRecordingId); + assertThat(upsertedDto.getFilename()).isEqualTo("TODO"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should prepare for and perform edit with legacy instructions") + void prepareForAndPerformEditWithLegacyInstructions() throws InterruptedException { + Integer nextVersionNumber = 4; + when(recordingService.getNextVersionNumber(mockParentRecordingId)).thenReturn(nextVersionNumber); + when(mockRecording.getDuration()).thenReturn(Duration.ofMinutes(10)); + + when(mockRecording.getEditRequest()).thenReturn(null); + when(mockRecording.getEditInstruction()).thenReturn(""" + { + "requestedInstructions": [ + { + "start_of_cut": "00:05:00", + "end_of_cut": "00:08:00", + "reason": "Removing 3 minutes", + "start": 300, + "end": 480 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 300 + }, + { + "start": 480, + "end": 907 + } + ] + } + """); + + try { + underTest.prepareForAndPerformEdit(mockEditRequest); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(recordingService.findById(mockRecordingId)); + verify(editingService, times(1)).performEdit(any(UUID.class), mockRecordingDTO); + verify(recordingService.getNextVersionNumber(mockParentRecordingId)); + verify(assetGenerationService, times(1)).generateAsset(any(UUID.class), mockRecordingId); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateRecordingDTO.class); + verify(recordingService.upsert(captor.capture())); + + CreateRecordingDTO upsertedDto = captor.getValue(); + assertThat(upsertedDto.getVersion()).isEqualTo(nextVersionNumber); + assertThat(upsertedDto.getParentRecordingId()).isEqualTo(mockParentRecordingId); + assertThat(upsertedDto.getFilename()).isEqualTo("TODO"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not perform edit for non-existent recording") + void validateNonExistentRecording() { + when(recordingService.findById(mockRecordingId)).thenReturn(null); + + assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ); + + verify(recordingService.findById(mockRecordingId)); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not perform edit for non-existent edit instructions") + void prepareForAndPerformEdit() { + when(mockRecording.getEditRequest()).thenReturn(null); + when(mockRecording.getEditInstruction()).thenReturn(null); + + assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ); + + verify(recordingService, times(1)).findById(mockRecordingId); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not perform edit for non-existent edit instructions") + void shouldNotPerformEditForNonExistentInstructions() { + when(mockRecording.getEditRequest()).thenReturn(null); + when(mockRecording.getEditInstruction()).thenReturn(null); + + assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ); + + verify(recordingService.findById(mockRecordingId)); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should not perform edit when instruction cuts entire recording") + void validateInstructionsBadRequestCutToZeroDuration() { + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 0, 180, "reason")); + when(mockEditRequest.getEditCutInstructions()).thenReturn(instructions); + + when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(180)); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Invalid Instruction: Cannot cut an entire recording: " + + "Start(00:00:00), End(00:03:00), " + + "Recording Duration(00:03:00)"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw bad request when instruction has same value for start and end") + void validateInstructionsBadRequestStartEndEqual() { + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 60, 60, "reason")); + when(mockEditRequest.getEditCutInstructions()).thenReturn(instructions); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Invalid instruction: Instruction with 0 second duration invalid: " + + "Start(00:01:00), End(00:01:00)"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw bad request when instruction end time is less than start time") + void validateInstructionsBadRequestEndLTStart() { + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 60, 50, "reason")); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Invalid instruction: Instruction with end time before start time: " + + "Start(00:01:00), End(00:00:50)"); + + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + + @Test + @DisplayName("Should throw bad request when instruction end time exceeds duration") + void validateInstructionsBadRequestEndTimeExceedsDuration() { + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), + 60, + 200, // duration is 180 + "reason")); + when(mockEditRequest.getEditCutInstructions()).thenReturn(instructions); + when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(180)); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Invalid instruction: Instruction end time exceeding duration: " + + "Start(00:01:00), End(00:03:20), " + + "Recording Duration(00:03:00)"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + + @Test + @DisplayName("Should throw bad request when instructions overlap") + void validateInstructionsOverlap() { + List instructions = new ArrayList<>(); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 10, 30, "first edit")); + instructions.add(new EditCutInstructions(UUID.randomUUID(), 20, 40, "overlapping")); + when(mockEditRequest.getEditCutInstructions()).thenReturn(instructions); + when(mockRecording.getDuration()).thenReturn(Duration.ofSeconds(180)); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message).isEqualTo("Overlapping instructions: " + + "Previous End(00:00:30), Current Start(00:00:20)"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw error when source recording does not have a duration") + void validateZeroRecordingDuration() { + when(mockRecording.getDuration()).thenReturn(null); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Source Recording (" + mockRecordingId + ") does not have a valid duration"); + + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editRequestProcessingService); + verifyNoMoreInteractions(assetGenerationService); + } + + @Test + @DisplayName("Should throw bad request when trying to perform edit with empty instructions") + void validateEmptyInstructions() { + when(mockEditRequest.getEditCutInstructions()).thenReturn(List.of()); + when(mockEditRequest.getStatus()).thenReturn(EditRequestStatus.DRAFT); + + var message = assertThrows( + BadRequestException.class, + () -> underTest.prepareForAndPerformEdit(mockEditRequest) + ).getMessage(); + + assertThat(message) + .isEqualTo("Invalid Instruction: Cannot create an edit request with empty instructions"); + + verifyNoMoreInteractions(recordingService); + verifyNoMoreInteractions(editNotificationService); + verifyNoMoreInteractions(assetGenerationService); + } + + + +} diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/tasks/BaseCleanupNullRecordingDurationTest.java b/src/test/java/uk/gov/hmcts/reform/preapi/tasks/BaseCleanupNullRecordingDurationTest.java index 2cf281b407..bca950e17e 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/tasks/BaseCleanupNullRecordingDurationTest.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/tasks/BaseCleanupNullRecordingDurationTest.java @@ -7,7 +7,7 @@ import uk.gov.hmcts.reform.preapi.dto.AccessDTO; import uk.gov.hmcts.reform.preapi.dto.base.BaseAppAccessDTO; import uk.gov.hmcts.reform.preapi.dto.base.BaseUserDTO; -import uk.gov.hmcts.reform.preapi.media.edit.FfmpegService; +import uk.gov.hmcts.reform.preapi.services.edit.FfmpegService; import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService; import uk.gov.hmcts.reform.preapi.security.authentication.UserAuthentication; import uk.gov.hmcts.reform.preapi.security.service.UserAuthenticationService; diff --git a/src/test/java/uk/gov/hmcts/reform/preapi/util/HelperFactory.java b/src/test/java/uk/gov/hmcts/reform/preapi/util/HelperFactory.java index 9e3335b783..5cb748d5b9 100644 --- a/src/test/java/uk/gov/hmcts/reform/preapi/util/HelperFactory.java +++ b/src/test/java/uk/gov/hmcts/reform/preapi/util/HelperFactory.java @@ -11,6 +11,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.EditCutInstructions; import uk.gov.hmcts.reform.preapi.entities.EditRequest; import uk.gov.hmcts.reform.preapi.entities.Participant; import uk.gov.hmcts.reform.preapi.entities.PortalAccess; @@ -30,11 +31,12 @@ import uk.gov.hmcts.reform.preapi.enums.RecordingStatus; import uk.gov.hmcts.reform.preapi.enums.TermsAndConditionsType; +import javax.annotation.Nullable; import java.sql.Timestamp; +import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; -import javax.annotation.Nullable; @UtilityClass @SuppressWarnings({"checkstyle:HideUtilityClassConstructor", "PMD.CouplingBetweenObjects"}) @@ -69,6 +71,15 @@ public static Court createCourt(CourtType courtType, String name, @Nullable Stri return court; } + public static Court createCourt(CourtType courtType, String name, @Nullable String locationCode, String email) { + Court court = new Court(); + court.setCourtType(courtType); + court.setName(name); + court.setLocationCode(locationCode); + court.setGroupEmail(email); + return court; + } + public static Role createRole(String name) { Role role = new Role(); role.setName(name); @@ -228,9 +239,23 @@ public static UserTermsAccepted createUserTermsAccepted(User user, return termsAccepted; } + public static EditRequest createSimpleEditRequest(UUID id, + UUID sourceRecordingId, + List editInstructions, + EditRequestStatus status, + User createdBy){ + EditRequest editRequest = new EditRequest(); + editRequest.setId(id); + editRequest.setSourceRecordingId(sourceRecordingId); + editRequest.setEditCutInstructions(editInstructions); + editRequest.setStatus(status); + editRequest.setCreatedBy(createdBy); + return editRequest; + } + public static EditRequest createEditRequest(UUID id, - Recording sourceRecording, - String editInstructions, + UUID sourceRecordingId, + List editInstructions, EditRequestStatus status, User createdBy, @Nullable Timestamp startedAt, @@ -239,12 +264,7 @@ public static EditRequest createEditRequest(UUID id, @Nullable String rejectionReason, @Nullable Timestamp approvedAt, @Nullable String approvedBy) { - var editRequest = new EditRequest(); - editRequest.setId(id); - editRequest.setSourceRecording(sourceRecording); - editRequest.setEditInstruction(editInstructions); - editRequest.setStatus(status); - editRequest.setCreatedBy(createdBy); + EditRequest editRequest = createSimpleEditRequest(id, sourceRecordingId, editInstructions, status, createdBy); editRequest.setStartedAt(startedAt); editRequest.setFinishedAt(finishedAt); editRequest.setJointlyAgreed(jointlyAgreed); diff --git a/src/test/resources/edit-requests/existing/sample-edited-recording-testno2.json b/src/test/resources/edit-requests/existing/sample-edited-recording-testno2.json new file mode 100644 index 0000000000..e7c7c37342 --- /dev/null +++ b/src/test/resources/edit-requests/existing/sample-edited-recording-testno2.json @@ -0,0 +1,46 @@ +{ + "id": "b817685b-0155-4946-b09e-6bd369ef0381", + "parent_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 4, + "total_version_count": 4, + "edit_instructions": "{\"editRequestId\": \"f264f9cb-0203-4fa6-9234-b6efec06819e\", \"editInstructions\": {\"ffmpegInstructions\": [{\"end\": 180, \"start\": 0}, {\"end\": 733, \"start\": 300}], \"requestedInstructions\": [{\"end\": 300, \"start\": 180, \"reason\": \"Removing 2 minutes\", \"end_of_cut\": \"00:05:00\", \"start_of_cut\": \"00:03:00\"}]}}", + "edit_requests": [], + "filename": "b817685b-0155-4946-b09e-6bd369ef_1280x720_4500k.mp4", + "duration": "PT10M15.2S", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "created_at": "2026-02-24T10:42:40.059Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/existing/sample-for-edit-request-test.json b/src/test/resources/edit-requests/existing/sample-for-edit-request-test.json new file mode 100644 index 0000000000..7ec408fbfe --- /dev/null +++ b/src/test/resources/edit-requests/existing/sample-for-edit-request-test.json @@ -0,0 +1,31 @@ +{ + "editRequestId": "f264f9cb-0203-4fa6-9234-b6efec06819e", + "editInstructions": { + "ffmpegInstructions": [ + { + "end": 180, + "start": 0 + }, + { + "end": 733, + "start": 300 + } + ], + "requestedInstructions": [ + { + "end": 300, + "start": 180, + "reason": "Removing 2 minutes", + "end_of_cut": "00:05:00", + "start_of_cut": "00:03:00" + }, + { + "end": 490, + "start": 480, + "reason": "Removing 10 seconds", + "end_of_cut": "00:08:10", + "start_of_cut": "00:08:00" + } + ] + } +} diff --git a/src/test/resources/edit-requests/existing/sample-recording-2-edits-not-displayed.json b/src/test/resources/edit-requests/existing/sample-recording-2-edits-not-displayed.json new file mode 100644 index 0000000000..d9af211ef3 --- /dev/null +++ b/src/test/resources/edit-requests/existing/sample-recording-2-edits-not-displayed.json @@ -0,0 +1,46 @@ +{ + "id": "ed360496-e35f-4c71-9f05-e6c4ce843370", + "parent_recording_id": "8c73cf3e-efc5-485b-91b0-7ba62f3985b5", + "version": 7, + "total_version_count": 7, + "edit_instructions": "{\"ffmpegInstructions\": [{\"end\": 600, \"start\": 0}, {\"end\": 2400, \"start\": 1200}, {\"end\": 3955, \"start\": 2700}], \"requestedInstructions\": [{\"end\": 1200, \"start\": 600, \"reason\": \"Removing 10 minutes from version 1\", \"end_of_cut\": \"00:20:00\", \"start_of_cut\": \"00:10:00\"}, {\"end\": 2700, \"start\": 2400, \"reason\": \"Also removing 5 minutes from version 1\", \"end_of_cut\": \"00:45:00\", \"start_of_cut\": \"00:40:00\"}]}", + "edit_requests": [], + "filename": "ed360496-e35f-4c71-9f05-e6c4ce84_1280x720_4500k.mp4", + "duration": "PT50M55.7S", + "capture_session": { + "id": "bb5ec935-5bb6-43ba-8ec5-241aba1d1b52", + "booking_id": "15cfbbed-53bd-41b5-ab57-0962ab919f18", + "origin": "PRE", + "ingest_address": "rtmps://in-890f8537-c5d4-4b81-bb69-31fa36e21e26.uksouth.streaming.mediakind.com:2935/eca9b219-03c4-4e70-b768-c513122b859a", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/bb5ec9355bb643ba8ec5241aba1d1b52/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-05-27T08:55:58.931Z", + "started_by_user_id": "f8750aa0-ecf3-ec11-bb3c-0022481b1f95", + "finished_at": "2025-05-27T10:54:16.279Z", + "finished_by_user_id": "f8750aa0-ecf3-ec11-bb3c-0022481b1f95", + "status": "RECORDING_AVAILABLE", + "court_name": "Leeds Youth", + "case_state": "OPEN" + }, + "created_at": "2025-08-26T11:09:49.447Z", + "case_id": "eb0d9ede-6c96-4819-8d70-cdfc630e7c1f", + "case_reference": "STG-AUTOEDIT2", + "is_test_case": false, + "participants": [ + { + "id": "c5f269be-0d57-408d-9fde-51e0f221138e", + "participant_type": "DEFENDANT", + "first_name": "Test", + "last_name": "Test2", + "created_at": "2025-05-27T08:53:32.855Z", + "modified_at": "2025-05-27T08:53:32.831Z" + }, + { + "id": "f442ca19-da72-4d23-a239-a27d6bb9b4c8", + "participant_type": "WITNESS", + "first_name": "Test2", + "last_name": "", + "created_at": "2025-05-27T08:53:32.851Z", + "modified_at": "2025-05-27T08:53:32.825Z" + } + ] +} diff --git a/src/test/resources/edit-requests/existing/sample-recording-parent-stgautoedit2.json b/src/test/resources/edit-requests/existing/sample-recording-parent-stgautoedit2.json new file mode 100644 index 0000000000..10f3f62867 --- /dev/null +++ b/src/test/resources/edit-requests/existing/sample-recording-parent-stgautoedit2.json @@ -0,0 +1,230 @@ +{ + "id": "8c73cf3e-efc5-485b-91b0-7ba62f3985b5", + "version": 1, + "total_version_count": 7, + "edit_requests": [ + { + "id": "31de179a-f35e-41a9-a5c1-b406f824f13b", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:00:00", + "end_of_cut": "00:40:00", + "reason": "Remove first 40 minutes", + "start": 0, + "end": 2400 + } + ], + "ffmpegInstructions": [ + { + "start": 2400, + "end": 3955 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-05-29T17:29:56.611Z", + "finished_at": "2025-05-29T17:37:12.680Z", + "created_by_id": "7a36422a-6c2e-418d-87de-d914b9accf2a", + "created_by": "Cristiane Marsilio", + "created_at": "2025-05-29T17:01:35.039Z", + "modified_at": "2025-05-29T17:37:12.700Z" + }, + { + "id": "a8a92cdf-11d8-4eee-8ab8-e4b604db6200", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:15:00", + "end_of_cut": "00:20:00", + "reason": "Remove alarm test-version5", + "start": 900, + "end": 1200 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 900 + }, + { + "start": 1200, + "end": 3955 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-05-28T11:36:31.362Z", + "finished_at": "2025-05-28T11:50:29.397Z", + "created_by_id": "7a36422a-6c2e-418d-87de-d914b9accf2a", + "created_by": "Cristiane Marsilio", + "created_at": "2025-05-28T11:24:13.277Z", + "modified_at": "2025-05-28T11:50:29.424Z" + }, + { + "id": "f30d9e9d-787d-4878-aca8-9ca0ed7149a3", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:15:00", + "end_of_cut": "00:20:00", + "reason": "Remove alarm test-version4", + "start": 900, + "end": 1200 + }, + { + "start_of_cut": "00:40:00", + "end_of_cut": "01:00:00", + "reason": "Also removing this-version4", + "start": 2400, + "end": 3600 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 900 + }, + { + "start": 1200, + "end": 2400 + }, + { + "start": 3600, + "end": 3955 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-05-28T11:25:37.250Z", + "finished_at": "2025-05-28T11:35:49.990Z", + "created_by_id": "7a36422a-6c2e-418d-87de-d914b9accf2a", + "created_by": "Cristiane Marsilio", + "created_at": "2025-05-28T11:21:44.131Z", + "modified_at": "2025-05-28T11:35:50.018Z" + }, + { + "id": "606cbba7-483d-4cac-9112-5f9fba4eaacf", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:15:00", + "end_of_cut": "00:20:00", + "reason": "Remove alarm test-version3", + "start": 900, + "end": 1200 + }, + { + "start_of_cut": "00:40:00", + "end_of_cut": "00:50:00", + "reason": "Also removing this-version3", + "start": 2400, + "end": 3000 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 900 + }, + { + "start": 1200, + "end": 2400 + }, + { + "start": 3000, + "end": 3955 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-05-27T16:55:38.158Z", + "finished_at": "2025-05-27T17:07:57.423Z", + "created_by_id": "7a36422a-6c2e-418d-87de-d914b9accf2a", + "created_by": "Cristiane Marsilio", + "created_at": "2025-05-27T16:52:54.984Z", + "modified_at": "2025-05-27T17:07:57.446Z" + }, + { + "id": "02801650-d08a-41a5-88f8-da9586f6b28d", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:02:00", + "end_of_cut": "00:12:00", + "reason": "Remove alarm test", + "start": 120, + "end": 720 + }, + { + "start_of_cut": "00:30:00", + "end_of_cut": "00:50:00", + "reason": "Also removing this", + "start": 1800, + "end": 3000 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 120 + }, + { + "start": 720, + "end": 1800 + }, + { + "start": 3000, + "end": 3955 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-05-27T14:30:39.858Z", + "finished_at": "2025-05-27T14:39:55.425Z", + "created_by_id": "7a36422a-6c2e-418d-87de-d914b9accf2a", + "created_by": "Cristiane Marsilio", + "created_at": "2025-05-27T14:27:39.208Z", + "modified_at": "2025-05-27T14:39:55.456Z" + } + ], + "edit_status": "COMPLETE", + "capture_session": { + "id": "bb5ec935-5bb6-43ba-8ec5-241aba1d1b52", + "booking_id": "15cfbbed-53bd-41b5-ab57-0962ab919f18", + "origin": "PRE", + "ingest_address": "rtmps://in-890f8537-c5d4-4b81-bb69-31fa36e21e26.uksouth.streaming.mediakind.com:2935/eca9b219-03c4-4e70-b768-c513122b859a", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/bb5ec9355bb643ba8ec5241aba1d1b52/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-05-27T08:55:58.931Z", + "started_by_user_id": "f8750aa0-ecf3-ec11-bb3c-0022481b1f95", + "finished_at": "2025-05-27T10:54:16.279Z", + "finished_by_user_id": "f8750aa0-ecf3-ec11-bb3c-0022481b1f95", + "status": "RECORDING_AVAILABLE", + "court_name": "Leeds Youth", + "case_state": "OPEN" + }, + "filename": "bb5ec9355bb643ba8ec5241aba1d1b52_1280x720_4500k.mp4", + "duration": "PT1H5M55.9S", + "created_at": "2025-05-27T10:59:41.027Z", + "case_id": "eb0d9ede-6c96-4819-8d70-cdfc630e7c1f", + "case_reference": "STG-AUTOEDIT2", + "is_test_case": false, + "participants": [ + { + "id": "c5f269be-0d57-408d-9fde-51e0f221138e", + "participant_type": "DEFENDANT", + "first_name": "Test", + "last_name": "Test2", + "created_at": "2025-05-27T08:53:32.855Z", + "modified_at": "2025-05-27T08:53:32.831Z" + }, + { + "id": "f442ca19-da72-4d23-a239-a27d6bb9b4c8", + "participant_type": "WITNESS", + "first_name": "Test2", + "last_name": "", + "created_at": "2025-05-27T08:53:32.851Z", + "modified_at": "2025-05-27T08:53:32.825Z" + } + ] +} diff --git a/src/test/resources/edit-requests/existing/sample-recording-parent-testno2.json b/src/test/resources/edit-requests/existing/sample-recording-parent-testno2.json new file mode 100644 index 0000000000..d91f50245a --- /dev/null +++ b/src/test/resources/edit-requests/existing/sample-recording-parent-testno2.json @@ -0,0 +1,225 @@ +{ + "id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 1, + "total_version_count": 4, + "edit_requests": [ + { + "id": "d3a1f270-9037-4782-8e73-462e2e0b7340", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:00:01", + "end_of_cut": "00:00:02", + "reason": "test", + "start": 1, + "end": 2 + }, + { + "start_of_cut": "00:03:00", + "end_of_cut": "00:05:00", + "reason": "Removing 2 minutes", + "start": 180, + "end": 300 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 1 + }, + { + "start": 2, + "end": 180 + }, + { + "start": 300, + "end": 733 + } + ] + }, + "status": "DRAFT", + "created_by_id": "c56e379e-88cc-417c-801a-8647b82825c9", + "created_by": "Lydia Ralph (DEV)", + "created_at": "2026-02-27T14:25:31.228Z", + "modified_at": "2026-02-27T14:25:31.223Z" + }, + { + "id": "91746e31-5d54-4b94-9b5e-61df2637c108", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:00:01", + "end_of_cut": "00:00:02", + "reason": "test", + "start": 1, + "end": 2 + }, + { + "start_of_cut": "00:03:00", + "end_of_cut": "00:05:00", + "reason": "Removing 2 minutes", + "start": 180, + "end": 300 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 1 + }, + { + "start": 2, + "end": 180 + }, + { + "start": 300, + "end": 733 + } + ] + }, + "status": "DRAFT", + "created_by_id": "c56e379e-88cc-417c-801a-8647b82825c9", + "created_by": "Lydia Ralph (DEV)", + "created_at": "2026-02-27T14:25:13.784Z", + "modified_at": "2026-02-27T14:25:13.773Z" + }, + { + "id": "f264f9cb-0203-4fa6-9234-b6efec06819e", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:03:00", + "end_of_cut": "00:05:00", + "reason": "Removing 2 minutes", + "start": 180, + "end": 300 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 180 + }, + { + "start": 300, + "end": 733 + } + ] + }, + "status": "COMPLETE", + "started_at": "2026-02-24T10:40:36.066Z", + "finished_at": "2026-02-24T10:42:39.851Z", + "created_by_id": "8da77965-acec-4256-b695-509924a11f06", + "created_by": "Komal Dabhi (DEV)", + "created_at": "2026-02-24T10:39:01.665Z", + "modified_at": "2026-02-24T10:42:40.050Z" + }, + { + "id": "aa5e4e2f-0169-48af-a084-5f4cb00d2ec9", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:05:00", + "end_of_cut": "00:07:59", + "reason": "Removing almost 3 minutes", + "start": 300, + "end": 479 + } + ], + "ffmpegInstructions": [ + { + "start": 0, + "end": 300 + }, + { + "start": 479, + "end": 733 + } + ] + }, + "status": "COMPLETE", + "started_at": "2025-12-15T13:30:37.154Z", + "finished_at": "2025-12-15T13:32:22.546Z", + "created_by_id": "8da77965-acec-4256-b695-509924a11f06", + "created_by": "Komal Dabhi (DEV)", + "created_at": "2025-12-15T13:27:36.640Z", + "modified_at": "2025-12-18T12:05:52.098Z" + }, + { + "id": "0d71a6de-7db0-4e04-8611-f288d7e26a7d", + "edit_instruction": { + "requestedInstructions": [ + { + "start_of_cut": "00:00:00", + "end_of_cut": "00:01:00", + "reason": "I don’t want this", + "start": 0, + "end": 60 + }, + { + "start_of_cut": "00:01:01", + "end_of_cut": "00:02:00", + "reason": "", + "start": 61, + "end": 120 + } + ], + "ffmpegInstructions": [ + { + "start": 60, + "end": 61 + }, + { + "start": 120, + "end": 733 + } + ] + }, + "status": "PROCESSING", + "started_at": "2025-12-15T10:11:22.530Z", + "created_by_id": "c56e379e-88cc-417c-801a-8647b82825c9", + "created_by": "Lydia Ralph (DEV)", + "created_at": "2025-12-15T10:09:20.579Z", + "modified_at": "2025-12-15T10:11:22.544Z" + } + ], + "edit_status": "DRAFT", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "filename": "2a6d504d2c724f18aad4d08505caa304_1280x720_4500k.mp4", + "duration": "PT12M13.5S", + "created_at": "2025-06-26T10:23:23.276Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/existing/schema-edited-recording.json b/src/test/resources/edit-requests/existing/schema-edited-recording.json new file mode 100644 index 0000000000..14c408626e --- /dev/null +++ b/src/test/resources/edit-requests/existing/schema-edited-recording.json @@ -0,0 +1,182 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Recording", + "definitions": { + "Recording": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "parent_recording_id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer" + }, + "total_version_count": { + "type": "integer" + }, + "edit_instructions": { + "type": "string" + }, + "edit_requests": { + "type": "array", + "items": {} + }, + "filename": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "capture_session": { + "$ref": "#/definitions/CaptureSession" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "case_id": { + "type": "string", + "format": "uuid" + }, + "case_reference": { + "type": "string" + }, + "is_test_case": { + "type": "boolean" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/Participant" + } + } + }, + "required": [ + "capture_session", + "case_id", + "case_reference", + "created_at", + "duration", + "edit_instructions", + "edit_requests", + "filename", + "id", + "is_test_case", + "parent_recording_id", + "participants", + "total_version_count", + "version" + ], + "title": "Recording" + }, + "CaptureSession": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "booking_id": { + "type": "string", + "format": "uuid" + }, + "origin": { + "type": "string" + }, + "ingest_address": { + "type": "string" + }, + "live_output_url": { + "type": "string", + "format": "uri", + "qt-uri-protocols": [ + "https" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "started_by_user_id": { + "type": "string", + "format": "uuid" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "finished_by_user_id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string" + }, + "court_name": { + "type": "string" + }, + "case_state": { + "type": "string" + } + }, + "required": [ + "booking_id", + "case_state", + "court_name", + "finished_at", + "finished_by_user_id", + "id", + "ingest_address", + "live_output_url", + "origin", + "started_at", + "started_by_user_id", + "status" + ], + "title": "CaptureSession" + }, + "Participant": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "participant_type": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "modified_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created_at", + "first_name", + "id", + "last_name", + "modified_at", + "participant_type" + ], + "title": "Participant" + } + } +} diff --git a/src/test/resources/edit-requests/existing/schema-parent-recording.json b/src/test/resources/edit-requests/existing/schema-parent-recording.json new file mode 100644 index 0000000000..4b2f075823 --- /dev/null +++ b/src/test/resources/edit-requests/existing/schema-parent-recording.json @@ -0,0 +1,299 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Recording", + "definitions": { + "Recording": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer" + }, + "total_version_count": { + "type": "integer" + }, + "edit_status": { + "type": "string" + }, + "edit_requests": { + "type": "array", + "items": { + "$ref": "#/definitions/EditRequest" + } + }, + "capture_session": { + "$ref": "#/definitions/CaptureSession" + }, + "filename": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "case_id": { + "type": "string", + "format": "uuid" + }, + "case_reference": { + "type": "string" + }, + "is_test_case": { + "type": "boolean" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/Participant" + } + } + }, + "required": [ + "capture_session", + "case_id", + "case_reference", + "created_at", + "duration", + "edit_requests", + "edit_status", + "filename", + "id", + "is_test_case", + "participants", + "total_version_count", + "version" + ], + "title": "Recording" + }, + "EditRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "edit_instruction": { + "$ref": "#/definitions/EditInstruction" + }, + "status": { + "type": "string" + }, + "created_by_id": { + "type": "string", + "format": "uuid" + }, + "created_by": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "modified_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "finished_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created_at", + "created_by", + "created_by_id", + "edit_instruction", + "id", + "modified_at", + "status" + ], + "title": "EditRequest" + }, + "EditInstruction": { + "type": "object", + "additionalProperties": false, + "properties": { + "ffmpegInstructions": { + "type": "array", + "items": { + "$ref": "#/definitions/FfmpegInstruction" + } + }, + "requestedInstructions": { + "type": "array", + "items": { + "$ref": "#/definitions/RequestedInstruction" + } + } + }, + "required": [ + "ffmpegInstructions", + "requestedInstructions" + ], + "title": "EditInstruction" + }, + "FfmpegInstruction": { + "type": "object", + "additionalProperties": false, + "properties": { + "start": { + "type": "integer" + }, + "end": { + "type": "integer" + } + }, + "required": [ + "end", + "start" + ], + "title": "FfmpegInstruction" + }, + "RequestedInstruction": { + "type": "object", + "additionalProperties": false, + "properties": { + "start_of_cut": { + "type": "string", + "format": "time" + }, + "end_of_cut": { + "type": "string", + "format": "time" + }, + "reason": { + "type": "string" + }, + "start": { + "type": "integer" + }, + "end": { + "type": "integer" + } + }, + "required": [ + "end", + "end_of_cut", + "reason", + "start", + "start_of_cut" + ], + "title": "RequestedInstruction" + }, + "CaptureSession": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "booking_id": { + "type": "string", + "format": "uuid" + }, + "origin": { + "type": "string" + }, + "ingest_address": { + "type": "string" + }, + "live_output_url": { + "type": "string", + "format": "uri", + "qt-uri-protocols": [ + "https" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "started_by_user_id": { + "type": "string", + "format": "uuid" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "finished_by_user_id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string" + }, + "court_name": { + "type": "string" + }, + "case_state": { + "type": "string" + } + }, + "required": [ + "booking_id", + "case_state", + "court_name", + "finished_at", + "finished_by_user_id", + "id", + "ingest_address", + "live_output_url", + "origin", + "started_at", + "started_by_user_id", + "status" + ], + "title": "CaptureSession" + }, + "Participant": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "participant_type": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "modified_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created_at", + "first_name", + "id", + "last_name", + "modified_at", + "participant_type" + ], + "title": "Participant" + } + } +} diff --git a/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v1.json b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v1.json new file mode 100644 index 0000000000..3a0db259a0 --- /dev/null +++ b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v1.json @@ -0,0 +1,46 @@ +{ + "id": "b817685b-0155-4946-b09e-6bd369ef0381", + "parent_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 1, + "total_version_count": 4, + "edit_request_status": "ORIGINAL", + "edit_request": null, + "filename": "b817685b-0155-4946-b09e-6bd369ef_1280x720_4500k.mp4", + "duration": "PT10M15.2S", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "created_at": "2026-02-24T10:42:40.059Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v2.json b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v2.json new file mode 100644 index 0000000000..c2a6249bd4 --- /dev/null +++ b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v2.json @@ -0,0 +1,58 @@ +{ + "id": "b817685b-0155-4946-b09e-6bd369ef0382", + "parent_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 2, + "total_version_count": 4, + "edit_request_status": "OUTDATED", + "edit_request": { + "edit_request_id": "f264f9cb-0203-4fa6-9234-b6efec06819e", + "edit_request_status": "OUTDATED", + "source_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "output_recording_id": "b817685b-0155-4946-b09e-6bd369ef0382", + "edit_instructions": [ + { + "reason": "First edit", + "start_of_cut": "00:01:00", + "end_of_cut": "00:02:00" + } + ] + }, + "filename": "b817685b-0155-4946-b09e-6bd369ef_1280x720_4500k.mp4", + "duration": "PT10M15.2S", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "created_at": "2026-02-24T10:42:40.059Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v3.json b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v3.json new file mode 100644 index 0000000000..e8502d54ec --- /dev/null +++ b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v3.json @@ -0,0 +1,63 @@ +{ + "id": "b817685b-0155-4946-b09e-6bd369ef0383", + "parent_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 3, + "total_version_count": 4, + "edit_request_status": "COMPLETE", + "edit_request": { + "edit_request_id": "f264f9cb-0203-4fa6-9234-b6efec06819e", + "edit_request_status": "COMPLETE", + "source_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "output_recording_id": "b817685b-0155-4946-b09e-6bd369ef0383", + "edit_instructions": [ + { + "reason": "First edit", + "start_of_cut": "00:01:00", + "end_of_cut": "00:02:00" + }, + { + "reason": "Second edit", + "start_of_cut": "00:03:00", + "end_of_cut": "00:05:00" + } + ] + }, + "filename": "b817685b-0155-4946-b09e-6bd369ef_1280x720_4500k.mp4", + "duration": "PT10M15.2S", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "created_at": "2026-02-24T10:42:40.059Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v4.json b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v4.json new file mode 100644 index 0000000000..aeabeb5373 --- /dev/null +++ b/src/test/resources/edit-requests/proposed-schema/sample-edited-recording-v4.json @@ -0,0 +1,68 @@ +{ + "id": "b817685b-0155-4946-b09e-6bd369ef0384", + "parent_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "version": 4, + "total_version_count": 4, + "edit_request_status": "DRAFT", + "edit_request": { + "edit_request_id": "f264f9cb-0203-4fa6-9234-b6efec06819e", + "edit_request_status": "DRAFT", + "source_recording_id": "e0912d46-cded-4757-9367-2d8b3f81693f", + "output_recording_id": "b817685b-0155-4946-b09e-6bd369ef0384", + "edit_instructions": [ + { + "reason": "First edit", + "start_of_cut": "00:01:00", + "end_of_cut": "00:02:00" + }, + { + "reason": "Second edit", + "start_of_cut": "00:03:00", + "end_of_cut": "00:05:00" + }, + { + "reason": "Third edit", + "start_of_cut": "00:01:30", + "end_of_cut": "00:01:45" + } + ] + }, + "filename": "b817685b-0155-4946-b09e-6bd369ef_1280x720_4500k.mp4", + "duration": "PT10M15.2S", + "capture_session": { + "id": "2a6d504d-2c72-4f18-aad4-d08505caa304", + "booking_id": "9e823ccb-cb20-43ba-a430-56dbc6190b7c", + "origin": "PRE", + "ingest_address": "rtmps://in-2c40ef17-caa6-4caf-8268-775ab8c49e3d.uksouth.streaming.mediakind.com:2935/1ccdd12f-7114-4315-9b1e-243bd721cba2", + "live_output_url": "https://ep-default-live-pre-mediakind-stg.uksouth.streaming.mediakind.com/2a6d504d2c724f18aad4d08505caa304/index.qfm/manifest(format=m3u8-cmaf)", + "started_at": "2025-06-26T10:01:37.688Z", + "started_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "finished_at": "2025-06-26T10:20:34.489Z", + "finished_by_user_id": "8da77965-acec-4256-b695-509924a11f06", + "status": "RECORDING_AVAILABLE", + "court_name": "102 Petty France", + "case_state": "OPEN" + }, + "created_at": "2026-02-24T10:42:40.059Z", + "case_id": "368bc646-32eb-4e3a-9af3-aeac32f86630", + "case_reference": "TEST-NO-2", + "is_test_case": false, + "participants": [ + { + "id": "d863d7b5-63e4-4fad-b80c-6fe1848191c4", + "participant_type": "DEFENDANT", + "first_name": "Komal", + "last_name": "Dabhi", + "created_at": "2025-06-26T10:00:17.576Z", + "modified_at": "2025-06-26T10:00:17.556Z" + }, + { + "id": "eb518f93-9db5-40eb-a538-ac95df9a5b9a", + "participant_type": "WITNESS", + "first_name": "Simon", + "last_name": "", + "created_at": "2025-06-26T10:00:17.567Z", + "modified_at": "2025-06-26T10:00:17.549Z" + } + ] +} diff --git a/src/test/resources/edit-requests/proposed-schema/schema-recording-all-versions.json b/src/test/resources/edit-requests/proposed-schema/schema-recording-all-versions.json new file mode 100644 index 0000000000..eda6e8df6d --- /dev/null +++ b/src/test/resources/edit-requests/proposed-schema/schema-recording-all-versions.json @@ -0,0 +1,265 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Recording", + "definitions": { + "Recording": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "parent_recording_id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer" + }, + "total_version_count": { + "type": "integer" + }, + "edit_request_status": { + "type": "string" + }, + "edit_request": { + "type": "array", + "items": { + "$ref": "#/definitions/EditRequest" + } + }, + "capture_session": { + "$ref": "#/definitions/CaptureSession" + }, + "filename": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "case_id": { + "type": "string", + "format": "uuid" + }, + "case_reference": { + "type": "string" + }, + "is_test_case": { + "type": "boolean" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/Participant" + } + } + }, + "required": [ + "capture_session", + "case_id", + "case_reference", + "created_at", + "duration", + "edit_request_status", + "filename", + "id", + "parent_recording_id", + "participants", + "total_version_count", + "version" + ], + "title": "Recording" + }, + "EditRequest": { + "type": "object", + "additionalProperties": false, + "title": "EditRequest", + "properties": { + "edit_request_id": { + "type": "string", + "format": "uuid" + }, + "source_recording_id": { + "type": "string", + "format": "uuid" + }, + "output_recording_id": { + "type": "string", + "format": "uuid" + }, + "edit_request_status": { + "type": "string" + }, + "edit_instructions": { + "type": "array", + "items": { + "$ref": "#/definitions/EditCutInstructions" + } + }, + "started_at": { + "type": "string" + }, + "finished_at": { + "type": "string" + }, + "approved_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "approved_by": { + "type": "string" + }, + "jointly_agreed": { + "type": "string" + }, + "rejection_reason": { + "type": "string" + } + }, + "required": [ + "edit_instructions", + "source_recording_id", + "edit_request_id", + "edit_request_status", + "started_at", + "finished_at", + "approved_at", + "created_by", + "approved_by", + "jointly_agreed", + "rejection_reason" + ] + }, + "EditCutInstructions": { + "type": "object", + "additionalProperties": false, + "properties": { + "start_of_cut": { + "type": "string" + }, + "end_of_cut": { + "type": "string" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "start_of_cut", + "end_of_cut", + "reason" + ], + "title": "EditCutInstruction" + }, + "CaptureSession": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "booking_id": { + "type": "string", + "format": "uuid" + }, + "origin": { + "type": "string" + }, + "ingest_address": { + "type": "string" + }, + "live_output_url": { + "type": "string", + "format": "uri", + "qt-uri-protocols": [ + "https" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "started_by_user_id": { + "type": "string", + "format": "uuid" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "finished_by_user_id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string" + }, + "court_name": { + "type": "string" + }, + "case_state": { + "type": "string" + } + }, + "required": [ + "booking_id", + "case_state", + "court_name", + "finished_at", + "finished_by_user_id", + "id", + "ingest_address", + "live_output_url", + "origin", + "started_at", + "started_by_user_id", + "status" + ], + "title": "CaptureSession" + }, + "Participant": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "participant_type": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "modified_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created_at", + "first_name", + "id", + "last_name", + "modified_at", + "participant_type" + ], + "title": "Participant" + } + } +}