From feef6fd5bb019c5e0597f582761ec1c8bc3256f1 Mon Sep 17 00:00:00 2001 From: karen-hedges <133129444+karen-hedges@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:51:24 +0000 Subject: [PATCH 1/4] DMP-5445 Media request expiry state needs to be saved Added code to save the media request once set to expired and also fixed an issue with deleting media from hearing --- .../service/CaseExpiryDeleterIntTest.java | 264 ++++++++++++++++++ .../CloseOldCasesProcessorIntTest.java | 82 +++--- .../repository/MediaRepositoryIntTest.java | 13 +- .../HearingsGetEventsControllerTest.java | 10 +- ...aseExpiryDeletionAutomatedTaskIntTest.java | 2 - .../darts/testutils/stubs/EventStub.java | 4 +- .../darts/common/entity/MediaEntity.java | 2 +- .../impl/DataAnonymisationServiceImpl.java | 8 +- .../impl/CaseExpiryDeleterImplTest.java | 97 ++++++- .../service/impl/CaseServiceImplTest.java | 54 ++-- .../darts/common/entity/MediaEntityTest.java | 81 ++++++ .../DataAnonymisationServiceImplTest.java | 37 +-- .../expectedResponseWithCourtroom.json | 0 .../expectedResponseWithoutCourtroom.json | 0 .../expectedResponse.json | 0 .../expectedResponse.json | 0 .../expectedResponse.json | 0 .../expectedResponse.json | 0 18 files changed, 541 insertions(+), 113 deletions(-) create mode 100644 src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CaseExpiryDeleterIntTest.java rename src/test/resources/Tests/cases/CaseServiceTest/{testAddCase => AddCase}/expectedResponseWithCourtroom.json (100%) rename src/test/resources/Tests/cases/CaseServiceTest/{testAddCase => AddCase}/expectedResponseWithoutCourtroom.json (100%) rename src/test/resources/Tests/cases/CaseServiceTest/{updateCase => AddCaseOrUpdate}/expectedResponse.json (100%) rename src/test/resources/Tests/cases/CaseServiceTest/{testGetCasesById => GetCasesById}/expectedResponse.json (100%) rename src/test/resources/Tests/cases/CaseServiceTest/{testGetCasesWithMultipleHearing => GetCasesWithMultipleHearing}/expectedResponse.json (100%) rename src/test/resources/Tests/cases/CaseServiceTest/{testGetCasesWithSingleHearingAndDifferentCourtroom => GetCasesWithSingleHearingAndDifferentCourtroom}/expectedResponse.json (100%) diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CaseExpiryDeleterIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CaseExpiryDeleterIntTest.java new file mode 100644 index 00000000000..852b2143bcb --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CaseExpiryDeleterIntTest.java @@ -0,0 +1,264 @@ +package uk.gov.hmcts.darts.cases.service; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import uk.gov.hmcts.darts.common.entity.CourtCaseEntity; +import uk.gov.hmcts.darts.common.entity.EventEntity; +import uk.gov.hmcts.darts.common.entity.HearingEntity; +import uk.gov.hmcts.darts.common.entity.MediaEntity; +import uk.gov.hmcts.darts.common.repository.CaseRepository; +import uk.gov.hmcts.darts.retention.enums.CaseRetentionStatus; +import uk.gov.hmcts.darts.retention.enums.RetentionConfidenceCategoryEnum; +import uk.gov.hmcts.darts.retention.enums.RetentionConfidenceReasonEnum; +import uk.gov.hmcts.darts.retention.enums.RetentionConfidenceScoreEnum; +import uk.gov.hmcts.darts.test.common.data.PersistableFactory; +import uk.gov.hmcts.darts.test.common.data.RetentionConfidenceCategoryMapperTestData; +import uk.gov.hmcts.darts.test.common.data.builder.TestRetentionConfidenceCategoryMapperEntity; +import uk.gov.hmcts.darts.testutils.IntegrationBase; +import uk.gov.hmcts.darts.testutils.stubs.DartsPersistence; + +import java.time.Clock; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +@SpringBootTest(properties = "spring.main.allow-bean-definition-overriding=true") +class CaseExpiryDeleterIntTest extends IntegrationBase { + + private static final Integer BATCH_SIZE = 5; + private static final String REQUESTER_EMAIL = "test.user@example.com"; + private static final OffsetDateTime CURRENT_DATE_TIME = OffsetDateTime.of(2025, 10, 1, 10, 0, 0, 0, ZoneOffset.UTC); + private static final String COURTHOUSE_TO_BE_EXPIRED = "a_courthouse"; + private static final String COURTHOUSE_NOT_TO_BE_EXPIRED = "b_courthouse"; + private static final String CASE_NUMBER_TO_BE_EXPIRED = "019278"; + private static final String CASE_NUMBER_NOT_TO_BE_EXPIRED = "019279"; + private static final String COURTROOM_NAME = "1"; + private static final String TEST_EVENT_NAME_EXPIRED = "testEventName-expired"; + private static final String TEST_EVENT_NAME_NON_EXPIRED = "testEventName-non-expired"; + + @Autowired + private CaseExpiryDeleter caseExpiryDeleter; + + @Autowired + private DartsPersistence dartsPersistence; + + @Autowired + private CaseRepository caseRepository; + + private CourtCaseEntity caseToBeExpired; + private CourtCaseEntity caseNotToBeExpired; + private EventEntity caseToBeExpiredEvent1; + private EventEntity caseToBeExpiredEvent2; + private EventEntity caseToBeExpiredEvent3; + private EventEntity caseNotToBeExpiredEvent1; + private EventEntity caseNotToBeExpiredEvent2; + private EventEntity caseNotToBeExpiredEvent3; + + @TestConfiguration + static class ClockConfig { + @Bean + public Clock clock() { + return Clock.fixed(CURRENT_DATE_TIME.toInstant(), ZoneOffset.UTC); + } + } + + @BeforeEach + void beforeEach() { + Jwt jwt = Jwt.withTokenValue("test") + .header("alg", "RS256") + .claim("sub", UUID.randomUUID().toString()) + .claim("emails", List.of(REQUESTER_EMAIL)) + .build(); + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + dartsDatabase.createTestUserAccount(); + + createAndSaveRetentionConfidenceCategoryMappings(); + + OffsetDateTime expiredCaseDateTime = CURRENT_DATE_TIME.minusYears(9); + HearingEntity caseToBeExpiredHearing = dartsDatabase.createHearing(COURTHOUSE_TO_BE_EXPIRED, COURTROOM_NAME, CASE_NUMBER_TO_BE_EXPIRED, + expiredCaseDateTime.toLocalDateTime()); + dartsDatabase.getHearingRepository().saveAndFlush(caseToBeExpiredHearing); + + caseToBeExpiredEvent1 = createEvent(caseToBeExpiredHearing, 8, expiredCaseDateTime.minusMinutes(10), TEST_EVENT_NAME_EXPIRED, false); + caseToBeExpiredEvent2 = createEvent(caseToBeExpiredHearing, 214, expiredCaseDateTime, TEST_EVENT_NAME_EXPIRED, false); + caseToBeExpiredEvent3 = createEvent(caseToBeExpiredHearing, 23, expiredCaseDateTime.plusMinutes(10), TEST_EVENT_NAME_EXPIRED, false); + dartsDatabase.saveAll(caseToBeExpiredEvent1, caseToBeExpiredEvent2, caseToBeExpiredEvent3); + assertEquals(TEST_EVENT_NAME_EXPIRED, caseToBeExpiredEvent1.getEventText()); + + caseToBeExpired = caseToBeExpiredHearing.getCourtCase(); + caseToBeExpired.setCreatedDateTime(OffsetDateTime.now().minusYears(10)); + caseToBeExpired.setClosed(true); + caseToBeExpired.setCaseClosedTimestamp(OffsetDateTime.now().minusYears(7)); + caseToBeExpired.setDataAnonymised(false); + dartsDatabase.save(caseToBeExpired); + + // Link events to the case so anonymisation can locate them via event_linked_case. + dartsDatabase.createEventLinkedCase(caseToBeExpiredEvent1, caseToBeExpired); + dartsDatabase.createEventLinkedCase(caseToBeExpiredEvent2, caseToBeExpired); + dartsDatabase.createEventLinkedCase(caseToBeExpiredEvent3, caseToBeExpired); + + OffsetDateTime nonExpiredCaseDateTime = CURRENT_DATE_TIME.minusYears(1); + HearingEntity caseNotToBeExpiredHearing = dartsDatabase.createHearing(COURTHOUSE_NOT_TO_BE_EXPIRED, COURTROOM_NAME, CASE_NUMBER_NOT_TO_BE_EXPIRED, + nonExpiredCaseDateTime.toLocalDateTime()); + dartsDatabase.getHearingRepository().saveAndFlush(caseNotToBeExpiredHearing); + + caseNotToBeExpiredEvent1 = createEvent(caseNotToBeExpiredHearing, 8, nonExpiredCaseDateTime.minusMinutes(10), TEST_EVENT_NAME_NON_EXPIRED, null); + caseNotToBeExpiredEvent2 = createEvent(caseNotToBeExpiredHearing, 214, nonExpiredCaseDateTime, TEST_EVENT_NAME_NON_EXPIRED, null); + caseNotToBeExpiredEvent3 = createEvent(caseNotToBeExpiredHearing, 23, nonExpiredCaseDateTime.plusMinutes(10), TEST_EVENT_NAME_NON_EXPIRED, null); + dartsDatabase.saveAll(caseNotToBeExpiredEvent1, caseNotToBeExpiredEvent2, caseNotToBeExpiredEvent3); + + caseNotToBeExpired = caseNotToBeExpiredHearing.getCourtCase(); + caseNotToBeExpired.setCreatedDateTime(nonExpiredCaseDateTime); + caseNotToBeExpired.setClosed(true); + caseNotToBeExpired.setCaseClosedTimestamp(OffsetDateTime.now()); + caseNotToBeExpired.setDataAnonymised(false); + dartsDatabase.save(caseNotToBeExpired); + + // Link events to the case so we can assert non-expired events stay untouched. + dartsDatabase.createEventLinkedCase(caseNotToBeExpiredEvent1, caseNotToBeExpired); + dartsDatabase.createEventLinkedCase(caseNotToBeExpiredEvent2, caseNotToBeExpired); + dartsDatabase.createEventLinkedCase(caseNotToBeExpiredEvent3, caseNotToBeExpired); + + MediaEntity caseToBeExpiredMedia = PersistableFactory.getMediaTestData().someMinimalBuilderHolder() + .getBuilder() + .courtroom(caseToBeExpiredHearing.getCourtroom()) + .build() + .getEntity(); + MediaEntity caseNotToBeExpiredMedia = PersistableFactory.getMediaTestData().someMinimalBuilderHolder() + .getBuilder() + .courtroom(caseNotToBeExpiredHearing.getCourtroom()) + .build() + .getEntity(); + + caseToBeExpiredHearing.addMedia(caseToBeExpiredMedia); + caseNotToBeExpiredHearing.addMedia(caseNotToBeExpiredMedia); + dartsDatabase.save(caseToBeExpiredMedia); + dartsDatabase.save(caseNotToBeExpiredMedia); + dartsDatabase.save(caseToBeExpiredHearing); + dartsDatabase.save(caseNotToBeExpiredHearing); + + // Ensure media_linked_case exists; otherwise areAllAssociatedCasesAnonymised(media) returns null in H2 + // (no rows / no group), which triggers an AOP invocation exception and marks the transaction rollback-only. + dartsDatabase.createMediaLinkedCase(caseToBeExpiredMedia, caseToBeExpired); + dartsDatabase.createMediaLinkedCase(caseNotToBeExpiredMedia, caseNotToBeExpired); + + dartsDatabase.getCaseRetentionStub().createCaseRetentionObject(caseToBeExpired, CaseRetentionStatus.COMPLETE, + CURRENT_DATE_TIME.minusYears(1), false); + dartsDatabase.getCaseRetentionStub().createCaseRetentionObject(caseNotToBeExpired, CaseRetentionStatus.PENDING, + OffsetDateTime.now(), false); + + } + + private EventEntity createEvent(HearingEntity hearing, int eventTypeId, OffsetDateTime createdDateTime, String eventText, + Boolean dataAnonymised) { + EventEntity event = dartsDatabase.getEventStub().createEvent(hearing, eventTypeId); + event.setCreatedDateTime(createdDateTime); + event.setEventText(eventText); + if (dataAnonymised != null) { + event.setDataAnonymised(dataAnonymised); + } + return event; + } + + @Test + void delete_shouldAnonymiseExpiredCasesOnly() { + + // When + transactionalUtil.executeInTransaction(() -> { + caseExpiryDeleter.delete(BATCH_SIZE); + + CourtCaseEntity expiredCaseInTransaction = caseRepository.findById(caseToBeExpired.getId()).orElseThrow(); + CourtCaseEntity nonExpiredCaseInTransaction = caseRepository.findById(caseNotToBeExpired.getId()).orElseThrow(); + + assertThat(expiredCaseInTransaction.isDataAnonymised()).isTrue(); + assertThat(nonExpiredCaseInTransaction.isDataAnonymised()).isFalse(); + + EventEntity expiredEvent1 = dartsDatabase.getEventRepository().findById(caseToBeExpiredEvent1.getId()).orElseThrow(); + assertTrue(expiredEvent1.isDataAnonymised()); + assertNotEquals(TEST_EVENT_NAME_EXPIRED, expiredEvent1.getEventText()); + + EventEntity expiredEvent2 = dartsDatabase.getEventRepository().findById(caseToBeExpiredEvent2.getId()).orElseThrow(); + assertTrue(expiredEvent2.isDataAnonymised()); + assertNotEquals(TEST_EVENT_NAME_EXPIRED, expiredEvent2.getEventText()); + + EventEntity expiredEvent3 = dartsDatabase.getEventRepository().findById(caseToBeExpiredEvent3.getId()).orElseThrow(); + assertTrue(expiredEvent3.isDataAnonymised()); + assertNotEquals(TEST_EVENT_NAME_EXPIRED, expiredEvent3.getEventText()); + + EventEntity nonExpiredEvent1 = dartsDatabase.getEventRepository().findById(caseNotToBeExpiredEvent1.getId()).orElseThrow(); + assertFalse(nonExpiredEvent1.isDataAnonymised()); + assertEquals(TEST_EVENT_NAME_NON_EXPIRED, nonExpiredEvent1.getEventText()); + + EventEntity nonExpiredEvent2 = dartsDatabase.getEventRepository().findById(caseNotToBeExpiredEvent2.getId()).orElseThrow(); + assertFalse(nonExpiredEvent2.isDataAnonymised()); + assertEquals(TEST_EVENT_NAME_NON_EXPIRED, nonExpiredEvent2.getEventText()); + + EventEntity nonExpiredEvent3 = dartsDatabase.getEventRepository().findById(caseNotToBeExpiredEvent3.getId()).orElseThrow(); + assertFalse(nonExpiredEvent3.isDataAnonymised()); + assertEquals(TEST_EVENT_NAME_NON_EXPIRED, nonExpiredEvent3.getEventText()); + + }); + } + + private void createRetentionConfidenceCategoryMapperEntity(RetentionConfidenceCategoryEnum retentionConfidenceCategoryEnum, + RetentionConfidenceReasonEnum retentionConfidenceReasonEnum, + RetentionConfidenceScoreEnum retentionConfidenceScoreEnum) { + + RetentionConfidenceCategoryMapperTestData testData = PersistableFactory.getRetentionConfidenceCategoryMapperTestData(); + TestRetentionConfidenceCategoryMapperEntity agedCaseMappingEntity = testData.someMinimalBuilder() + .confidenceCategory(retentionConfidenceCategoryEnum.getId()) + .confidenceReason(retentionConfidenceReasonEnum) + .confidenceScore(retentionConfidenceScoreEnum) + .build(); + dartsPersistence.save(agedCaseMappingEntity.getEntity()); + } + + private void createAndSaveRetentionConfidenceCategoryMappings() { + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CLOSED, + RetentionConfidenceReasonEnum.AGED_CASE, + RetentionConfidenceScoreEnum.CASE_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_EVENT_CLOSED, + RetentionConfidenceReasonEnum.MAX_EVENT_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_MEDIA_CLOSED, + RetentionConfidenceReasonEnum.MAX_MEDIA_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_HEARING_CLOSED, + RetentionConfidenceReasonEnum.MAX_HEARING_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CREATION_CLOSED, + RetentionConfidenceReasonEnum.CASE_CREATION_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.CASE_CLOSED, + RetentionConfidenceReasonEnum.CASE_CLOSED, + RetentionConfidenceScoreEnum.CASE_PERFECTLY_CLOSED + ); + } +} diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CloseOldCasesProcessorIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CloseOldCasesProcessorIntTest.java index f3c44f08368..0fcdcea39c6 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CloseOldCasesProcessorIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/cases/service/CloseOldCasesProcessorIntTest.java @@ -78,47 +78,6 @@ void beforeEach() { dartsDatabase.createTestUserAccount(); } - private void createAndSaveRetentionConfidenceCategoryMappings() { - createRetentionConfidenceCategoryMapperEntity( - RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CLOSED, - RetentionConfidenceReasonEnum.AGED_CASE, - RetentionConfidenceScoreEnum.CASE_PERFECTLY_CLOSED - ); - createRetentionConfidenceCategoryMapperEntity( - RetentionConfidenceCategoryEnum.AGED_CASE_MAX_EVENT_CLOSED, - RetentionConfidenceReasonEnum.MAX_EVENT_CLOSED, - RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED - ); - createRetentionConfidenceCategoryMapperEntity( - RetentionConfidenceCategoryEnum.AGED_CASE_MAX_MEDIA_CLOSED, - RetentionConfidenceReasonEnum.MAX_MEDIA_CLOSED, - RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED - ); - createRetentionConfidenceCategoryMapperEntity( - RetentionConfidenceCategoryEnum.AGED_CASE_MAX_HEARING_CLOSED, - RetentionConfidenceReasonEnum.MAX_HEARING_CLOSED, - RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED - ); - createRetentionConfidenceCategoryMapperEntity( - RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CREATION_CLOSED, - RetentionConfidenceReasonEnum.CASE_CREATION_CLOSED, - RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED - ); - } - - private void createRetentionConfidenceCategoryMapperEntity(RetentionConfidenceCategoryEnum retentionConfidenceCategoryEnum, - RetentionConfidenceReasonEnum retentionConfidenceReasonEnum, - RetentionConfidenceScoreEnum retentionConfidenceScoreEnum) { - - RetentionConfidenceCategoryMapperTestData testData = PersistableFactory.getRetentionConfidenceCategoryMapperTestData(); - TestRetentionConfidenceCategoryMapperEntity agedCaseMappingEntity = testData.someMinimalBuilder() - .confidenceCategory(retentionConfidenceCategoryEnum.getId()) - .confidenceReason(retentionConfidenceReasonEnum) - .confidenceScore(retentionConfidenceScoreEnum) - .build(); - dartsPersistence.save(agedCaseMappingEntity.getEntity()); - } - @Test void givenClosedEventsUseDateAsClosedDate() { createAndSaveRetentionConfidenceCategoryMappings(); @@ -705,5 +664,46 @@ void givenNotSixYearsOldDoNotClose() { assertNull(updatedCourtCaseEntity.getRetConfScore()); assertNull(updatedCourtCaseEntity.getRetConfReason()); } + + private void createAndSaveRetentionConfidenceCategoryMappings() { + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CLOSED, + RetentionConfidenceReasonEnum.AGED_CASE, + RetentionConfidenceScoreEnum.CASE_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_EVENT_CLOSED, + RetentionConfidenceReasonEnum.MAX_EVENT_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_MEDIA_CLOSED, + RetentionConfidenceReasonEnum.MAX_MEDIA_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_MAX_HEARING_CLOSED, + RetentionConfidenceReasonEnum.MAX_HEARING_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + createRetentionConfidenceCategoryMapperEntity( + RetentionConfidenceCategoryEnum.AGED_CASE_CASE_CREATION_CLOSED, + RetentionConfidenceReasonEnum.CASE_CREATION_CLOSED, + RetentionConfidenceScoreEnum.CASE_NOT_PERFECTLY_CLOSED + ); + } + + private void createRetentionConfidenceCategoryMapperEntity(RetentionConfidenceCategoryEnum retentionConfidenceCategoryEnum, + RetentionConfidenceReasonEnum retentionConfidenceReasonEnum, + RetentionConfidenceScoreEnum retentionConfidenceScoreEnum) { + + RetentionConfidenceCategoryMapperTestData testData = PersistableFactory.getRetentionConfidenceCategoryMapperTestData(); + TestRetentionConfidenceCategoryMapperEntity agedCaseMappingEntity = testData.someMinimalBuilder() + .confidenceCategory(retentionConfidenceCategoryEnum.getId()) + .confidenceReason(retentionConfidenceReasonEnum) + .confidenceScore(retentionConfidenceScoreEnum) + .build(); + dartsPersistence.save(agedCaseMappingEntity.getEntity()); + } } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/common/repository/MediaRepositoryIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/common/repository/MediaRepositoryIntTest.java index 68abb29b1d4..fac26cc834f 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/common/repository/MediaRepositoryIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/common/repository/MediaRepositoryIntTest.java @@ -30,22 +30,19 @@ class MediaRepositoryIntTest extends PostgresIntegrationBase { - @Autowired - HearingRepository hearingRepository; + private static final int GENERATION_COUNT = 20; @Autowired - MediaRepository mediaRepository; - + private HearingRepository hearingRepository; @Autowired - MediaLinkedCaseRepository mediaLinkedCaseRepository; - + private MediaRepository mediaRepository; + @Autowired + private MediaLinkedCaseRepository mediaLinkedCaseRepository; @Autowired private MediaStub mediaStub; private List generatedMediaEntities; - private static final int GENERATION_COUNT = 20; - @BeforeEach public void before() { generatedMediaEntities = mediaStub.generateMediaEntities(GENERATION_COUNT); diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/hearings/controller/HearingsGetEventsControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/hearings/controller/HearingsGetEventsControllerTest.java index 7fcb9df6c64..25746a0c7d0 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/hearings/controller/HearingsGetEventsControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/hearings/controller/HearingsGetEventsControllerTest.java @@ -31,6 +31,8 @@ @Slf4j class HearingsGetEventsControllerTest extends IntegrationBase { + private static final String EVENT_ID = "<>"; + @Autowired private transient MockMvc mockMvc; @@ -75,11 +77,11 @@ void okGet() throws Exception { "id":<>, "timestamp":"2020-06-20T10:00:00Z", "name":"Defendant recalled", - "text":"testEventText", + "text":"testEventName", "is_data_anonymised": false }] """; - expectedJson = expectedJson.replace("<>", event.getId().toString()); + expectedJson = expectedJson.replace(EVENT_ID, event.getId().toString()); JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.NON_EXTENSIBLE); } @@ -104,14 +106,14 @@ void getEvents_shouldBeOrderedByTimestamp() throws Exception { "id": 2, "timestamp": "2020-06-20T10:01:00Z", "name": "Defendant recalled", - "text": "testEventText", + "text": "testEventName", "is_data_anonymised": false }, { "id": 1, "timestamp": "2020-06-20T10:00:00Z", "name": "Defendant recalled", - "text": "testEventText", + "text": "testEventName", "is_data_anonymised": false } ] diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/task/runner/impl/CaseExpiryDeletionAutomatedTaskIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/task/runner/impl/CaseExpiryDeletionAutomatedTaskIntTest.java index 22739e8129c..db047defa9a 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/task/runner/impl/CaseExpiryDeletionAutomatedTaskIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/task/runner/impl/CaseExpiryDeletionAutomatedTaskIntTest.java @@ -146,7 +146,6 @@ void positiveMultipleToAnonymiseAndSomeNotTo() { final CourtCaseEntity courtCase4 = createCaseWithRetentionRecords(List.of(new RetentionRecordData(-1, CaseRetentionStatus.PENDING))); final CourtCaseEntity courtCase5 = createCaseWithRetentionRecords(List.of(new RetentionRecordData(1, CaseRetentionStatus.COMPLETE))); - caseExpiryDeletionAutomatedTask.preRunTask(); caseExpiryDeletionAutomatedTask.runTask(); @@ -154,7 +153,6 @@ void positiveMultipleToAnonymiseAndSomeNotTo() { assertCase(courtCase2.getId(), true); assertCase(courtCase3.getId(), true); - assertCase(courtCase4.getId(), false); assertCase(courtCase5.getId(), false); } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/EventStub.java b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/EventStub.java index 77a4449dcf9..374c9509545 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/EventStub.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/EventStub.java @@ -64,7 +64,7 @@ public EventEntity createEvent(HearingEntity hearing, int eventHandlerId, Offset @Transactional public EventEntity createEvent(HearingEntity hearing, int eventHandlerId, OffsetDateTime eventTimestamp, String eventName, Integer eventId) { EventEntity eventEntity = new EventEntity(); - eventEntity.setEventText("testEventText"); + eventEntity.setEventText(eventName); EventHandlerEntity eventHandlerEntity = eventHandlerRepository.findById(eventHandlerId).get(); eventEntity.setEventType(eventHandlerEntity); eventEntity.setTimestamp(eventTimestamp); @@ -90,7 +90,7 @@ public EventEntity createEvent(CourtroomEntity courtroom, int eventHandlerId, Of @Transactional public EventEntity createEvent(CourtroomEntity courtroom, int eventHandlerId, OffsetDateTime eventTimestamp, String eventName, Integer eventId) { EventEntity eventEntity = new EventEntity(); - eventEntity.setEventText("testEventText"); + eventEntity.setEventText(eventName); EventHandlerEntity eventHandlerEntity = eventHandlerRepository.findById(eventHandlerId).get(); eventEntity.setEventType(eventHandlerEntity); eventEntity.setTimestamp(eventTimestamp); diff --git a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java index 555f6944277..337dacbe9bd 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java +++ b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java @@ -160,7 +160,7 @@ public List associatedCourtCases() { public void removeHearing(HearingEntity hearing) { hearing.getMedias().remove(this); - getHearings().remove(this); + getHearings().remove(hearing); } public void addHearing(HearingEntity hearing) { diff --git a/src/main/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImpl.java b/src/main/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImpl.java index 6f58110aea8..3de821cbbd3 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImpl.java @@ -28,6 +28,7 @@ import uk.gov.hmcts.darts.common.entity.base.CreatedModifiedBaseEntity; import uk.gov.hmcts.darts.common.helper.CurrentTimeHelper; import uk.gov.hmcts.darts.common.repository.DataAnonymisationRepository; +import uk.gov.hmcts.darts.common.repository.MediaRequestRepository; import uk.gov.hmcts.darts.common.repository.TransformedMediaRepository; import uk.gov.hmcts.darts.common.repository.TransientObjectDirectoryRepository; import uk.gov.hmcts.darts.common.service.DataAnonymisationService; @@ -63,7 +64,7 @@ public class DataAnonymisationServiceImpl implements DataAnonymisationService { private final CaseService caseService; private final EventService eventService; private final DataAnonymisationRepository dataAnonymisationRepository; - + private final MediaRequestRepository mediaRequestRepository; @Override @Transactional @@ -111,9 +112,7 @@ public void anonymiseEventByIds(UserAccountEntity userAccount, List eveIds eveIds.stream() .map(eventService::getEventByEveId) .distinct() - .forEach(eventEntity -> { - anonymiseEvent(userAccount, eventEntity, isManuallyRequested); - }); + .forEach(eventEntity -> anonymiseEvent(userAccount, eventEntity, isManuallyRequested)); } @Override @@ -203,6 +202,7 @@ boolean deleteTransientObjectDirectoryEntity(TransientObjectDirectoryEntity tran void expiredMediaRequest(UserAccountEntity userAccount, MediaRequestEntity mediaRequestEntity) { mediaRequestEntity.setStatus(MediaRequestStatus.EXPIRED); anonymiseCreatedModifiedBaseEntity(userAccount, mediaRequestEntity); + mediaRequestRepository.save(mediaRequestEntity); } void registerDataAnonymisation(UserAccountEntity userAccount, EventEntity eventEntity, boolean isManualRequest) { diff --git a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseExpiryDeleterImplTest.java b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseExpiryDeleterImplTest.java index fdc72e2add0..c6ce295e979 100644 --- a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseExpiryDeleterImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseExpiryDeleterImplTest.java @@ -55,7 +55,7 @@ void setUp() { } @Test - void delete_should() { + void delete_shouldAnonymiseData() { Duration duration = Duration.ofHours(24); when(config.getBufferDuration()).thenReturn(duration); UserAccountEntity userAccount = mock(UserAccountEntity.class); @@ -79,4 +79,97 @@ void delete_should() { verify(caseRepository).findCaseIdsToBeAnonymised(offsetDateTime.minus(duration), Limit.of(5)); verify(userIdentity).getUserAccount(); } -} \ No newline at end of file + + @Test + void delete_shouldDoNothingWhenNoCasesToAnonymise() { + Duration duration = Duration.ofHours(48); + when(config.getBufferDuration()).thenReturn(duration); + UserAccountEntity userAccount = mock(UserAccountEntity.class); + when(userIdentity.getUserAccount()).thenReturn(userAccount); + OffsetDateTime now = OffsetDateTime.now(); + when(currentTimeHelper.currentOffsetDateTime()).thenReturn(now); + + when(caseRepository.findCaseIdsToBeAnonymised(any(), any())) + .thenReturn(List.of()); + + caseExpiryDeleter.delete(10); + + verify(currentTimeHelper, times(1)).currentOffsetDateTime(); + verify(caseRepository).findCaseIdsToBeAnonymised(now.minus(duration), Limit.of(10)); + // No anonymisation or hearing updates when list is empty + verify(dataAnonymisationService, times(0)).anonymiseCourtCaseById(any(UserAccountEntity.class), any(Integer.class), any(Boolean.class)); + verify(hearingsService, times(0)).removeMediaLinkToHearing(any()); + } + + @Test + void delete_shouldContinueProcessingWhenAnonymisationThrowsException() { + Duration duration = Duration.ofDays(1); + when(config.getBufferDuration()).thenReturn(duration); + UserAccountEntity userAccount = mock(UserAccountEntity.class); + when(userIdentity.getUserAccount()).thenReturn(userAccount); + OffsetDateTime now = OffsetDateTime.now(); + when(currentTimeHelper.currentOffsetDateTime()).thenReturn(now); + + when(caseRepository.findCaseIdsToBeAnonymised(any(), any())) + .thenReturn(List.of(10, 20, 30)); + + // First case throws, others succeed + RuntimeException anonymiseFailure = new RuntimeException("failure on first case"); + org.mockito.Mockito.doThrow(anonymiseFailure) + .when(dataAnonymisationService).anonymiseCourtCaseById(userAccount, 10, false); + + caseExpiryDeleter.delete(3); + + // First case anonymisation attempted but fails, so no media unlink for that case + verify(dataAnonymisationService).anonymiseCourtCaseById(userAccount, 10, false); + // Other cases should still be processed fully + verify(dataAnonymisationService).anonymiseCourtCaseById(userAccount, 20, false); + verify(dataAnonymisationService).anonymiseCourtCaseById(userAccount, 30, false); + + // removeMediaLinkToHearing should be invoked only for successful anonymisations (20 and 30) + verify(hearingsService, times(0)).removeMediaLinkToHearing(10); + verify(hearingsService).removeMediaLinkToHearing(20); + verify(hearingsService).removeMediaLinkToHearing(30); + + verify(caseRepository).findCaseIdsToBeAnonymised(now.minus(duration), Limit.of(3)); + } + + @Test + void delete_shouldUseProvidedBatchSizeInLimit() { + Duration duration = Duration.ofHours(6); + when(config.getBufferDuration()).thenReturn(duration); + UserAccountEntity userAccount = mock(UserAccountEntity.class); + when(userIdentity.getUserAccount()).thenReturn(userAccount); + OffsetDateTime now = OffsetDateTime.now(); + when(currentTimeHelper.currentOffsetDateTime()).thenReturn(now); + + when(caseRepository.findCaseIdsToBeAnonymised(any(), any())) + .thenReturn(List.of(100)); + + int batchSize = 25; + caseExpiryDeleter.delete(batchSize); + + verify(caseRepository).findCaseIdsToBeAnonymised(now.minus(duration), Limit.of(batchSize)); + verify(dataAnonymisationService).anonymiseCourtCaseById(userAccount, 100, false); + verify(hearingsService).removeMediaLinkToHearing(100); + } + + @Test + void delete_shouldCalculateMaxRetentionDateUsingConfigBuffer() { + Duration duration = Duration.ofDays(7); + when(config.getBufferDuration()).thenReturn(duration); + UserAccountEntity userAccount = mock(UserAccountEntity.class); + when(userIdentity.getUserAccount()).thenReturn(userAccount); + OffsetDateTime fixedNow = OffsetDateTime.parse("2026-03-16T10:00:00Z"); + when(currentTimeHelper.currentOffsetDateTime()).thenReturn(fixedNow); + + when(caseRepository.findCaseIdsToBeAnonymised(any(), any())) + .thenReturn(List.of()); + + caseExpiryDeleter.delete(1); + + OffsetDateTime expectedMaxRetentionDate = fixedNow.minus(duration); + verify(caseRepository).findCaseIdsToBeAnonymised(expectedMaxRetentionDate, Limit.of(1)); + } +} + diff --git a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java index 8cb5d4ff63c..f67b3e6c5d1 100644 --- a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java @@ -157,7 +157,7 @@ void setUp() { } @Test - void testGetCasesById() throws Exception { + void getCasesById_ReturnsCase() throws Exception { List hearings = CommonTestDataUtil.createHearings(1); CourtCaseEntity courtCaseEntity = hearings.getFirst().getCourtCase(); courtCaseEntity.setHearings(hearings); @@ -168,13 +168,13 @@ void testGetCasesById() throws Exception { String actualResponse = objectMapper.writeValueAsString(result); String expectedResponse = getContentsFromFile( - "Tests/cases/CaseServiceTest/testGetCasesById/expectedResponse.json"); + "Tests/cases/CaseServiceTest/getCasesById/expectedResponse.json"); JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); } @Test - void testGetCasesByIdHearingsNotActual() { + void getCasesById_HearingsNotActual() { CourtCaseEntity courtCaseEntity = CommonTestDataUtil.createCase("1"); when(caseRepository.findById(any())).thenReturn(Optional.of(courtCaseEntity)); @@ -185,7 +185,7 @@ void testGetCasesByIdHearingsNotActual() { } @Test - void testGetCasesWithMultipleHearing() throws IOException { + void getHearings_GetsCasesWithMultipleHearings() throws IOException { List hearingEntities = CommonTestDataUtil.createHearings(8); when(hearingRepository.findByCourthouseCourtroomAndDate( @@ -202,14 +202,14 @@ void testGetCasesWithMultipleHearing() throws IOException { List resultList = caseService.getHearings(request); String actualResponse = objectMapper.writeValueAsString(resultList); String expectedResponse = getContentsFromFile( - "Tests/cases/CaseServiceTest/testGetCasesWithMultipleHearing/expectedResponse.json"); + "Tests/cases/CaseServiceTest/GetCasesWithMultipleHearing/expectedResponse.json"); JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); verify(logApi, times(1)).casesRequestedByDarPc(request); } @Test - void testGetCasesWithSingleHearingAndDifferentCourtroom() throws IOException { + void getHearings_GetsCasesWithSingleHearingAndDifferentCourtroom() throws IOException { HearingEntity hearingEntity = createHearingEntity(); when(hearingRepository.findByCourthouseCourtroomAndDate( any(), @@ -225,14 +225,14 @@ void testGetCasesWithSingleHearingAndDifferentCourtroom() throws IOException { List resultList = caseService.getHearings(request); String actualResponse = objectMapper.writeValueAsString(resultList); String expectedResponse = getContentsFromFile( - "Tests/cases/CaseServiceTest/testGetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json"); + "Tests/cases/CaseServiceTest/GetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json"); JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); verify(logApi, times(1)).casesRequestedByDarPc(request); } @Test - void testGetCasesCreateCourtroom() { + void getHearings_ReturnsHearing() { String courtroomName = "99"; when(hearingRepository.findByCourthouseCourtroomAndDate( @@ -256,7 +256,7 @@ void testGetCasesCreateCourtroom() { } @Test - void testAddCase() throws IOException { + void addCaseOrUpdate_ReturnsSuccess() throws IOException { when(caseRepository.saveAndFlush(any())).thenAnswer(invocation -> { Object[] args = invocation.getArguments(); return args[0]; @@ -272,7 +272,7 @@ void testAddCase() throws IOException { String actualResponse = TestUtils.removeTags(List.of("case_id"), objectMapper.writeValueAsString(result)); String expectedResponse = getContentsFromFile( - "Tests/cases/CaseServiceTest/testAddCase/expectedResponseWithoutCourtroom.json"); + "Tests/cases/CaseServiceTest/AddCase/expectedResponseWithoutCourtroom.json"); JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); verify(caseRepository, times(1)).saveAndFlush(caseEntityArgumentCaptor.capture()); @@ -293,7 +293,7 @@ void testAddCase() throws IOException { } @Test - void testAddCaseNonExistingCourthouse() { + void addCaseOrUpdate_NonExistingCourthouse() { AddCaseRequest request = CommonTestDataUtil.createAddCaseRequest(); when(retrieveCoreObjectService.retrieveOrCreateCase( @@ -311,7 +311,7 @@ void testAddCaseNonExistingCourthouse() { } @Test - void testGetCaseHearingsWhenHearingIsActualTrue() { + void getCaseHearings_WhenHearingIsActualTrue() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtroomEntity courtroomEntity = CommonTestDataUtil.createCourtroom(courthouseEntity, "1"); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); @@ -335,7 +335,7 @@ void testGetCaseHearingsWhenHearingIsActualTrue() { } @Test - void testGetCaseHearingsWhenHearingIsActualFalse() { + void getCaseHearings_WhenHearingIsActualFalse() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtroomEntity courtroomEntity = CommonTestDataUtil.createCourtroom(courthouseEntity, "1"); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); @@ -356,7 +356,7 @@ void testGetCaseHearingsWhenHearingIsActualFalse() { } @Test - void testGetCaseHearingsWhenCaseDoesNotExistThrowsException() { + void getCaseHearings_WhenCaseDoesNotExistThrowsException() { when(hearingRepository.findByCaseIds(any())).thenReturn(Collections.emptyList()); var exception = assertThrows(DartsApiException.class, () -> @@ -366,7 +366,7 @@ void testGetCaseHearingsWhenCaseDoesNotExistThrowsException() { } @Test - void testGetCaseHearingsWhenCaseIsExpiredThrowsException() { + void getCaseHearings_WhenCaseIsExpiredThrowsException() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); existingCaseEntity.setId(1); @@ -382,7 +382,7 @@ void testGetCaseHearingsWhenCaseIsExpiredThrowsException() { @Test - void updateCase_WithNonExistingCourtroomAndMatchingHearingDate() { + void addCaseOrUpdate_WithNonExistingCourtroomAndMatchingHearingDate() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); existingCaseEntity.setId(1); @@ -409,11 +409,10 @@ void updateCase_WithNonExistingCourtroomAndMatchingHearingDate() { assertEquals(3, updatedCaseEntity.getProsecutorList().size()); assertEquals(3, updatedCaseEntity.getDefenceList().size()); - } @Test - void updateCase_WithMultipleHearingsWithOldHearingDateWithCourtroomInRequest() { + void addCaseOrUpdate_WithMultipleHearingsWithOldHearingDateWithCourtroomInRequest() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); existingCaseEntity.setId(1); @@ -443,7 +442,7 @@ void updateCase_WithMultipleHearingsWithOldHearingDateWithCourtroomInRequest() { } @Test - void updateCase_WithMultipleHearingsWithCourtroomInRequest() { + void addCaseOrUpdate_WithMultipleHearingsWithCourtroomInRequest() { CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); existingCaseEntity.setId(1); @@ -473,7 +472,7 @@ void updateCase_WithMultipleHearingsWithCourtroomInRequest() { } @Test - void updateCase_WithMultipleHearingsWithoutCourtroomInRequest() { + void addCaseOrUpdate_WithMultipleHearingsWithoutCourtroomInRequest() { // given CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); CourtCaseEntity existingCaseEntity = CommonTestDataUtil.createCase("case1", courthouseEntity); @@ -548,13 +547,6 @@ void adminGetCaseById_ShouldThrowException_WhenCaseDoesNotExist() { verify(caseRepository, times(1)).findById(1); } - private HearingEntity createHearingEntity() { - CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); - CourtroomEntity courtroomEntity = CommonTestDataUtil.createCourtroom(courthouseEntity, "2"); - CourtCaseEntity caseEntity = CommonTestDataUtil.createCase("Case0000009", courthouseEntity); - return CommonTestDataUtil.createHearing(caseEntity, courtroomEntity, LocalDate.of(2023, Month.JULY, 20), true); - } - @Test @SuppressWarnings("unchecked") void getEventsByCaseIdPaginated_shouldPaginatedCorrectly_whenGivenTypicalData() { @@ -609,4 +601,12 @@ void getEventsByCaseIdPaginated_whenCaseIsExpired_shouldThrowError() { DartsApiException exception = assertThrows(DartsApiException.class, () -> caseService.getEventsByCaseId(caseId, null)); assertThat(exception.getError()).isEqualTo(CaseApiError.CASE_EXPIRED); } + + private HearingEntity createHearingEntity() { + CourthouseEntity courthouseEntity = CommonTestDataUtil.createCourthouse(SWANSEA); + CourtroomEntity courtroomEntity = CommonTestDataUtil.createCourtroom(courthouseEntity, "2"); + CourtCaseEntity caseEntity = CommonTestDataUtil.createCase("Case0000009", courthouseEntity); + return CommonTestDataUtil.createHearing(caseEntity, courtroomEntity, LocalDate.of(2023, Month.JULY, 20), true); + } + } \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/darts/common/entity/MediaEntityTest.java b/src/test/java/uk/gov/hmcts/darts/common/entity/MediaEntityTest.java index 7f98572691e..ac1d3f8a90b 100644 --- a/src/test/java/uk/gov/hmcts/darts/common/entity/MediaEntityTest.java +++ b/src/test/java/uk/gov/hmcts/darts/common/entity/MediaEntityTest.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -130,4 +131,84 @@ void addHearing_shouldNotAddHearing_whenMediaAlreadyContainsHearing() { verify(hearing, never()).addMedia(any()); } + @Test + void removeHearing_shouldRemoveBidirectionalLink() { + MediaEntity media = new MediaEntity(); + media.setId(1L); + + HearingEntity hearing = new HearingEntity(); + hearing.setId(101); + + // establish link on both sides + hearing.setMedias(new HashSet<>()); + hearing.getMedias().add(media); + + media.setHearings(new HashSet<>()); + media.getHearings().add(hearing); + + // sanity check + assertThat(hearing.getMedias()).contains(media); + assertThat(media.getHearings()).contains(hearing); + + media.removeHearing(hearing); + + assertThat(hearing.getMedias()).doesNotContain(media); + assertThat(media.getHearings()).doesNotContain(hearing); + } + + @Test + void removeHearing_shouldBeIdempotent_whenLinkDoesNotExist() { + MediaEntity media = new MediaEntity(); + media.setId(1L); + + HearingEntity hearing = new HearingEntity(); + hearing.setId(101); + + hearing.setMedias(new HashSet<>()); + media.setHearings(new HashSet<>()); + + media.removeHearing(hearing); + + assertThat(hearing.getMedias()).isEmpty(); + assertThat(media.getHearings()).isEmpty(); + } + + @Test + void removeHearing_shouldRemoveFromHearingSide_evenIfMediaSideWasNotLinked() { + MediaEntity media = new MediaEntity(); + media.setId(1L); + + HearingEntity hearing = new HearingEntity(); + hearing.setId(101); + + hearing.setMedias(new HashSet<>()); + hearing.getMedias().add(media); + + media.setHearings(new HashSet<>()); // no back-link + + media.removeHearing(hearing); + + assertThat(hearing.getMedias()).doesNotContain(media); + assertThat(media.getHearings()).isEmpty(); + } + + @Test + void removeHearing_shouldRemoveFromMediaSide_evenIfHearingSideWasNotLinked() { + MediaEntity media = new MediaEntity(); + media.setId(1L); + + HearingEntity hearing = new HearingEntity(); + hearing.setId(101); + + hearing.setMedias(new HashSet<>()); // no link + + media.setHearings(new HashSet<>()); + media.getHearings().add(hearing); + + media.removeHearing(hearing); + + assertThat(hearing.getMedias()).isEmpty(); + assertThat(media.getHearings()).doesNotContain(hearing); + } + } \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImplTest.java index 2fb8312f213..922de5f987d 100644 --- a/src/test/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/common/service/impl/DataAnonymisationServiceImplTest.java @@ -32,6 +32,7 @@ import uk.gov.hmcts.darts.common.entity.base.CreatedModifiedBaseEntity; import uk.gov.hmcts.darts.common.helper.CurrentTimeHelper; import uk.gov.hmcts.darts.common.repository.DataAnonymisationRepository; +import uk.gov.hmcts.darts.common.repository.MediaRequestRepository; import uk.gov.hmcts.darts.common.repository.TransformedMediaRepository; import uk.gov.hmcts.darts.common.repository.TransientObjectDirectoryRepository; import uk.gov.hmcts.darts.event.service.EventService; @@ -78,6 +79,8 @@ class DataAnonymisationServiceImplTest { private EventService eventService; @Mock private DataAnonymisationRepository dataAnonymisationRepository; + @Mock + private MediaRequestRepository mediaRequestRepository; @InjectMocks @Spy @@ -227,7 +230,6 @@ void anonymiseTranscriptionCommentEntity_typical(boolean isManuallyRequested) { verify(dataAnonymisationService).registerDataAnonymisation(userAccount, transcriptionCommentEntity, isManuallyRequested); } - @ParameterizedTest(name = "Anonymise CourtCase with isManuallyRequested = {0}") @ValueSource(booleans = {true, false}) void anonymiseCourtCaseById_typical(boolean isManuallyRequested) { @@ -294,7 +296,6 @@ void anonymiseCourtCaseById_typical(boolean isManuallyRequested) { verify(eventService).allAssociatedCasesAnonymised(eventEntity2); } - @ParameterizedTest(name = "Anonymise Transcription with isManuallyRequested = {0}") @ValueSource(booleans = {true, false}) void anonymiseTranscriptionEntity_typical(boolean isManuallyRequested) { @@ -311,7 +312,6 @@ void anonymiseTranscriptionEntity_typical(boolean isManuallyRequested) { doNothing().when(dataAnonymisationService).anonymiseTranscriptionCommentEntity(any(), any(), anyBoolean()); doNothing().when(dataAnonymisationService).anonymiseTranscriptionWorkflowEntity(any()); - UserAccountEntity userAccount = new UserAccountEntity(); dataAnonymisationService.anonymiseTranscriptionEntity(userAccount, transcriptionEntity, isManuallyRequested); @@ -344,17 +344,20 @@ void anonymiseHearingEntity_typical(boolean isManuallyRequested) { verify(dataAnonymisationService).anonymiseTranscriptionEntity(userAccount, transcriptionEntity2, isManuallyRequested); } - @Test - void positiveExpireMediaRequest() { + void expiredMediaRequest_updatesStatusAndSavesEntity() { setupOffsetDateTime(); - MediaRequestEntity mediaRequestEntity = new MediaRequestEntity(); UserAccountEntity userAccount = new UserAccountEntity(); + userAccount.setId(42); + + MediaRequestEntity mediaRequestEntity = new MediaRequestEntity(); + mediaRequestEntity.setStatus(MediaRequestStatus.COMPLETED); dataAnonymisationService.expiredMediaRequest(userAccount, mediaRequestEntity); assertThat(mediaRequestEntity.getStatus()).isEqualTo(MediaRequestStatus.EXPIRED); assertLastModifiedByAndAt(mediaRequestEntity, userAccount); + verify(mediaRequestRepository).save(mediaRequestEntity); } @Test @@ -425,7 +428,6 @@ void positiveTidyUpTransformedMediaEntities() { UserAccountEntity userAccount = new UserAccountEntity(); - TransformedMediaEntity transformedMediaEntity1 = new TransformedMediaEntity(); TransformedMediaEntity transformedMediaEntity2 = new TransformedMediaEntity(); TransformedMediaEntity transformedMediaEntity3 = new TransformedMediaEntity(); @@ -452,31 +454,24 @@ void positiveTidyUpTransformedMediaEntities() { transformedMediaEntity4 )); - dataAnonymisationService.tidyUpTransformedMediaEntities(userAccount, courtCase); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing1MediaRequestEntity1); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing1MediaRequestEntity2); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing1MediaRequestEntity3); - verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing2MediaRequestEntity1); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing2MediaRequestEntity2); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing2MediaRequestEntity3); verify(dataAnonymisationService).expiredMediaRequest(userAccount, hearing3MediaRequestEntity1); + verify(dataAnonymisationService).deleteTransformedMediaEntity(transformedMediaEntity1); + verify(dataAnonymisationService).deleteTransformedMediaEntity(transformedMediaEntity2); + verify(dataAnonymisationService).deleteTransformedMediaEntity(transformedMediaEntity3); + verify(dataAnonymisationService).deleteTransformedMediaEntity(transformedMediaEntity4); + verify(dataAnonymisationService).deleteTransformedMediaEntity(transformedMediaEntity5); - verify(dataAnonymisationService) - .deleteTransformedMediaEntity(transformedMediaEntity1); - verify(dataAnonymisationService) - .deleteTransformedMediaEntity(transformedMediaEntity2); - verify(dataAnonymisationService) - .deleteTransformedMediaEntity(transformedMediaEntity3); - verify(dataAnonymisationService) - .deleteTransformedMediaEntity(transformedMediaEntity4); - verify(dataAnonymisationService) - .deleteTransformedMediaEntity(transformedMediaEntity5); } @ParameterizedTest(name = "Anonymise event by ids with isManuallyRequested = {0}") @@ -486,7 +481,6 @@ void anonymiseEventByIds_typical(boolean isManuallyRequested) { EventEntity event2 = mock(EventEntity.class); EventEntity event3 = mock(EventEntity.class); - doReturn(event1).when(eventService).getEventByEveId(1L); doReturn(event2).when(eventService).getEventByEveId(2L); doReturn(event3).when(eventService).getEventByEveId(3L); @@ -496,7 +490,6 @@ void anonymiseEventByIds_typical(boolean isManuallyRequested) { dataAnonymisationService.anonymiseEventByIds(userAccount, List.of(1L, 2L, 3L, 4L), isManuallyRequested); - verify(dataAnonymisationService).anonymiseEventEntity(userAccount, event1, false, isManuallyRequested); verify(dataAnonymisationService).anonymiseEventEntity(userAccount, event2, false, isManuallyRequested); verify(dataAnonymisationService).anonymiseEventEntity(userAccount, event3, false, isManuallyRequested); @@ -558,4 +551,4 @@ void registerDataAnonymisation_transcriptionComment_typical(boolean isManuallyRe assertThat(dataAnonymisationEntity.getApprovedTs()).isEqualTo(currentTime); verify(currentTimeHelper, times(1)).currentOffsetDateTime(); } -} \ No newline at end of file +} diff --git a/src/test/resources/Tests/cases/CaseServiceTest/testAddCase/expectedResponseWithCourtroom.json b/src/test/resources/Tests/cases/CaseServiceTest/AddCase/expectedResponseWithCourtroom.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/testAddCase/expectedResponseWithCourtroom.json rename to src/test/resources/Tests/cases/CaseServiceTest/AddCase/expectedResponseWithCourtroom.json diff --git a/src/test/resources/Tests/cases/CaseServiceTest/testAddCase/expectedResponseWithoutCourtroom.json b/src/test/resources/Tests/cases/CaseServiceTest/AddCase/expectedResponseWithoutCourtroom.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/testAddCase/expectedResponseWithoutCourtroom.json rename to src/test/resources/Tests/cases/CaseServiceTest/AddCase/expectedResponseWithoutCourtroom.json diff --git a/src/test/resources/Tests/cases/CaseServiceTest/updateCase/expectedResponse.json b/src/test/resources/Tests/cases/CaseServiceTest/AddCaseOrUpdate/expectedResponse.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/updateCase/expectedResponse.json rename to src/test/resources/Tests/cases/CaseServiceTest/AddCaseOrUpdate/expectedResponse.json diff --git a/src/test/resources/Tests/cases/CaseServiceTest/testGetCasesById/expectedResponse.json b/src/test/resources/Tests/cases/CaseServiceTest/GetCasesById/expectedResponse.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/testGetCasesById/expectedResponse.json rename to src/test/resources/Tests/cases/CaseServiceTest/GetCasesById/expectedResponse.json diff --git a/src/test/resources/Tests/cases/CaseServiceTest/testGetCasesWithMultipleHearing/expectedResponse.json b/src/test/resources/Tests/cases/CaseServiceTest/GetCasesWithMultipleHearing/expectedResponse.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/testGetCasesWithMultipleHearing/expectedResponse.json rename to src/test/resources/Tests/cases/CaseServiceTest/GetCasesWithMultipleHearing/expectedResponse.json diff --git a/src/test/resources/Tests/cases/CaseServiceTest/testGetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json b/src/test/resources/Tests/cases/CaseServiceTest/GetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json similarity index 100% rename from src/test/resources/Tests/cases/CaseServiceTest/testGetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json rename to src/test/resources/Tests/cases/CaseServiceTest/GetCasesWithSingleHearingAndDifferentCourtroom/expectedResponse.json From a0402abe9a1494b3f3bfdee5ac8158bd7ae9fdaa Mon Sep 17 00:00:00 2001 From: karen-hedges <133129444+karen-hedges@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:47:04 +0000 Subject: [PATCH 2/4] DMP-5445 Media request expiry state needs to be saved Added code to save the media request once set to expired and also fixed an issue with deleting media from hearing --- .../audio/service/impl/AudioUploadServiceImpl.java | 4 +++- .../gov/hmcts/darts/common/entity/MediaEntity.java | 12 ++++++++++-- .../hearings/service/impl/HearingsServiceImpl.java | 5 +++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImpl.java b/src/main/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImpl.java index ba07a8b900a..9ae9f8553a2 100644 --- a/src/main/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImpl.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -228,7 +229,8 @@ public void linkAudioToHearingInMetadata(AddAudioMetadataRequest addAudioMetadat @Override public void deleteMediaLinkingAndSetCurrentFalse(MediaEntity mediaEntity) { - Set hearingList = mediaEntity.getHearings(); + // removeHearing mutates mediaEntity.getHearings(); iterate over a copy to avoid ConcurrentModificationException + Set hearingList = new HashSet<>(mediaEntity.getHearings()); for (HearingEntity hearing : hearingList) { mediaEntity.removeHearing(hearing); } diff --git a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java index 337dacbe9bd..c9ea201b065 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java +++ b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java @@ -159,8 +159,16 @@ public List associatedCourtCases() { } public void removeHearing(HearingEntity hearing) { - hearing.getMedias().remove(this); - getHearings().remove(hearing); + // HearingEntity is the owning side of the ManyToMany (join table), so update it first. + if (hearing != null) { + hearing.getMedias().remove(this); + } + // Also update the inverse side to keep the in-memory graph consistent. + // (Important: callers may iterate mediaEntity.getHearings(); mutating via hearing.getMedias() above + // doesn't necessarily mutate this Set depending on persistence state, so explicitly remove.) + if (hearing != null) { + hearings.remove(hearing); + } } public void addHearing(HearingEntity hearing) { diff --git a/src/main/java/uk/gov/hmcts/darts/hearings/service/impl/HearingsServiceImpl.java b/src/main/java/uk/gov/hmcts/darts/hearings/service/impl/HearingsServiceImpl.java index 87f80db1aaa..ed382fc60ea 100644 --- a/src/main/java/uk/gov/hmcts/darts/hearings/service/impl/HearingsServiceImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/hearings/service/impl/HearingsServiceImpl.java @@ -30,6 +30,7 @@ import uk.gov.hmcts.darts.hearings.model.Transcript; import uk.gov.hmcts.darts.hearings.service.HearingsService; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -92,7 +93,6 @@ public List getEvents(Integer hearingId) { return GetEventsResponseMapper.mapToEvents(eventEntities); } - @Override public List getTranscriptsByHearingId(Integer hearingId) { validateCaseIsNotExpiredFromHearingId(hearingId); @@ -131,7 +131,8 @@ public void removeMediaLinkToHearing(Integer courtCaseId) { }) .forEach(media -> { Set hearingEntities = media.getHearings(); - hearingEntities.forEach(media::removeHearing); + // media.removeHearing mutates media.getHearings(); iterate over a copy to avoid ConcurrentModificationException + new HashSet<>(hearingEntities).forEach(media::removeHearing); mediaRepository.save(media); hearingRepository.saveAll(hearingEntities); From 01bf1f37075665f7f03643d1ce21e64df503c37f Mon Sep 17 00:00:00 2001 From: karen-hedges <133129444+karen-hedges@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:34:18 +0000 Subject: [PATCH 3/4] DMP-5445 Media request expiry state needs to be saved Added code to save the media request once set to expired and also fixed an issue with deleting media from hearing --- .../java/uk/gov/hmcts/darts/common/entity/MediaEntity.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java index c9ea201b065..55d80b6ae3d 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java +++ b/src/main/java/uk/gov/hmcts/darts/common/entity/MediaEntity.java @@ -162,11 +162,8 @@ public void removeHearing(HearingEntity hearing) { // HearingEntity is the owning side of the ManyToMany (join table), so update it first. if (hearing != null) { hearing.getMedias().remove(this); - } - // Also update the inverse side to keep the in-memory graph consistent. - // (Important: callers may iterate mediaEntity.getHearings(); mutating via hearing.getMedias() above - // doesn't necessarily mutate this Set depending on persistence state, so explicitly remove.) - if (hearing != null) { + // Callers may iterate mediaEntity.getHearings(); mutating via hearing.getMedias() above + // doesn't necessarily mutate this Set depending on persistence state, so explicitly remove. hearings.remove(hearing); } } From 2410493e55eded45974d45cece9165b528ff1b2c Mon Sep 17 00:00:00 2001 From: karen-hedges <133129444+karen-hedges@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:37:02 +0000 Subject: [PATCH 4/4] DMP-5445 Media request expiry state needs to be saved Added code to save the media request once set to expired and also fixed an issue with deleting media from hearing --- .../gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java index f67b3e6c5d1..7abdc647260 100644 --- a/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/cases/service/impl/CaseServiceImplTest.java @@ -168,7 +168,7 @@ void getCasesById_ReturnsCase() throws Exception { String actualResponse = objectMapper.writeValueAsString(result); String expectedResponse = getContentsFromFile( - "Tests/cases/CaseServiceTest/getCasesById/expectedResponse.json"); + "Tests/cases/CaseServiceTest/GetCasesById/expectedResponse.json"); JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); }