diff --git a/build.gradle b/build.gradle index 3f794a9a12..8b6f38a036 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ swaggerList.each { modelPackage = "uk.gov.hmcts.darts.".toString() + "${apiName}" + ".model" // https://openapi-generator.tech/docs/generators/java/#config-options skipOperationExample = true + openapiNormalizer = [ + REF_AS_PARENT_IN_ALLOF: "true" + ] configOptions = [ dateLibrary : "java8", interfaceOnly : "true", diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetaDataNoSessionITest.java b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetaDataNoSessionITest.java new file mode 100644 index 0000000000..47aaa7758e --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetaDataNoSessionITest.java @@ -0,0 +1,159 @@ +package uk.gov.hmcts.darts.audio.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.util.unit.DataSize; +import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; +import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequestWithStorageGUID; +import uk.gov.hmcts.darts.authorisation.component.UserIdentity; +import uk.gov.hmcts.darts.common.entity.MediaEntity; +import uk.gov.hmcts.darts.common.entity.UserAccountEntity; +import uk.gov.hmcts.darts.common.enums.SecurityRoleEnum; +import uk.gov.hmcts.darts.test.common.DataGenerator; +import uk.gov.hmcts.darts.testutils.IntegrationBase; +import uk.gov.hmcts.darts.testutils.stubs.AuthorisationStub; +import uk.gov.hmcts.darts.testutils.stubs.SuperAdminUserStub; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { + "spring.servlet.multipart.max-file-size=4MB", + "spring.servlet.multipart.max-request-size=4MB", +}) +@SuppressWarnings({"PMD.DoNotUseThreads", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidThrowingRawExceptionTypes"}) +class AudioControllerAddAudioMetaDataNoSessionITest extends IntegrationBase { + + @Value("${local.server.port}") + protected int port; + + private static final URI ENDPOINT = URI.create("/audios/metadata"); + private static final OffsetDateTime STARTED_AT = OffsetDateTime.of(2024, 10, 10, 10, 0, 0, 0, ZoneOffset.UTC); + private static final Path AUDIO_BINARY_PAYLOAD_1 = DataGenerator.createUniqueFile(DataSize.ofBytes(10), DataGenerator.FileType.MP2); + + + @Autowired + private MockMvc mockMvc; + @Autowired + private AuthorisationStub authorisationStub; + @MockBean + private UserIdentity mockUserIdentity; + @Autowired + private SuperAdminUserStub superAdminUserStub; + + + @BeforeEach + void beforeEach() { + UserAccountEntity testUser = authorisationStub.getTestUser(); + when(mockUserIdentity.getUserAccount()).thenReturn(testUser); + + dartsDatabase.createCourthouseUnlessExists("Bristol"); + + } + + + @Test + void testAddAudioWithConcurrency() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + int numberOfThreads = 5; + try (ExecutorService service = Executors.newFixedThreadPool(5)) { + CountDownLatch latch = new CountDownLatch(numberOfThreads); + List> futures = new ArrayList<>(); + + for (int i = 0; i < numberOfThreads; i++) { + final int threadNum = i; + Future future = service.submit(() -> { + try { + OffsetDateTime startTime = STARTED_AT.plusDays(threadNum); + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(startTime, startTime.plusHours(1), "Bristol", "1"); + mockMvc.perform(post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andReturn(); + + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + latch.countDown(); + } + }); + futures.add(future); + } + + // Wait for all tasks to complete + boolean completed = latch.await(5, TimeUnit.SECONDS); + assertTrue(completed, "Not all threads completed in time"); + + // Wait for all futures to complete to ensure DB operations are done + for (Future future : futures) { + future.get(); + } + + // Add a small delay to allow for any potential lag in DB updates + Thread.sleep(100); + + List mediaList = dartsDatabase.getMediaRepository().findAll(); + + assertEquals(numberOfThreads, mediaList.size()); + } + } + + private AddAudioMetadataRequestWithStorageGUID createAddAudioRequest(OffsetDateTime startedAt, + OffsetDateTime endedAt, String courthouse, String courtroom) throws IOException { + return createAddAudioRequest(startedAt, endedAt, courthouse, courtroom, + "mp2", AUDIO_BINARY_PAYLOAD_1, "case1", "case2", "case3"); + } + + private AddAudioMetadataRequestWithStorageGUID createAddAudioRequest(OffsetDateTime startedAt, OffsetDateTime endedAt, + String courthouse, String courtroom, String filetype, Path audioBinaryPayload, + String... casesList) throws IOException { + + AddAudioMetadataRequestWithStorageGUID addAudioMetadataRequest = new AddAudioMetadataRequestWithStorageGUID(); + addAudioMetadataRequest.startedAt(startedAt); + addAudioMetadataRequest.endedAt(endedAt); + addAudioMetadataRequest.setChannel(1); + addAudioMetadataRequest.totalChannels(2); + addAudioMetadataRequest.format(filetype); + addAudioMetadataRequest.filename("test"); + addAudioMetadataRequest.courthouse(courthouse); + addAudioMetadataRequest.courtroom(courtroom); + addAudioMetadataRequest.cases(List.of(casesList)); + addAudioMetadataRequest.setMediaFile("media file"); + addAudioMetadataRequest.setFileSize(Files.size(audioBinaryPayload)); + addAudioMetadataRequest.storageGuid(UUID.randomUUID()); + addAudioMetadataRequest.setChecksum("checksum-" + addAudioMetadataRequest.getStorageGuid()); + return addAudioMetadataRequest; + } +} \ No newline at end of file diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataITest.java b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataITest.java new file mode 100644 index 0000000000..d2e2c21896 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataITest.java @@ -0,0 +1,612 @@ +package uk.gov.hmcts.darts.audio.controller; + +import ch.qos.logback.classic.Level; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.util.unit.DataSize; +import uk.gov.hmcts.darts.audio.exception.AudioApiError; +import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; +import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequestWithStorageGUID; +import uk.gov.hmcts.darts.audio.model.Problem; +import uk.gov.hmcts.darts.audio.service.AudioAsyncService; +import uk.gov.hmcts.darts.authorisation.component.UserIdentity; +import uk.gov.hmcts.darts.common.entity.HearingEntity; +import uk.gov.hmcts.darts.common.entity.MediaEntity; +import uk.gov.hmcts.darts.common.entity.MediaLinkedCaseEntity; +import uk.gov.hmcts.darts.common.entity.UserAccountEntity; +import uk.gov.hmcts.darts.common.enums.SecurityRoleEnum; +import uk.gov.hmcts.darts.common.util.DateConverterUtil; +import uk.gov.hmcts.darts.test.common.DataGenerator; +import uk.gov.hmcts.darts.test.common.LogUtil; +import uk.gov.hmcts.darts.testutils.IntegrationBase; +import uk.gov.hmcts.darts.testutils.stubs.AuthorisationStub; +import uk.gov.hmcts.darts.testutils.stubs.EventStub; +import uk.gov.hmcts.darts.testutils.stubs.HearingStub; +import uk.gov.hmcts.darts.testutils.stubs.SuperAdminUserStub; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static ch.qos.logback.classic.Level.toLevel; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { + "spring.servlet.multipart.max-file-size=4MB", + "spring.servlet.multipart.max-request-size=4MB", +}) +class AudioControllerAddAudioMetadataITest extends IntegrationBase { + + @Value("${local.server.port}") + protected int port; + + private static final URI ENDPOINT = URI.create("/audios/metadata"); + private static final OffsetDateTime STARTED_AT = OffsetDateTime.of(2024, 10, 10, 10, 0, 0, 0, ZoneOffset.UTC); + private static final Path AUDIO_BINARY_PAYLOAD_1 = DataGenerator.createUniqueFile(DataSize.ofBytes(10), + DataGenerator.FileType.MP2); + private static final Path AUDIO_BINARY_PAYLOAD_2 = DataGenerator.createUniqueFile(DataSize.ofBytes(10), + DataGenerator.FileType.MP2); + private static final Path AUDIO_BINARY_PAYLOAD_3 = DataGenerator.createUniqueFile(DataSize.ofBytes(10), + DataGenerator.FileType.MP2); + private static final Path AUDIO_BINARY_PAYLOAD_EXCEEDING_MAX_ALLOWABLE_SIZE = DataGenerator.createUniqueFile(DataSize.ofMegabytes(5), + DataGenerator.FileType.MP2); + + @Value("${darts.audio.max-file-duration}") + private Duration maxFileDuration; + + @Autowired + private MockMvc mockMvc; + @Autowired + private AuthorisationStub authorisationStub; + @Autowired + private EventStub eventStub; + @Autowired + HearingStub hearingStub; + @MockBean + private UserIdentity mockUserIdentity; + + @MockBean + AudioAsyncService audioAsyncService; + + @Value("${spring.servlet.multipart.max-file-size}") + private DataSize addAudioThreshold; + + @Autowired + private SuperAdminUserStub superAdminUserStub; + + private UUID guid = UUID.randomUUID(); + + + @BeforeEach + void beforeEach() { + openInViewUtil.openEntityManager(); + authorisationStub.givenTestSchema(); + + UserAccountEntity testUser = authorisationStub.getTestUser(); + when(mockUserIdentity.getUserAccount()).thenReturn(testUser); + + dartsDatabase.getUserAccountRepository().save(testUser); + + dartsDatabase.createCase("Bristol", "case1"); + dartsDatabase.createCase("Bristol", "case2"); + dartsDatabase.createCase("Bristol", "case3"); + + HearingEntity hearingForEvent = hearingStub.createHearing("Bristol", "1", "case1", DateConverterUtil.toLocalDateTime(STARTED_AT)); + eventStub.createEvent(hearingForEvent, 10, STARTED_AT.minusMinutes(20), "LOG"); + HearingEntity hearingDifferentCourtroom = hearingStub.createHearing("Bristol", "2", "case2", DateConverterUtil.toLocalDateTime(STARTED_AT)); + eventStub.createEvent(hearingDifferentCourtroom, 10, STARTED_AT.minusMinutes(20), "LOG"); + } + + @AfterEach + void closeHibernateSession() { + openInViewUtil.closeEntityManager(); + } + + @Test + void addAudioMetadata() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration), "Bristol", "1"); + + mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isOk()) + .andReturn(); + + List allHearings = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + List addAudioLinkedHearings = new ArrayList<>(); + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(3, addAudioLinkedHearings.size()); + + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + MediaEntity media = mediaEntities.get(0); + assertEquals(1, mediaEntities.size()); + assertEquals(STARTED_AT, media.getStart()); + assertEquals(STARTED_AT.plus(maxFileDuration), media.getEnd()); + assertEquals(1, media.getChannel()); + assertEquals(2, media.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(media); + assertEquals(3, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(media.getCourtroom().getId()).get().getName()); + assertEquals(media.getId().toString(), media.getChronicleId()); + assertEquals(true, media.getIsCurrent()); + assertNull(media.getAntecedentId()); + } + + List hearingsInAnotherCourtroom = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate( + "bristol", + "2", + STARTED_AT.toLocalDate() + ); + assertEquals(1, hearingsInAnotherCourtroom.size());//should have hearingDifferentCourtroom + + HearingEntity hearingEntity = hearingsInAnotherCourtroom.get(0); + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearingEntity.getId()); + assertEquals(0, mediaEntities.size());//shouldn't have any as no audio in that courtroom + } + + @Test + void addAudioMetadataChecksumsDoNotMatch() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + AddAudioMetadataRequestWithStorageGUID addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration), "Bristol", "1"); + addAudioMetadataRequest.setChecksum("invalidchecksum"); + MvcResult mvcResult = mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isUnprocessableEntity()) + .andReturn(); + + String actualJson = mvcResult.getResponse().getContentAsString(); + + Problem problem = objectMapper.readValue(actualJson, Problem.class); + assertEquals(AudioApiError.FAILED_TO_ADD_AUDIO_META_DATA.getType(), problem.getType().toString()); + assertEquals(AudioApiError.FAILED_TO_ADD_AUDIO_META_DATA.getTitle(), problem.getTitle()); + assertEquals( + "Checksum in DETs (checksum-" + addAudioMetadataRequest.getStorageGuid() + ") does not match the one passed in the API request (invalidchecksum).", + problem.getDetail()); + } + + + @Test + void addAudioMetadataDifferentCases() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + makeAddAudioCall(AUDIO_BINARY_PAYLOAD_1, "case1") + .andExpect(status().isOk()); + + List allHearings = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + List addAudioLinkedHearings = new ArrayList<>(); + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case1")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(1, addAudioLinkedHearings.size()); + + MediaEntity mediaFirst = null; + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + mediaFirst = mediaEntities.get(0); + assertEquals(1, mediaEntities.size()); + assertEquals(STARTED_AT, mediaFirst.getStart()); + assertEquals(STARTED_AT, mediaFirst.getEnd()); + assertEquals(1, mediaFirst.getChannel()); + assertEquals(2, mediaFirst.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(mediaFirst); + assertEquals(1, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(mediaFirst.getCourtroom().getId()).get().getName()); + assertEquals(mediaFirst.getId().toString(), mediaFirst.getChronicleId()); + assertNull(mediaFirst.getAntecedentId()); + } + + makeAddAudioCall(AUDIO_BINARY_PAYLOAD_1, "case2") + .andExpect(status().isOk()); + + addAudioLinkedHearings = new ArrayList<>(); + allHearings = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case2")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(1, addAudioLinkedHearings.size()); + + MediaEntity mediaSecond = null; + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + mediaSecond = mediaEntities.get(0); + assertEquals(1, mediaEntities.size()); + assertEquals(STARTED_AT, mediaSecond.getStart()); + assertEquals(STARTED_AT, mediaSecond.getEnd()); + assertEquals(1, mediaSecond.getChannel()); + assertEquals(2, mediaSecond.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(mediaSecond); + assertEquals(1, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(mediaSecond.getCourtroom().getId()).get().getName()); + assertEquals(mediaSecond.getId().toString(), mediaSecond.getChronicleId()); + assertNull(mediaSecond.getAntecedentId()); + } + + assertNotSame(mediaFirst, mediaSecond); + } + + @Test + void addAudioMetadataDuplicate() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + makeAddAudioCall(); + makeAddAudioCall(); + + List allHearings = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + List addAudioLinkedHearings = new ArrayList<>(); + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(3, addAudioLinkedHearings.size()); + + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + MediaEntity media = mediaEntities.get(0); + assertEquals(1, mediaEntities.size()); + assertEquals(STARTED_AT, media.getStart()); + assertEquals(STARTED_AT, media.getEnd()); + assertEquals(1, media.getChannel()); + assertEquals(2, media.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(media); + assertEquals(3, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(media.getCourtroom().getId()).get().getName()); + assertEquals(media.getId().toString(), media.getChronicleId()); + assertNull(media.getAntecedentId()); + assertEquals(media.getId().toString(), media.getChronicleId()); + } + + List hearingsInAnotherCourtroom = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate( + "bristol", + "2", + STARTED_AT.toLocalDate() + ); + assertEquals(1, hearingsInAnotherCourtroom.size());//should have hearingDifferentCourtroom + + HearingEntity hearingEntity = hearingsInAnotherCourtroom.get(0); + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearingEntity.getId()); + assertEquals(0, mediaEntities.size());//shouldn't have any as no audio in that courtroom + + assertFalse(Objects.requireNonNull(LogUtil.getMemoryLogger()) + .searchLogs("Exact duplicate detected based upon media metadata and checksum.", toLevel( + Level.INFO_INT)).isEmpty()); + } + + @Test + void addAudioMetadataVersionedDueToDuplicateMetadataButDifferentChecksum() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + makeAddAudioCall(); + + List allHearings = dartsDatabase.getHearingRepository() + .findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + List addAudioLinkedHearings = new ArrayList<>(); + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(3, addAudioLinkedHearings.size()); + + MediaEntity originalMedia = null; + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + + assertEquals(1, mediaEntities.size()); + + originalMedia = mediaEntities.get(0); + assertEquals(STARTED_AT, originalMedia.getStart()); + assertEquals(STARTED_AT, originalMedia.getEnd()); + assertEquals(1, originalMedia.getChannel()); + assertEquals(2, originalMedia.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(originalMedia); + assertEquals(3, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(originalMedia.getCourtroom().getId()).get().getName()); + assertEquals(originalMedia.getId().toString(), originalMedia.getChronicleId()); + assertNull(originalMedia.getAntecedentId()); + } + + List hearingsInAnotherCourtroom = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate( + "bristol", + "2", + STARTED_AT.toLocalDate() + ); + assertEquals(1, hearingsInAnotherCourtroom.size());//should have hearingDifferentCourtroom + + guid = UUID.randomUUID(); + Integer newMedia = uploadAnotherAudioWithSize(AUDIO_BINARY_PAYLOAD_2, originalMedia.getId().toString(), originalMedia.getId().toString()); + guid = UUID.randomUUID(); + Integer newMedia2 = uploadAnotherAudioWithSize(AUDIO_BINARY_PAYLOAD_3, newMedia.toString(), originalMedia.getId().toString()); + assertNotEquals(newMedia, newMedia2); + Optional newMediaEntity = dartsDatabase.getMediaRepository().findById(newMedia); + assertEquals(false, newMediaEntity.get().getIsCurrent()); + Optional newMedia2Entity = dartsDatabase.getMediaRepository().findById(newMedia2); + assertEquals(true, newMedia2Entity.get().getIsCurrent()); + + } + + @Test + void addAudioBeyondAudioFileSizeThresholdExceeded() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + MvcResult mvcResult = makeAddAudioCall(AUDIO_BINARY_PAYLOAD_EXCEEDING_MAX_ALLOWABLE_SIZE) + .andExpect(status().isBadRequest()) + .andReturn(); + + String actualResponse = mvcResult.getResponse().getContentAsString(); + + String expectedResponse = """ + { + "type": "AUDIO_108", + "title": "The audio metadata size exceeds maximum threshold", + "status": 400 + } + """; + JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + void addAudioMetadataNonExistingCourthouse() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration), "TEST", "1"); + + MvcResult mvcResult = mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isBadRequest()) + .andReturn(); + + String actualJson = mvcResult.getResponse().getContentAsString(); + String expectedJson = """ + {"type":"COMMON_100","title":"Provided courthouse does not exist","status":400,"detail":"Courthouse 'TEST' not found."}"""; + + JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.NON_EXTENSIBLE); + } + + + @Test + void addAudioDurationOutOfBounds() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration).plusSeconds(1), "Bristol", "1"); + + MvcResult mvcResult = mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isBadRequest()) + .andReturn(); + + String actualJson = mvcResult.getResponse().getContentAsString(); + + Problem problem = objectMapper.readValue(actualJson, Problem.class); + assertEquals(AudioApiError.FILE_DURATION_OUT_OF_BOUNDS.getType(), problem.getType()); + assertEquals(AudioApiError.FILE_DURATION_OUT_OF_BOUNDS.getTitle(), problem.getTitle()); + } + + @Test + void addAudioSizeOutsideOfBoundaries() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.MID_TIER); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration), "Bristol", "1"); + + // set the file size to be greater than the maximum threshold + addAudioMetadataRequest.setFileSize(addAudioThreshold.toBytes() + 1); + + + MvcResult mvcResult = mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isBadRequest()) + .andReturn(); + + String actualJson = mvcResult.getResponse().getContentAsString(); + + Problem problem = objectMapper.readValue(actualJson, Problem.class); + assertEquals(AudioApiError.FILE_SIZE_OUT_OF_BOUNDS.getType(), problem.getType()); + assertEquals(AudioApiError.FILE_SIZE_OUT_OF_BOUNDS.getTitle(), problem.getTitle()); + } + + + @Test + void addAudioReturnForbiddenError() throws Exception { + superAdminUserStub.givenUserIsAuthorised(mockUserIdentity, SecurityRoleEnum.DAR_PC); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT.plus(maxFileDuration), "TEST", "1"); + + + MvcResult mvcResult = mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))) + .andExpect(status().isForbidden()) + .andReturn(); + + String actualResponse = mvcResult.getResponse().getContentAsString(); + + String expectedResponse = """ + {"type":"AUTHORISATION_109","title":"User is not authorised for this endpoint","status":403} + """; + JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.NON_EXTENSIBLE); + } + + private AddAudioMetadataRequestWithStorageGUID createAddAudioRequest(OffsetDateTime startedAt, + OffsetDateTime endedAt, String courthouse, String courtroom) throws IOException { + return createAddAudioRequest(startedAt, endedAt, courthouse, courtroom, + "mp2", AUDIO_BINARY_PAYLOAD_1, "case1", "case2", "case3"); + } + + + private AddAudioMetadataRequest createAddAudioRequest(OffsetDateTime startedAt, + OffsetDateTime endedAt, + String courthouse, String courtroom, Path audioBinaryPayload) throws IOException { + return createAddAudioRequest(startedAt, endedAt, courthouse, courtroom, "mp2", audioBinaryPayload, "case1", "case2", "case3"); + } + + private AddAudioMetadataRequest createAddAudioRequest(OffsetDateTime startedAt, OffsetDateTime endedAt, + String courthouse, String courtroom, Path audioBinaryPayload, + String... casesList) throws IOException { + return createAddAudioRequest(startedAt, endedAt, courthouse, courtroom, + "mp2", audioBinaryPayload, casesList); + } + + private AddAudioMetadataRequestWithStorageGUID createAddAudioRequest(OffsetDateTime startedAt, OffsetDateTime endedAt, + String courthouse, String courtroom, String filetype, Path audioBinaryPayload, + String... casesList) throws IOException { + + AddAudioMetadataRequestWithStorageGUID addAudioMetadataRequest = new AddAudioMetadataRequestWithStorageGUID(); + addAudioMetadataRequest.startedAt(startedAt); + addAudioMetadataRequest.endedAt(endedAt); + addAudioMetadataRequest.setChannel(1); + addAudioMetadataRequest.totalChannels(2); + addAudioMetadataRequest.format(filetype); + addAudioMetadataRequest.filename("test"); + addAudioMetadataRequest.courthouse(courthouse); + addAudioMetadataRequest.courtroom(courtroom); + addAudioMetadataRequest.cases(List.of(casesList)); + addAudioMetadataRequest.setMediaFile("media file"); + addAudioMetadataRequest.setFileSize(Files.size(audioBinaryPayload)); + addAudioMetadataRequest.setChecksum("calculatedchecksum"); + addAudioMetadataRequest.storageGuid(guid); + addAudioMetadataRequest.setChecksum("checksum-" + guid); + return addAudioMetadataRequest; + } + + @SuppressWarnings({"PMD.SignatureDeclareThrowsException"}) + private ResultActions makeAddAudioCall() throws Exception { + return makeAddAudioCall(AUDIO_BINARY_PAYLOAD_1); + } + + @SuppressWarnings({"PMD.SignatureDeclareThrowsException"}) + private ResultActions makeAddAudioCall(Path audioBinaryPayload, String... casesToMapTo) throws Exception { + UserAccountEntity testUser = authorisationStub.getSystemUser(); + dartsDatabase.getUserAccountRepository().save(testUser); + + dartsDatabase.createCase("Bristol", "case1"); + dartsDatabase.createCase("Bristol", "case2"); + dartsDatabase.createCase("Bristol", "case3"); + + HearingEntity hearingForEvent = hearingStub.createHearing("Bristol", "1", "case1", DateConverterUtil.toLocalDateTime(STARTED_AT)); + eventStub.createEvent(hearingForEvent, 10, STARTED_AT.minusMinutes(20), "LOG"); + HearingEntity hearingDifferentCourtroom = hearingStub.createHearing("Bristol", "2", "case2", DateConverterUtil.toLocalDateTime(STARTED_AT)); + eventStub.createEvent(hearingDifferentCourtroom, 10, STARTED_AT.minusMinutes(20), "LOG"); + HearingEntity hearingAfter = hearingStub.createHearing("Bristol", "1", "case3", DateConverterUtil.toLocalDateTime(STARTED_AT)); + eventStub.createEvent(hearingAfter, 10, STARTED_AT.plusMinutes(20), "LOG"); + + AddAudioMetadataRequest addAudioMetadataRequest; + if (casesToMapTo.length == 0) { + addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT, "Bristol", "1", audioBinaryPayload); + } else { + addAudioMetadataRequest = createAddAudioRequest(STARTED_AT, STARTED_AT, "Bristol", "1", audioBinaryPayload, casesToMapTo); + } + + return mockMvc.perform( + post(ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addAudioMetadataRequest))); + } + + @SuppressWarnings({"PMD.SignatureDeclareThrowsException"}) + private Integer uploadAnotherAudioWithSize(Path audioBinaryPayload, String expectedAntecedantId, String expectedChronicleId) throws Exception { + makeAddAudioCall(audioBinaryPayload) + .andExpect(status().isOk()); + + List allHearings = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate("bristol", "1", STARTED_AT.toLocalDate()); + + List addAudioLinkedHearings = new ArrayList<>(); + for (HearingEntity hearing : allHearings) { + if (hearing.getCourtCase().getCaseNumber().contains("case")) { + addAudioLinkedHearings.add(hearing); + } + } + assertEquals(3, addAudioLinkedHearings.size()); + + MediaEntity media = null; + for (HearingEntity hearing : addAudioLinkedHearings) { + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearing.getId()); + + assertEquals(1, mediaEntities.size()); + + media = mediaEntities.get(0); + assertEquals(STARTED_AT, media.getStart()); + assertEquals(STARTED_AT, media.getEnd()); + assertEquals(1, media.getChannel()); + assertEquals(2, media.getTotalChannels()); + List mediaLinkedCaseEntities = dartsDatabase.getMediaLinkedCaseRepository().findByMedia(media); + assertEquals(3, mediaLinkedCaseEntities.size()); + assertEquals("1", dartsDatabase.getCourtroomRepository().findById(media.getCourtroom().getId()).get().getName()); + assertEquals(expectedChronicleId, media.getChronicleId()); + assertEquals(expectedAntecedantId, media.getAntecedentId()); + } + + List hearingsInAnotherCourtroom = dartsDatabase.getHearingRepository().findByCourthouseCourtroomAndDate( + "bristol", + "2", + STARTED_AT.toLocalDate() + ); + assertEquals(1, hearingsInAnotherCourtroom.size());//should have hearingDifferentCourtroom + HearingEntity hearingEntity = hearingsInAnotherCourtroom.get(0); + List mediaEntities = dartsDatabase.getMediaRepository().findAllCurrentMediaByHearingId(hearingEntity.getId()); + assertEquals(0, mediaEntities.size());//shouldn't have any as no audio in that courtroom + assertFalse(Objects.requireNonNull(LogUtil.getMemoryLogger()) + .searchLogs("Revised version of media added", toLevel( + Level.INFO_INT)).isEmpty()); + + return media.getId(); + } + +} \ No newline at end of file diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataIntTest.java index 6fd8edc13b..c3b18b86e3 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/audio/controller/AudioControllerAddAudioMetadataIntTest.java @@ -447,7 +447,7 @@ void addAudioUnsupportedType() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(problem.getType().toString(), AudioApiError.UNEXPECTED_FILE_TYPE.getType().toString()); + assertEquals(problem.getType(), AudioApiError.UNEXPECTED_FILE_TYPE.getType()); assertEquals(problem.getTitle(), AudioApiError.UNEXPECTED_FILE_TYPE.getTitle()); } @@ -481,7 +481,7 @@ void addAudioNotProvided() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.AUDIO_NOT_PROVIDED.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.AUDIO_NOT_PROVIDED.getType(), problem.getType()); assertEquals(AudioApiError.AUDIO_NOT_PROVIDED.getTitle(), problem.getTitle()); } @@ -515,7 +515,7 @@ void addAudioDurationOutOfBounds() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.FILE_DURATION_OUT_OF_BOUNDS.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.FILE_DURATION_OUT_OF_BOUNDS.getType(), problem.getType()); assertEquals(AudioApiError.FILE_DURATION_OUT_OF_BOUNDS.getTitle(), problem.getTitle()); } @@ -562,7 +562,7 @@ public InputStream getInputStream() throws IOException { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.FAILED_TO_UPLOAD_AUDIO_FILE.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.FAILED_TO_UPLOAD_AUDIO_FILE.getType(), problem.getType()); assertEquals(AudioApiError.FAILED_TO_UPLOAD_AUDIO_FILE.getTitle(), problem.getTitle()); } @@ -599,7 +599,7 @@ void addAudioSizeOutsideOfBoundaries() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.FILE_SIZE_OUT_OF_BOUNDS.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.FILE_SIZE_OUT_OF_BOUNDS.getType(), problem.getType()); assertEquals(AudioApiError.FILE_SIZE_OUT_OF_BOUNDS.getTitle(), problem.getTitle()); } @@ -633,7 +633,7 @@ void addAudioFileExtensionIncorrect() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType(), problem.getType()); assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getTitle(), problem.getTitle()); } @@ -667,7 +667,7 @@ void addAudioFileExtensionContentType() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType(), problem.getType()); assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getTitle(), problem.getTitle()); } @@ -701,7 +701,7 @@ void addAudioFileSignatureException() throws Exception { String actualJson = mvcResult.getResponse().getContentAsString(); Problem problem = objectMapper.readValue(actualJson, Problem.class); - assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType().toString(), problem.getType().toString()); + assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getType(), problem.getType()); assertEquals(AudioApiError.UNEXPECTED_FILE_TYPE.getTitle(), problem.getTitle()); } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerGetCaseHearingsTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerGetCaseHearingsTest.java index 5b4c25f5c1..2ebdd614b0 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerGetCaseHearingsTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerGetCaseHearingsTest.java @@ -185,7 +185,7 @@ void casesSearchWithInactiveUser() throws Exception { MockHttpServletRequestBuilder requestBuilder = get(endpointUrl, hearingEntity.getCourtCase().getId()); mockMvc.perform(requestBuilder).andExpect(status().isForbidden()).andExpect(jsonPath("$.type").value( - AuthorisationError.USER_DETAILS_INVALID.getType().toString())); + AuthorisationError.USER_DETAILS_INVALID.getType())); } } \ No newline at end of file diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerSearchPostTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerSearchPostTest.java index df9ad10632..a800acf580 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerSearchPostTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/cases/controller/CaseControllerSearchPostTest.java @@ -436,7 +436,7 @@ void casesSearchPostEndpointJudgeNameInactive() throws Exception { .contentType(MediaType.APPLICATION_JSON_VALUE) .content(requestBody); mockMvc.perform(requestBuilder).andExpect(status().isForbidden()).andExpect(jsonPath("$.type").value( - AuthorisationError.USER_DETAILS_INVALID.getType().toString())); + AuthorisationError.USER_DETAILS_INVALID.getType())); } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/common/controller/CourthouseApiTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/common/controller/CourthouseApiTest.java index 4c6902131f..1997bc325c 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/common/controller/CourthouseApiTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/common/controller/CourthouseApiTest.java @@ -296,7 +296,7 @@ void courthousesGet_ThreeCourthousesAssignedToUserInactive() throws Exception { MockHttpServletRequestBuilder requestBuilder = get("/courthouses") .contentType(MediaType.APPLICATION_JSON_VALUE); mockMvc.perform(requestBuilder).andExpect(status().isForbidden()).andExpect(jsonPath("$.type").value( - AuthorisationError.USER_DETAILS_INVALID.getType().toString())); + AuthorisationError.USER_DETAILS_INVALID.getType())); } @Test diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/IntegrationTestConfiguration.java b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/IntegrationTestConfiguration.java index a7a66404d3..27c598ed47 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/IntegrationTestConfiguration.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/IntegrationTestConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Primary; import uk.gov.hmcts.darts.arm.service.ArmService; import uk.gov.hmcts.darts.common.datamanagement.component.DataManagementAzureClientFactory; +import uk.gov.hmcts.darts.common.util.FileContentChecksum; import uk.gov.hmcts.darts.datamanagement.config.DataManagementConfiguration; import uk.gov.hmcts.darts.datamanagement.service.DataManagementService; import uk.gov.hmcts.darts.datamanagement.service.impl.DataManagementServiceImpl; @@ -31,12 +32,13 @@ public ArmService armService() { @Primary public DataManagementService getDartsDataManagementService(DataManagementConfiguration configuration, DataManagementAzureClientFactory factory, - AzureCopyUtil azureCopyUtil) { + AzureCopyUtil azureCopyUtil, + FileContentChecksum fileContentChecksum) { String[] profiles = applicationContext.getEnvironment().getActiveProfiles(); - for (String profile: profiles) { + for (String profile : profiles) { if (BLOB_STORAGE_PROFILE.equals(profile)) { - return new DataManagementServiceImpl(configuration, factory, azureCopyUtil); + return new DataManagementServiceImpl(configuration, factory, azureCopyUtil, fileContentChecksum); } } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/DataManagementServiceStubImpl.java b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/DataManagementServiceStubImpl.java index f40ee08a4e..7c189b36b0 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/DataManagementServiceStubImpl.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/testutils/stubs/DataManagementServiceStubImpl.java @@ -122,6 +122,11 @@ public void addMetaData(BlobClient client, Map metadata) { } + @Override + public String getChecksum(String containerName, UUID blobId) { + return "checksum-" + blobId; + } + @Override public Response deleteBlobData(String containerName, UUID blobId) { logStubUsageWarning(); diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/transcriptions/controller/TranscriptionControllerGetTranscriptionTranscriberCountsIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/transcriptions/controller/TranscriptionControllerGetTranscriptionTranscriberCountsIntTest.java index eb26ce7a9f..311ef64339 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/transcriptions/controller/TranscriptionControllerGetTranscriptionTranscriberCountsIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/transcriptions/controller/TranscriptionControllerGetTranscriptionTranscriberCountsIntTest.java @@ -177,7 +177,7 @@ void getTranscriberCountsShouldReturnOkWithInactive() throws Exception { mockMvc.perform(requestBuilder) .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.type").value(TranscriptionApiError.USER_NOT_TRANSCRIBER.getType().toString())) + .andExpect(jsonPath("$.type").value(TranscriptionApiError.USER_NOT_TRANSCRIBER.getType())) .andReturn(); } diff --git a/src/integrationTest/java/uk/gov/hmcts/darts/usermanagement/controller/PatchSecurityGroupIntTest.java b/src/integrationTest/java/uk/gov/hmcts/darts/usermanagement/controller/PatchSecurityGroupIntTest.java index 802e991c89..69ededeee4 100644 --- a/src/integrationTest/java/uk/gov/hmcts/darts/usermanagement/controller/PatchSecurityGroupIntTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/darts/usermanagement/controller/PatchSecurityGroupIntTest.java @@ -222,7 +222,7 @@ void patchSecurityGroupShouldFailWhenProvidedButInactive() throws Exception { mockMvc.perform(patchRequest) .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.type").value(UserManagementError.USER_NOT_FOUND.getType().toString())) + .andExpect(jsonPath("$.type").value(UserManagementError.USER_NOT_FOUND.getType())) .andReturn(); } diff --git a/src/main/java/uk/gov/hmcts/darts/audio/controller/AudioController.java b/src/main/java/uk/gov/hmcts/darts/audio/controller/AudioController.java index 5f3c618c79..748845257c 100644 --- a/src/main/java/uk/gov/hmcts/darts/audio/controller/AudioController.java +++ b/src/main/java/uk/gov/hmcts/darts/audio/controller/AudioController.java @@ -1,12 +1,15 @@ package uk.gov.hmcts.darts.audio.controller; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import uk.gov.hmcts.darts.audio.component.AudioResponseMapper; @@ -14,6 +17,7 @@ import uk.gov.hmcts.darts.audio.http.api.AudioApi; import uk.gov.hmcts.darts.audio.mapper.TransformedMediaMapper; import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; +import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequestWithStorageGUID; import uk.gov.hmcts.darts.audio.model.AdminMediaResponse; import uk.gov.hmcts.darts.audio.model.AudioMetadata; import uk.gov.hmcts.darts.audio.model.AudioPreview; @@ -101,6 +105,20 @@ public ResponseEntity addAudio(MultipartFile file, AddAudioMetadataRequest return new ResponseEntity<>(HttpStatus.OK); } + @Override + @SecurityRequirement(name = SECURITY_SCHEMES_BEARER_AUTH) + @Authorisation(contextId = ANY_ENTITY_ID, + globalAccessSecurityRoles = {MID_TIER}) + public ResponseEntity addAudioMetaData( + @Parameter(name = "AddAudioMetadataRequestWithStorageGUID", description = "") @Valid @RequestBody(required = false) + AddAudioMetadataRequestWithStorageGUID metadata) { + + // validate the payloads + addAudioMetaDataValidator.validate(metadata); + audioUploadService.addAudio(metadata.getStorageGuid(), metadata); + return new ResponseEntity<>(HttpStatus.OK); + } + @SneakyThrows @Override @SecurityRequirement(name = SECURITY_SCHEMES_BEARER_AUTH) diff --git a/src/main/java/uk/gov/hmcts/darts/audio/exception/AudioApiError.java b/src/main/java/uk/gov/hmcts/darts/audio/exception/AudioApiError.java index 8a2670409c..2dd75e535e 100644 --- a/src/main/java/uk/gov/hmcts/darts/audio/exception/AudioApiError.java +++ b/src/main/java/uk/gov/hmcts/darts/audio/exception/AudioApiError.java @@ -110,6 +110,11 @@ public enum AudioApiError implements DartsApiError { AddAudioErrorCode.USER_CANT_APPROVE_THEIR_OWN_DELETION.getValue(), HttpStatus.BAD_REQUEST, AddAudioTitleErrors.USER_CANT_APPROVE_THEIR_OWN_DELETION.getValue() + ), + FAILED_TO_ADD_AUDIO_META_DATA( + AddAudioErrorCode.FAILED_TO_ADD_AUDIO_META_DATA.getValue(), + HttpStatus.UNPROCESSABLE_ENTITY, + AddAudioTitleErrors.FAILED_TO_ADD_AUDIO_META_DATA.getValue() ); diff --git a/src/main/java/uk/gov/hmcts/darts/audio/service/AudioUploadService.java b/src/main/java/uk/gov/hmcts/darts/audio/service/AudioUploadService.java index 0bd5209bc9..10d425e5b8 100644 --- a/src/main/java/uk/gov/hmcts/darts/audio/service/AudioUploadService.java +++ b/src/main/java/uk/gov/hmcts/darts/audio/service/AudioUploadService.java @@ -4,8 +4,12 @@ import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; import uk.gov.hmcts.darts.common.entity.MediaEntity; +import java.util.UUID; + public interface AudioUploadService { void addAudio(MultipartFile audioFileStream, AddAudioMetadataRequest addAudioMetadataRequest); + void addAudio(UUID guid, AddAudioMetadataRequest addAudioMetadataRequest); + void linkAudioToHearingInMetadata(AddAudioMetadataRequest addAudioMetadataRequest, MediaEntity mediaEntity); } \ No newline at end of file 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 54025a1084..e4b1be21f4 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 @@ -6,10 +6,12 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import uk.gov.hmcts.darts.audio.component.AddAudioRequestMapper; +import uk.gov.hmcts.darts.audio.exception.AudioApiError; import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; import uk.gov.hmcts.darts.audio.service.AudioAsyncService; import uk.gov.hmcts.darts.audio.service.AudioUploadService; import uk.gov.hmcts.darts.authorisation.component.UserIdentity; +import uk.gov.hmcts.darts.common.datamanagement.enums.DatastoreContainerType; import uk.gov.hmcts.darts.common.entity.CourtCaseEntity; import uk.gov.hmcts.darts.common.entity.CourtroomEntity; import uk.gov.hmcts.darts.common.entity.ExternalObjectDirectoryEntity; @@ -40,6 +42,7 @@ import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.apache.commons.collections4.CollectionUtils.isEmpty; @@ -67,9 +70,32 @@ public class AudioUploadServiceImpl implements AudioUploadService { private final MediaLinkedCaseRepository mediaLinkedCaseRepository; private final AudioAsyncService audioAsyncService; + @Override public void addAudio(MultipartFile audioMultipartFile, AddAudioMetadataRequest addAudioMetadataRequest) { + String incomingChecksum; + try { + incomingChecksum = fileContentChecksum.calculate(audioMultipartFile.getInputStream()); + } catch (IOException e) { + throw new DartsApiException(FAILED_TO_UPLOAD_AUDIO_FILE, "Failed to compute incoming checksum", e); + } + addAudio(incomingChecksum, () -> saveAudioToInbound(audioMultipartFile), addAudioMetadataRequest); + } + @Override + public void addAudio(UUID guid, AddAudioMetadataRequest addAudioMetadataRequest) { + String checksum = dataManagementApi.getChecksum(DatastoreContainerType.INBOUND, guid); + if (!checksum.equals(addAudioMetadataRequest.getChecksum())) { + throw new DartsApiException(AudioApiError.FAILED_TO_ADD_AUDIO_META_DATA, + String.format("Checksum in DETs (%s) does not match the one passed in the API request (%s).", + checksum, addAudioMetadataRequest.getChecksum())); + } + addAudio(checksum, () -> guid, addAudioMetadataRequest); + } + + private void addAudio(String incomingChecksum, + Supplier externalLocationSupplier, + AddAudioMetadataRequest addAudioMetadataRequest) { log.info("Adding audio using metadata {}", addAudioMetadataRequest.toString()); //remove duplicate cases as they can appear more than once, e.g. if they broke for lunch. @@ -78,14 +104,7 @@ public void addAudio(MultipartFile audioMultipartFile, AddAudioMetadataRequest a List duplicatesToBeSuperseded = getLatestDuplicateMediaFiles(addAudioMetadataRequest); - String incomingChecksum; - try { - incomingChecksum = fileContentChecksum.calculate(audioMultipartFile.getInputStream()); - } catch (IOException e) { - throw new DartsApiException(FAILED_TO_UPLOAD_AUDIO_FILE, "Failed to compute incoming checksum", e); - } List duplicatesWithDifferentChecksum = filterForMediaWithMismatchingChecksum(duplicatesToBeSuperseded, incomingChecksum); - if (isNotEmpty(duplicatesToBeSuperseded) && isEmpty(duplicatesWithDifferentChecksum)) { log.info("Exact duplicate detected based upon media metadata and checksum. Returning 200 with no changes "); for (MediaEntity entity : duplicatesToBeSuperseded) { @@ -96,7 +115,7 @@ public void addAudio(MultipartFile audioMultipartFile, AddAudioMetadataRequest a // upload to the blob store ObjectRecordStatusEntity objectRecordStatusEntity = objectRecordStatusRepository.getReferenceById(STORED.getId()); - UUID externalLocation = saveAudioToInbound(audioMultipartFile); + UUID externalLocation = externalLocationSupplier.get(); UserAccountEntity currentUser = userIdentity.getUserAccount(); diff --git a/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiError.java b/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiError.java index 7dd89300b5..de825b6d56 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiError.java +++ b/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiError.java @@ -2,8 +2,6 @@ import org.springframework.http.HttpStatus; -import java.net.URI; - public interface DartsApiError { String getErrorTypePrefix(); @@ -14,10 +12,7 @@ public interface DartsApiError { String getTitle(); - default URI getType() { - return URI.create( - getErrorTypeNumeric() - ); + default String getType() { + return getErrorTypeNumeric(); } - } diff --git a/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiTrait.java b/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiTrait.java index 97222239b1..abe535ed1e 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiTrait.java +++ b/src/main/java/uk/gov/hmcts/darts/common/exception/DartsApiTrait.java @@ -10,6 +10,7 @@ import org.zalando.problem.spring.common.HttpStatusAdapter; import org.zalando.problem.spring.web.advice.AdviceTrait; +import java.net.URI; import java.util.Map.Entry; public interface DartsApiTrait extends AdviceTrait { @@ -25,7 +26,7 @@ default ResponseEntity handleDartsApiException(DartsApiException except HttpStatusAdapter problemHttpStatus = new HttpStatusAdapter(error.getHttpStatus()); ProblemBuilder problemBuilder = Problem.builder() - .withType(error.getType()) + .withType(URI.create(error.getType())) .withStatus(problemHttpStatus) .withTitle(error.getTitle()) .withDetail(exception.getDetail()); diff --git a/src/main/java/uk/gov/hmcts/darts/common/util/FileContentChecksum.java b/src/main/java/uk/gov/hmcts/darts/common/util/FileContentChecksum.java index af12ace8bc..9308c28300 100644 --- a/src/main/java/uk/gov/hmcts/darts/common/util/FileContentChecksum.java +++ b/src/main/java/uk/gov/hmcts/darts/common/util/FileContentChecksum.java @@ -41,7 +41,7 @@ public String calculate(Path filePath) { return calculate(new FileInputStream(filePath.toFile())); } - protected String encodeToString(byte[] bytes) { + public String encodeToString(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); diff --git a/src/main/java/uk/gov/hmcts/darts/datamanagement/api/DataManagementApi.java b/src/main/java/uk/gov/hmcts/darts/datamanagement/api/DataManagementApi.java index 1100cf2027..5de2bcf78d 100644 --- a/src/main/java/uk/gov/hmcts/darts/datamanagement/api/DataManagementApi.java +++ b/src/main/java/uk/gov/hmcts/darts/datamanagement/api/DataManagementApi.java @@ -35,4 +35,5 @@ public interface DataManagementApi extends BlobContainerDownloadable { UUID saveBlobDataToUnstructuredContainer(BinaryData binaryData); + String getChecksum(DatastoreContainerType datastoreContainerType, UUID guid); } diff --git a/src/main/java/uk/gov/hmcts/darts/datamanagement/api/impl/DataManagementApiImpl.java b/src/main/java/uk/gov/hmcts/darts/datamanagement/api/impl/DataManagementApiImpl.java index 118a975fa9..b780852273 100644 --- a/src/main/java/uk/gov/hmcts/darts/datamanagement/api/impl/DataManagementApiImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/datamanagement/api/impl/DataManagementApiImpl.java @@ -41,9 +41,7 @@ public BlobClient saveBlobDataToContainer(BinaryData binaryData, DatastoreContai @Override public BlobClientUploadResponse saveBlobToContainer(InputStream inputStream, DatastoreContainerType container, Map metadata) { - String containerName = getContainerName(container) - .orElseThrow(() -> new IllegalArgumentException("Container name cannot be resolved")); - + String containerName = getContainerNameRequired(container); return dataManagementService.saveBlobData(containerName, inputStream, metadata); } @@ -86,6 +84,11 @@ public UUID saveBlobDataToUnstructuredContainer(BinaryData binaryData) { return dataManagementService.saveBlobData(getUnstructuredContainerName(), binaryData); } + @Override + public String getChecksum(DatastoreContainerType datastoreContainerType, UUID guid) { + return dataManagementService.getChecksum(getContainerNameRequired(datastoreContainerType), guid); + } + private String getOutboundContainerName() { return dataManagementConfiguration.getOutboundContainerName(); } @@ -108,6 +111,12 @@ public DownloadResponseMetaData downloadBlobFromContainer(DatastoreContainerType throw new FileNotDownloadedException(externalObjectDirectoryEntity.getExternalLocation(), container.name(), "Container not found."); } + + public String getContainerNameRequired(DatastoreContainerType datastoreContainerType) { + return getContainerName(datastoreContainerType) + .orElseThrow(() -> new IllegalArgumentException("Container name cannot be resolved")); + } + @Override public Optional getContainerName(DatastoreContainerType datastoreContainerType) { switch (datastoreContainerType) { diff --git a/src/main/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementService.java b/src/main/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementService.java index d0d8c6dda6..05bf004559 100644 --- a/src/main/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementService.java +++ b/src/main/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementService.java @@ -31,6 +31,8 @@ public interface DataManagementService { void addMetaData(BlobClient client, Map metadata); + String getChecksum(String containerName, UUID blobId); + Response deleteBlobData(String containerName, UUID blobId) throws AzureDeleteBlobException; DownloadResponseMetaData downloadData(DatastoreContainerType type, String containerName, UUID blobId) throws FileNotDownloadedException; diff --git a/src/main/java/uk/gov/hmcts/darts/datamanagement/service/impl/DataManagementServiceImpl.java b/src/main/java/uk/gov/hmcts/darts/datamanagement/service/impl/DataManagementServiceImpl.java index 1c84406684..0ae8cf6970 100644 --- a/src/main/java/uk/gov/hmcts/darts/datamanagement/service/impl/DataManagementServiceImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/datamanagement/service/impl/DataManagementServiceImpl.java @@ -18,7 +18,10 @@ import uk.gov.hmcts.darts.common.datamanagement.component.impl.FileBasedDownloadResponseMetaData; import uk.gov.hmcts.darts.common.datamanagement.enums.DatastoreContainerType; import uk.gov.hmcts.darts.common.exception.AzureDeleteBlobException; +import uk.gov.hmcts.darts.common.exception.CommonApiError; +import uk.gov.hmcts.darts.common.exception.DartsApiException; import uk.gov.hmcts.darts.common.exception.DartsException; +import uk.gov.hmcts.darts.common.util.FileContentChecksum; import uk.gov.hmcts.darts.datamanagement.config.DataManagementConfiguration; import uk.gov.hmcts.darts.datamanagement.exception.FileNotDownloadedException; import uk.gov.hmcts.darts.datamanagement.model.BlobClientUploadResponseImpl; @@ -50,6 +53,7 @@ public class DataManagementServiceImpl implements DataManagementService { private final DataManagementAzureClientFactory blobServiceFactory; private final AzureCopyUtil azureCopyUtil; + private final FileContentChecksum fileContentChecksum; /** @@ -204,6 +208,27 @@ public DownloadResponseMetaData downloadData(DatastoreContainerType type, String return downloadResponse; } + @Override + public String getChecksum(String containerName, UUID blobId) { + BlobServiceClient serviceClient = blobServiceFactory.getBlobServiceClient(dataManagementConfiguration.getBlobStorageAccountConnectionString()); + BlobContainerClient containerClient = blobServiceFactory.getBlobContainerClient(containerName, serviceClient); + BlobClient blobClient = blobServiceFactory.getBlobClient(containerClient, blobId); + + boolean exists = blobClient.exists() != null && blobClient.exists(); + + if (!exists) { + throw new DartsApiException(CommonApiError.NOT_FOUND, + String.format("Blob %s does not exist in container %s was not found.", blobId, containerName)); + } + byte[] checksumByte = blobClient.getProperties().getContentMd5(); + + if (checksumByte == null) { + throw new DartsApiException(CommonApiError.NOT_FOUND, + String.format("Blob %s does not exist in container %s does not contain a checksum.", blobId, containerName)); + } + return fileContentChecksum.encodeToString(checksumByte); + } + @Override public Response deleteBlobData(String containerName, UUID blobId) throws AzureDeleteBlobException { diff --git a/src/main/java/uk/gov/hmcts/darts/dets/api/DetsDataManagementApi.java b/src/main/java/uk/gov/hmcts/darts/dets/api/DetsDataManagementApi.java index 80e2815384..fda1c0f19f 100644 --- a/src/main/java/uk/gov/hmcts/darts/dets/api/DetsDataManagementApi.java +++ b/src/main/java/uk/gov/hmcts/darts/dets/api/DetsDataManagementApi.java @@ -9,6 +9,8 @@ public interface DetsDataManagementApi extends BlobContainerDownloadable { void deleteBlobDataFromContainer(UUID blobId) throws AzureDeleteBlobException; + String getChecksum(UUID guid); + void copyDetsBlobDataToArm(String detsUuid, String blobPathAndName); } \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/darts/dets/api/impl/DetsDataManagementApiImpl.java b/src/main/java/uk/gov/hmcts/darts/dets/api/impl/DetsDataManagementApiImpl.java index a261b9139c..a43189b1d4 100644 --- a/src/main/java/uk/gov/hmcts/darts/dets/api/impl/DetsDataManagementApiImpl.java +++ b/src/main/java/uk/gov/hmcts/darts/dets/api/impl/DetsDataManagementApiImpl.java @@ -8,6 +8,7 @@ import uk.gov.hmcts.darts.common.entity.ExternalObjectDirectoryEntity; import uk.gov.hmcts.darts.common.exception.AzureDeleteBlobException; import uk.gov.hmcts.darts.datamanagement.exception.FileNotDownloadedException; +import uk.gov.hmcts.darts.datamanagement.service.DataManagementService; import uk.gov.hmcts.darts.dets.api.DetsDataManagementApi; import uk.gov.hmcts.darts.dets.config.DetsDataManagementConfiguration; import uk.gov.hmcts.darts.dets.service.DetsApiService; @@ -23,6 +24,8 @@ public class DetsDataManagementApiImpl implements DetsDataManagementApi { private final DetsApiService service; private final DetsDataManagementConfiguration detsManagementConfiguration; + private final DataManagementService dataManagementService; + @Override public DownloadResponseMetaData downloadBlobFromContainer(DatastoreContainerType container, @@ -52,6 +55,11 @@ public void deleteBlobDataFromContainer(UUID blobId) throws AzureDeleteBlobExcep service.deleteBlobDataFromContainer(blobId); } + @Override + public String getChecksum(UUID guid) { + return dataManagementService.getChecksum(detsManagementConfiguration.getContainerName(), guid); + } + @Override public void copyDetsBlobDataToArm(String detsUuid, String blobPathAndName) { service.copyDetsBlobDataToArm(detsUuid, blobPathAndName); diff --git a/src/main/resources/openapi/audio.yaml b/src/main/resources/openapi/audio.yaml index f1a653b18c..757d44e2f9 100644 --- a/src/main/resources/openapi/audio.yaml +++ b/src/main/resources/openapi/audio.yaml @@ -178,6 +178,32 @@ paths: application/json+problem: schema: $ref: './problem.yaml' + /audios/metadata: + post: + tags: + - Audio + summary: Upload audio metadata + operationId: addAudioMetaData + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddAudioMetadataRequestWithStorageGUID' + responses: + 200: + description: Audio metadata accepted + 400: + description: Bad request + content: + application/json+problem: + schema: + $ref: './problem.yaml' + 500: + description: Internal server error + content: + application/json+problem: + schema: + $ref: './problem.yaml' /admin/transformed-medias/{id}: get: @@ -683,6 +709,16 @@ components: file_size: type: integer format: int64 + AddAudioMetadataRequestWithStorageGUID: + allOf: # Combines the BasicErrorModel and the inline model + - $ref: "#/components/schemas/AddAudioMetadataRequest" + - type: object + required: + - storage_guid + properties: + storage_guid: + type: string + format: uuid AddAudioMetadataRequest: type: object required: @@ -1000,6 +1036,7 @@ components: - "AUDIO_118" - "AUDIO_119" - "AUDIO_120" + - "AUDIO_121" x-enum-varnames: [ FAILED_TO_PROCESS_AUDIO_REQUEST, REQUESTED_DATA_CANNOT_BE_LOCATED, MEDIA_NOT_FOUND, @@ -1019,7 +1056,8 @@ components: MEDIA_ALREADY_MARKED_FOR_DELETION, ADMIN_MEDIA_MARKED_FOR_DELETION_NOT_FOUND, MARKED_FOR_DELETION_REASON_NOT_FOUND, - USER_CANT_APPROVE_THEIR_OWN_DELETION + USER_CANT_APPROVE_THEIR_OWN_DELETION, + FAILED_TO_ADD_AUDIO_META_DATA, ] AddAudioTitleErrors: @@ -1045,6 +1083,7 @@ components: - "Media marked for deletion not found" - "Media marked for deletion reason not found" - "User cannot approve their own deletion" + - "Failed to add audio meta data" x-enum-varnames: [ FAILED_TO_PROCESS_AUDIO_REQUEST, REQUESTED_DATA_CANNOT_BE_LOCATED, MEDIA_NOT_FOUND, @@ -1064,5 +1103,6 @@ components: MEDIA_ALREADY_MARKED_FOR_DELETION, ADMIN_MEDIA_MARKED_FOR_DELETION_NOT_FOUND, MARKED_FOR_DELETION_REASON_NOT_FOUND, - USER_CANT_APPROVE_THEIR_OWN_DELETION + USER_CANT_APPROVE_THEIR_OWN_DELETION, + FAILED_TO_ADD_AUDIO_META_DATA, ] \ No newline at end of file diff --git a/src/main/resources/openapi/problem.yaml b/src/main/resources/openapi/problem.yaml index 03043aeb23..b369e093f7 100644 --- a/src/main/resources/openapi/problem.yaml +++ b/src/main/resources/openapi/problem.yaml @@ -1,8 +1,7 @@ type: object properties: type: - type: string - format: uri + type: object description: | A URI that identifies the unique error code representing this problem. Consumers can rely on this value to be stable. diff --git a/src/test/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImplTest.java index c555a90872..218a27bd89 100644 --- a/src/test/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/audio/service/impl/AudioUploadServiceImplTest.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.darts.audio.service.impl; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,16 +12,19 @@ import uk.gov.hmcts.darts.audio.component.AddAudioRequestMapper; import uk.gov.hmcts.darts.audio.component.impl.AddAudioRequestMapperImpl; import uk.gov.hmcts.darts.audio.config.AudioConfigurationProperties; +import uk.gov.hmcts.darts.audio.exception.AudioApiError; import uk.gov.hmcts.darts.audio.model.AddAudioMetadataRequest; import uk.gov.hmcts.darts.audio.service.AudioAsyncService; import uk.gov.hmcts.darts.audio.service.AudioUploadService; import uk.gov.hmcts.darts.authorisation.component.UserIdentity; +import uk.gov.hmcts.darts.common.datamanagement.enums.DatastoreContainerType; import uk.gov.hmcts.darts.common.entity.CourthouseEntity; import uk.gov.hmcts.darts.common.entity.CourtroomEntity; import uk.gov.hmcts.darts.common.entity.ExternalObjectDirectoryEntity; import uk.gov.hmcts.darts.common.entity.HearingEntity; import uk.gov.hmcts.darts.common.entity.MediaEntity; import uk.gov.hmcts.darts.common.entity.UserAccountEntity; +import uk.gov.hmcts.darts.common.exception.DartsApiException; import uk.gov.hmcts.darts.common.helper.MediaLinkedCaseHelper; import uk.gov.hmcts.darts.common.repository.CourtLogEventRepository; import uk.gov.hmcts.darts.common.repository.ExternalLocationTypeRepository; @@ -48,6 +52,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -176,6 +181,86 @@ void addAudio() { assertEquals(externalLocation, externalObjectDirectoryEntity.getExternalLocation()); } + @Test + void addAudioNoUpload() { + UserAccountEntity userAccount = new UserAccountEntity(); + userAccount.setId(10); + when(userIdentity.getUserAccount()).thenReturn(userAccount); + + // Given + HearingEntity hearingEntity = new HearingEntity(); + when(retrieveCoreObjectService.retrieveOrCreateHearing( + anyString(), + anyString(), + anyString(), + any(), + any() + )).thenReturn(hearingEntity); + + CourthouseEntity courthouse = new CourthouseEntity(); + courthouse.setCourthouseName("SWANSEA"); + CourtroomEntity courtroomEntity = new CourtroomEntity(1, "1", courthouse); + when(retrieveCoreObjectService.retrieveOrCreateCourtroom(eq("SWANSEA"), eq("1"), any(UserAccountEntity.class))) + .thenReturn(courtroomEntity); + + OffsetDateTime startedAt = OffsetDateTime.now().minusHours(1); + OffsetDateTime endedAt = OffsetDateTime.now(); + + MediaEntity mediaEntity = createMediaEntity(startedAt, endedAt); + mediaEntity.setId(10); + when(mediaRepository.save(any(MediaEntity.class))).thenReturn(mediaEntity); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(startedAt, endedAt); + when(dataManagementApi.getChecksum(any(), any())) + .thenReturn(addAudioMetadataRequest.getChecksum()); + + // When + UUID externalLocation = UUID.randomUUID(); + audioService.addAudio(externalLocation, addAudioMetadataRequest); + + // Then + verify(mediaRepository, times(2)).save(mediaEntityArgumentCaptor.capture()); + verify(hearingRepository, times(3)).saveAndFlush(any()); + verify(logApi, times(1)).audioUploaded(addAudioMetadataRequest); + verify(externalObjectDirectoryRepository).save(externalObjectDirectoryEntityArgumentCaptor.capture()); + verify(dataManagementApi, times(1)).getChecksum(DatastoreContainerType.INBOUND, externalLocation); + MediaEntity savedMedia = mediaEntityArgumentCaptor.getValue(); + assertEquals(startedAt, savedMedia.getStart()); + assertEquals(endedAt, savedMedia.getEnd()); + assertEquals(1, savedMedia.getChannel()); + assertEquals(2, savedMedia.getTotalChannels()); + assertEquals("SWANSEA", savedMedia.getCourtroom().getCourthouse().getCourthouseName()); + assertEquals("1", savedMedia.getCourtroom().getName()); + + ExternalObjectDirectoryEntity externalObjectDirectoryEntity = externalObjectDirectoryEntityArgumentCaptor.getValue(); + assertEquals(savedMedia.getChecksum(), externalObjectDirectoryEntity.getChecksum()); + assertNotNull(externalObjectDirectoryEntity.getChecksum()); + assertEquals(externalLocation, externalObjectDirectoryEntity.getExternalLocation()); + } + + @Test + void addAudioNoUploadChecksumDoNotMatch() { + OffsetDateTime startedAt = OffsetDateTime.now().minusHours(1); + OffsetDateTime endedAt = OffsetDateTime.now(); + + AddAudioMetadataRequest addAudioMetadataRequest = createAddAudioRequest(startedAt, endedAt); + when(dataManagementApi.getChecksum(any(), any())) + .thenReturn("123"); + + // When + UUID externalLocation = UUID.randomUUID(); + + Assertions.assertThatThrownBy(() -> audioService.addAudio(externalLocation, addAudioMetadataRequest)) + .isInstanceOf(DartsApiException.class) + .hasMessage("Failed to add audio meta data. Checksum in DETs (123) does not match the one passed in the API request (123456).") + .hasFieldOrPropertyWithValue("error", AudioApiError.FAILED_TO_ADD_AUDIO_META_DATA); + + verify(dataManagementApi, times(1)).getChecksum(DatastoreContainerType.INBOUND, externalLocation); + + // Then + verifyNoInteractions(mediaRepository, hearingRepository, logApi, externalObjectDirectoryRepository); + } + private MediaEntity createMediaEntity(OffsetDateTime startedAt, OffsetDateTime endedAt) { MediaEntity mediaEntity = new MediaEntity(); mediaEntity.setStart(startedAt); @@ -199,6 +284,7 @@ private AddAudioMetadataRequest createAddAudioRequest(OffsetDateTime startedAt, addAudioMetadataRequest.courthouse("SWANSEA"); addAudioMetadataRequest.courtroom("1"); addAudioMetadataRequest.cases(List.of("1", "2", "3")); + addAudioMetadataRequest.checksum("123456"); addAudioMetadataRequest.setFileSize((long) DUMMY_FILE_CONTENT.length()); return addAudioMetadataRequest; } diff --git a/src/test/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementServiceImplTest.java index c399e16aa0..f60843c3d2 100644 --- a/src/test/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/datamanagement/service/DataManagementServiceImplTest.java @@ -5,6 +5,8 @@ import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobProperties; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,7 +19,10 @@ import uk.gov.hmcts.darts.common.datamanagement.component.DataManagementAzureClientFactory; import uk.gov.hmcts.darts.common.datamanagement.enums.DatastoreContainerType; import uk.gov.hmcts.darts.common.exception.AzureDeleteBlobException; +import uk.gov.hmcts.darts.common.exception.CommonApiError; +import uk.gov.hmcts.darts.common.exception.DartsApiException; import uk.gov.hmcts.darts.common.exception.DartsException; +import uk.gov.hmcts.darts.common.util.FileContentChecksum; import uk.gov.hmcts.darts.datamanagement.config.DataManagementConfiguration; import uk.gov.hmcts.darts.datamanagement.model.BlobClientUploadResponseImpl; import uk.gov.hmcts.darts.datamanagement.service.impl.DataManagementServiceImpl; @@ -39,6 +44,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -56,6 +62,8 @@ class DataManagementServiceImplTest { private DataManagementConfiguration dataManagementConfiguration; @Mock private AzureCopyUtil azureCopyUtil; + @Mock + private FileContentChecksum fileContentChecksum; @InjectMocks private DataManagementServiceImpl dataManagementService; private BlobContainerClient blobContainerClient; @@ -242,4 +250,132 @@ void testCopyDataRethrowsExceptionsAsDartsApiException() { UUID.randomUUID().toString(), UUID.randomUUID().toString())); } + + + @Test + void positiveGetChecksumTypical() { + final String checksum = "abc123"; + final byte[] checkSumBytes = checksum.getBytes(); + final UUID blobId = UUID.fromString("431318c8-97db-415c-b321-120c48f0ffe2"); + final String containerName = "container123"; + final String connectionString = "connectionString"; + + BlobServiceClient blobServiceClient = mock(BlobServiceClient.class); + BlobContainerClient blobContainerClient = mock(BlobContainerClient.class); + BlobProperties blobProperties = mock(BlobProperties.class); + when(blobProperties.getContentMd5()) + .thenReturn(checkSumBytes); + BlobClient blobClient = mock(BlobClient.class); + when(blobClient.getProperties()) + .thenReturn(blobProperties); + + when(dataManagementFactory.getBlobServiceClient(any())) + .thenReturn(blobServiceClient); + when(dataManagementFactory.getBlobContainerClient(any(), any())) + .thenReturn(blobContainerClient); + when(dataManagementFactory.getBlobClient(any(), any())) + .thenReturn(blobClient); + when(dataManagementConfiguration.getBlobStorageAccountConnectionString()) + .thenReturn(connectionString); + when(fileContentChecksum.encodeToString(checkSumBytes)) + .thenReturn(checksum); + + when(blobClient.exists()).thenReturn(true); + + + assertThat(dataManagementService.getChecksum(containerName, blobId)) + .isEqualTo(checksum); + + verify(dataManagementConfiguration, times(1)).getBlobStorageAccountConnectionString(); + verify(dataManagementFactory, times(1)).getBlobServiceClient(connectionString); + verify(dataManagementFactory, times(1)).getBlobContainerClient(containerName, blobServiceClient); + verify(dataManagementFactory, times(1)).getBlobClient(blobContainerClient, blobId); + verify(blobClient, times(2)).exists(); + verify(blobClient, times(1)).getProperties(); + verify(blobProperties, times(1)).getContentMd5(); + verify(fileContentChecksum, times(1)).encodeToString(checkSumBytes); + } + + @Test + void negativeGetChecksumFileNotFoundExistsFalse() { + assertGetChecksumNotFound(false); + } + + @Test + void negativeGetChecksumFileNotFoundExistsNull() { + assertGetChecksumNotFound(null); + } + + @Test + void negativeGetChecksumChecksumNotFound() { + final UUID blobId = UUID.fromString("431318c8-97db-415c-b321-120c48f0ffe2"); + final String containerName = "container123"; + final String connectionString = "connectionString"; + + BlobServiceClient blobServiceClient = mock(BlobServiceClient.class); + BlobContainerClient blobContainerClient = mock(BlobContainerClient.class); + BlobProperties blobProperties = mock(BlobProperties.class); + when(blobProperties.getContentMd5()).thenReturn(null); + BlobClient blobClient = mock(BlobClient.class); + when(blobClient.getProperties()).thenReturn(blobProperties); + + when(dataManagementFactory.getBlobServiceClient(any())) + .thenReturn(blobServiceClient); + when(dataManagementFactory.getBlobContainerClient(any(), any())) + .thenReturn(blobContainerClient); + when(dataManagementFactory.getBlobClient(any(), any())) + .thenReturn(blobClient); + when(dataManagementConfiguration.getBlobStorageAccountConnectionString()) + .thenReturn(connectionString); + when(blobClient.exists()).thenReturn(true); + + + Assertions.assertThatThrownBy(() -> dataManagementService.getChecksum(containerName, blobId)) + .isInstanceOf(DartsApiException.class) + .hasMessage("Resource not found. Blob 431318c8-97db-415c-b321-120c48f0ffe2 does not exist in container container123 does not contain a checksum.") + .hasFieldOrPropertyWithValue("error", CommonApiError.NOT_FOUND); + + verify(dataManagementConfiguration, times(1)).getBlobStorageAccountConnectionString(); + verify(dataManagementFactory, times(1)).getBlobServiceClient(connectionString); + verify(dataManagementFactory, times(1)).getBlobContainerClient(containerName, blobServiceClient); + verify(dataManagementFactory, times(1)).getBlobClient(blobContainerClient, blobId); + verify(blobClient, times(2)).exists(); + verify(blobClient, times(1)).getProperties(); + verify(blobProperties, times(1)).getContentMd5(); + verify(blobClient, times(2)).exists(); + verifyNoInteractions(fileContentChecksum); + } + + private void assertGetChecksumNotFound(Boolean exists) { + final UUID blobId = UUID.fromString("431318c8-97db-415c-b321-120c48f0ffe2"); + final String containerName = "container123"; + final String connectionString = "connectionString"; + + BlobServiceClient blobServiceClient = mock(BlobServiceClient.class); + BlobContainerClient blobContainerClient = mock(BlobContainerClient.class); + BlobClient blobClient = mock(BlobClient.class); + when(dataManagementFactory.getBlobServiceClient(any())) + .thenReturn(blobServiceClient); + when(dataManagementFactory.getBlobContainerClient(any(), any())) + .thenReturn(blobContainerClient); + when(dataManagementFactory.getBlobClient(any(), any())) + .thenReturn(blobClient); + when(dataManagementConfiguration.getBlobStorageAccountConnectionString()) + .thenReturn(connectionString); + + when(blobClient.exists()).thenReturn(exists); + + + Assertions.assertThatThrownBy(() -> dataManagementService.getChecksum(containerName, blobId)) + .isInstanceOf(DartsApiException.class) + .hasMessage("Resource not found. Blob 431318c8-97db-415c-b321-120c48f0ffe2 does not exist in container container123 was not found.") + .hasFieldOrPropertyWithValue("error", CommonApiError.NOT_FOUND); + + verify(dataManagementConfiguration, times(1)).getBlobStorageAccountConnectionString(); + verify(dataManagementFactory, times(1)).getBlobServiceClient(connectionString); + verify(dataManagementFactory, times(1)).getBlobContainerClient(containerName, blobServiceClient); + verify(dataManagementFactory, times(1)).getBlobClient(blobContainerClient, blobId); + verify(blobClient, times(exists == null ? 1 : 2)).exists(); + verifyNoInteractions(fileContentChecksum); + } } \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/darts/retention/service/impl/RetentionPostServiceImplTest.java b/src/test/java/uk/gov/hmcts/darts/retention/service/impl/RetentionPostServiceImplTest.java index fe16b46cb7..12db71a995 100644 --- a/src/test/java/uk/gov/hmcts/darts/retention/service/impl/RetentionPostServiceImplTest.java +++ b/src/test/java/uk/gov/hmcts/darts/retention/service/impl/RetentionPostServiceImplTest.java @@ -140,7 +140,7 @@ void fail_NoCase() { ); assertEquals("The selected caseId '1' cannot be found.", exception.getDetail()); - assertEquals("RETENTION_103", exception.getError().getType().toString()); + assertEquals("RETENTION_103", exception.getError().getType()); assertEquals(400, exception.getError().getHttpStatus().value()); } @@ -162,7 +162,7 @@ void fail_case_retention_passed() { () -> retentionPostService.postRetention(false, postRetentionRequest) ); assertEquals("caseId '101' retention date cannot be amended as the case is already expired.", exception.getDetail()); - assertEquals("RETENTION_118", exception.getError().getType().toString()); + assertEquals("RETENTION_118", exception.getError().getType()); assertEquals(422, exception.getError().getHttpStatus().value()); } @@ -184,7 +184,7 @@ void fail_CaseOpen() { ); assertEquals("caseId '101' must be closed before the retention period can be amended.", exception.getDetail()); - assertEquals("RETENTION_104", exception.getError().getType().toString()); + assertEquals("RETENTION_104", exception.getError().getType()); assertEquals(400, exception.getError().getHttpStatus().value()); } @@ -204,7 +204,7 @@ void fail_NoCurrentRetention() { ); assertEquals("caseId '101' must have a retention policy applied before being changed.", exception.getDetail()); - assertEquals("RETENTION_105", exception.getError().getType().toString()); + assertEquals("RETENTION_105", exception.getError().getType()); assertEquals(400, exception.getError().getHttpStatus().value()); } @@ -225,7 +225,7 @@ void fail_BeforeAutomatedRetentionDate() { ); assertEquals("caseId '101' must have a retention date after the last completed automated retention date '2025-10-01'.", exception.getDetail()); - assertEquals("RETENTION_101", exception.getError().getType().toString()); + assertEquals("RETENTION_101", exception.getError().getType()); assertEquals(422, exception.getError().getHttpStatus().value()); } @@ -246,7 +246,7 @@ void fail_BeforeCurrentRetentionDate_NotJudge() { ); assertEquals("You do not have permission to reduce the retention period.", exception.getDetail()); - assertEquals("RETENTION_100", exception.getError().getType().toString()); + assertEquals("RETENTION_100", exception.getError().getType()); assertEquals(403, exception.getError().getHttpStatus().value()); } @@ -279,7 +279,7 @@ void fail_multiplePolicies() { ); assertEquals("More than 1 retention policy found for fixedPolicyKey 'MANUAL'", exception.getDetail()); - assertEquals("RETENTION_106", exception.getError().getType().toString()); + assertEquals("RETENTION_106", exception.getError().getType()); assertEquals(500, exception.getError().getHttpStatus().value()); } diff --git a/src/test/java/uk/gov/hmcts/darts/retention/validation/RetentionsPostRequestValidatorTest.java b/src/test/java/uk/gov/hmcts/darts/retention/validation/RetentionsPostRequestValidatorTest.java index 8ddc496122..39663c723c 100644 --- a/src/test/java/uk/gov/hmcts/darts/retention/validation/RetentionsPostRequestValidatorTest.java +++ b/src/test/java/uk/gov/hmcts/darts/retention/validation/RetentionsPostRequestValidatorTest.java @@ -40,7 +40,7 @@ void fail_bothPermanentRetentionAndDate() { postRetentionRequest.setComments("theComment"); DartsApiException exception = assertThrows(DartsApiException.class, () -> RetentionsPostRequestValidator.validate(postRetentionRequest)); - assertEquals("RETENTION_102", exception.getError().getType().toString()); + assertEquals("RETENTION_102", exception.getError().getType()); assertEquals("Both 'is_permanent_retention' and 'retention_date' cannot be set, must be either one or the other.", exception.getDetail()); } @@ -52,7 +52,7 @@ void fail_RetentionDateFalse() { postRetentionRequest.setComments("theComment"); DartsApiException exception = assertThrows(DartsApiException.class, () -> RetentionsPostRequestValidator.validate(postRetentionRequest)); - assertEquals("RETENTION_102", exception.getError().getType().toString()); + assertEquals("RETENTION_102", exception.getError().getType()); assertEquals("Either 'is_permanent_retention' or 'retention_date' must be set.", exception.getDetail()); } @@ -63,7 +63,7 @@ void fail_neitherPermanentRetentionAndDate() { postRetentionRequest.setComments("theComment"); DartsApiException exception = assertThrows(DartsApiException.class, () -> RetentionsPostRequestValidator.validate(postRetentionRequest)); - assertEquals("RETENTION_102", exception.getError().getType().toString()); + assertEquals("RETENTION_102", exception.getError().getType()); assertEquals("Either 'is_permanent_retention' or 'retention_date' must be set.", exception.getDetail()); } diff --git a/src/test/java/uk/gov/hmcts/darts/transcriptions/component/impl/TranscriptionRequestDetailsValidatorTest.java b/src/test/java/uk/gov/hmcts/darts/transcriptions/component/impl/TranscriptionRequestDetailsValidatorTest.java index 0b5b9fbd61..49adad6b1a 100644 --- a/src/test/java/uk/gov/hmcts/darts/transcriptions/component/impl/TranscriptionRequestDetailsValidatorTest.java +++ b/src/test/java/uk/gov/hmcts/darts/transcriptions/component/impl/TranscriptionRequestDetailsValidatorTest.java @@ -21,7 +21,6 @@ import uk.gov.hmcts.darts.transcriptions.enums.TranscriptionUrgencyEnum; import uk.gov.hmcts.darts.transcriptions.model.TranscriptionRequestDetails; -import java.net.URI; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.List; @@ -66,7 +65,7 @@ void validateShouldThrowExceptionWhenHearingIdAndCaseIdIsNull() { var actualException = assertThrows(DARTS_EXCEPTION, () -> validator.validate(validatable)); // Then - assertEquals(URI.create("TRANSCRIPTION_100"), actualException.getError().getType()); + assertEquals("TRANSCRIPTION_100", actualException.getError().getType()); } @ParameterizedTest @@ -106,7 +105,7 @@ void validateShouldThrowExceptionWhenProvidedCaseDoesNotExist() { var actualException = assertThrows(DARTS_EXCEPTION, () -> validator.validate(validatable)); // Then - assertEquals(URI.create("CASE_104"), actualException.getError().getType()); + assertEquals("CASE_104", actualException.getError().getType()); assertEquals("CASE", actualException.getError().getErrorTypePrefix()); }