17.103.163
- 17.0.149
+ 17.104.16817.104.4917.0.8517.103.42
diff --git a/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessor.java b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessor.java
index 4b288b6281..e4e3e894b3 100644
--- a/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessor.java
+++ b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessor.java
@@ -13,6 +13,7 @@
import uk.gov.justice.core.courts.DefendantsToRemove;
import uk.gov.justice.core.courts.Hearing;
import uk.gov.justice.core.courts.HearingDay;
+import uk.gov.justice.core.courts.JurisdictionType;
import uk.gov.justice.core.courts.ListCourtHearing;
import uk.gov.justice.core.courts.ListDefendantRequest;
import uk.gov.justice.core.courts.ListHearingRequested;
@@ -20,6 +21,7 @@
import uk.gov.justice.core.courts.OffencesToRemove;
import uk.gov.justice.core.courts.ProsecutionCase;
import uk.gov.justice.core.courts.ProsecutionCasesToRemove;
+import uk.gov.justice.core.courts.RotaSlot;
import uk.gov.justice.core.courts.UpdateHearingForPartialAllocation;
import uk.gov.justice.listing.courts.HearingPartiallyUpdated;
import uk.gov.justice.listing.events.HearingRequestedForListing;
@@ -32,6 +34,7 @@
import uk.gov.justice.services.messaging.JsonEnvelope;
import uk.gov.moj.cpp.progression.helper.HearingNotificationHelper;
import uk.gov.moj.cpp.progression.service.ApplicationParameters;
+import uk.gov.moj.cpp.progression.service.CourtScheduleQueryAdapter;
import uk.gov.moj.cpp.progression.service.ListingService;
import uk.gov.moj.cpp.progression.service.ProgressionService;
import uk.gov.moj.cpp.progression.service.dto.HearingNotificationInputData;
@@ -90,6 +93,9 @@ public class ListHearingRequestedProcessor {
@Inject
private HearingNotificationHelper hearingNotificationHelper;
+ @Inject
+ private CourtScheduleQueryAdapter courtScheduleQueryAdapter;
+
@ServiceComponent(EVENT_PROCESSOR)
@Inject
private Sender sender;
@@ -98,7 +104,18 @@ public class ListHearingRequestedProcessor {
@Handles("progression.event.list-hearing-requested")
public void handle(final JsonEnvelope jsonEnvelope) {
- final ListHearingRequested listHearingRequested = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), ListHearingRequested.class);
+ ListHearingRequested listHearingRequested = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), ListHearingRequested.class);
+
+ // Defence-in-depth for unallocated CROWN: when the user picks a DRAFT court-schedule
+ // slot via the CCP2 UI, the payload arrives with courtCentre.roomId / roomName
+ // populated (denormalised from bookedSlots for display purposes). That data must
+ // not be written to hearing.payload or surfaced in the new-hearing notification
+ // email - for an unallocated hearing there is no confirmed courtroom yet. Listing
+ // strips this on its side (HearingEnrichmentOrchestrator.stripRoomInfoIfAnyDraft,
+ // SPRDT-858) but progression sees the payload first and would otherwise leak it
+ // into hearing.payload and the notification. Mirror the strip here by asking
+ // listing-query-api whether any of the booked sessions is DRAFT.
+ listHearingRequested = stripCourtCentreRoomIfAnyDraftSession(listHearingRequested, jsonEnvelope);
final ListCourtHearing listCourtHearing = convertListCourtHearing(listHearingRequested, jsonEnvelope);
@@ -116,6 +133,48 @@ public void handle(final JsonEnvelope jsonEnvelope) {
}
}
+ private ListHearingRequested stripCourtCentreRoomIfAnyDraftSession(final ListHearingRequested original, final JsonEnvelope jsonEnvelope) {
+ final CourtHearingRequest listNewHearing = original.getListNewHearing();
+ if (listNewHearing == null
+ || !JurisdictionType.CROWN.equals(listNewHearing.getJurisdictionType())
+ || listNewHearing.getBookedSlots() == null
+ || listNewHearing.getBookedSlots().isEmpty()
+ || listNewHearing.getCourtCentre() == null
+ || listNewHearing.getCourtCentre().getRoomId() == null) {
+ return original;
+ }
+
+ final List courtScheduleIds = listNewHearing.getBookedSlots().stream()
+ .map(RotaSlot::getCourtScheduleId)
+ .filter(Objects::nonNull)
+ .filter(s -> !s.isBlank())
+ .map(UUID::fromString)
+ .collect(toList());
+
+ if (courtScheduleIds.isEmpty() || !courtScheduleQueryAdapter.anySessionIsDraft(jsonEnvelope, courtScheduleIds)) {
+ return original;
+ }
+
+ LOGGER.info("Stripping courtCentre.roomId/roomName for hearingId={} - at least one booked court-schedule session is DRAFT (unallocated CROWN)",
+ original.getHearingId());
+
+ final CourtCentre strippedCourtCentre = CourtCentre.courtCentre()
+ .withValuesFrom(listNewHearing.getCourtCentre())
+ .withRoomId(null)
+ .withRoomName(null)
+ .build();
+
+ final CourtHearingRequest strippedListNewHearing = CourtHearingRequest.courtHearingRequest()
+ .withValuesFrom(listNewHearing)
+ .withCourtCentre(strippedCourtCentre)
+ .build();
+
+ return ListHearingRequested.listHearingRequested()
+ .withValuesFrom(original)
+ .withListNewHearing(strippedListNewHearing)
+ .build();
+ }
+
@Handles("public.listing.hearing-requested-for-listing")
public void handlePublicEvent(final JsonEnvelope jsonEnvelope) {
final HearingRequestedForListing hearingRequestedForListing = jsonObjectToObjectConverter.convert(jsonEnvelope.payloadAsJsonObject(), HearingRequestedForListing.class);
diff --git a/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapter.java b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapter.java
new file mode 100644
index 0000000000..8f2832bd92
--- /dev/null
+++ b/progression-event/progression-event-processor/src/main/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapter.java
@@ -0,0 +1,94 @@
+package uk.gov.moj.cpp.progression.service;
+
+import static javax.json.Json.createArrayBuilder;
+import static javax.json.Json.createObjectBuilder;
+import static uk.gov.justice.services.core.annotation.Component.EVENT_PROCESSOR;
+
+import uk.gov.justice.services.core.annotation.ServiceComponent;
+import uk.gov.justice.services.core.enveloper.Enveloper;
+import uk.gov.justice.services.core.requester.Requester;
+import uk.gov.justice.services.messaging.JsonEnvelope;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Calls cpp-context-listing's {@code listing.query.court.schedule.draft.status} query to
+ * decide whether any of a hearing's booked court-schedule sessions is DRAFT.
+ *
+ *
Used by {@code ListHearingRequestedProcessor} to detect unallocated CROWN hearings so
+ * the courtroom info denormalised onto {@code courtCentre} by the UI can be stripped before
+ * progression writes {@code hearing.payload} or sends the new-hearing notification email.
+ * Mirrors the intent of listing-side
+ * {@code HearingEnrichmentOrchestrator.stripRoomInfoIfAnyDraft} (SPRDT-858) so the strip
+ * happens regardless of which side observes the payload first.
+ *
+ *
Goes through listing's query-api (not directly to listingcourtscheduler) so the cross-context
+ * boundary stays clean - progression only ever talks to listing-context, never bypasses to
+ * listingcourtscheduler-api. Fails-safe by returning {@code true} ("treat as DRAFT") if the
+ * remote call returns an unexpected payload; leaking a phantom courtroom into a notification
+ * email or persisted hearing snapshot is worse than dropping room info for what may be a
+ * confirmed-allocated hearing.
+ */
+public class CourtScheduleQueryAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CourtScheduleQueryAdapter.class);
+
+ private static final String LISTING_QUERY_COURT_SCHEDULE_DRAFT_STATUS = "listing.query.court.schedule.draft.status";
+ private static final String COURT_SCHEDULE_ID_LIST = "courtScheduleIdList";
+ private static final String ANY_DRAFT = "anyDraft";
+
+ @Inject
+ @ServiceComponent(EVENT_PROCESSOR)
+ private Requester requester;
+
+ @Inject
+ private Enveloper enveloper;
+
+ public boolean anySessionIsDraft(final JsonEnvelope sourceEnvelope, final List courtScheduleIds) {
+ if (courtScheduleIds == null || courtScheduleIds.isEmpty()) {
+ return false;
+ }
+
+ final JsonArrayBuilder list = createArrayBuilder();
+ courtScheduleIds.forEach(id -> list.add(id.toString()));
+ final JsonObject requestPayload = createObjectBuilder()
+ .add(COURT_SCHEDULE_ID_LIST, list)
+ .build();
+
+ final JsonEnvelope response;
+ try {
+ response = requester.requestAsAdmin(enveloper
+ .withMetadataFrom(sourceEnvelope, LISTING_QUERY_COURT_SCHEDULE_DRAFT_STATUS)
+ .apply(requestPayload));
+ } catch (Exception ex) {
+ // Fail-open: if we cannot prove a session is draft (e.g. listing not yet deployed
+ // with the endpoint, dispatch error, network blip) we preserve the original
+ // payload by returning false. The original unallocated-CROWN leak resumes during
+ // the outage, but allocated CROWN hearings keep their courtroom info - which is
+ // the much more common case. Operators will see this WARN if the call is
+ // persistently failing.
+ LOGGER.warn("listing.query.court.schedule.draft.status threw {} for {} ids - failing-open (anyDraft=false, no strip)",
+ ex.getClass().getSimpleName(), courtScheduleIds.size());
+ return false;
+ }
+
+ final JsonObject responseBody = response == null ? null : response.payloadAsJsonObject();
+ if (responseBody == null || !responseBody.containsKey(ANY_DRAFT) || responseBody.isNull(ANY_DRAFT)) {
+ // Same fail-open rationale as the catch block above - if listing's response is
+ // malformed we cannot prove draft state, so preserve the input.
+ LOGGER.warn("listing.query.court.schedule.draft.status returned an unexpected payload for {} ids - failing-open (anyDraft=false, no strip)",
+ courtScheduleIds.size());
+ return false;
+ }
+
+ return responseBody.getBoolean(ANY_DRAFT);
+ }
+}
diff --git a/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessorTest.java b/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessorTest.java
index 31c4b9c28e..892fa51e3a 100644
--- a/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessorTest.java
+++ b/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/processor/ListHearingRequestedProcessorTest.java
@@ -36,6 +36,7 @@
import uk.gov.justice.core.courts.PersonDefendant;
import uk.gov.justice.core.courts.ProsecutionCase;
import uk.gov.justice.core.courts.ProsecutionCaseIdentifier;
+import uk.gov.justice.core.courts.RotaSlot;
import uk.gov.justice.core.courts.UpdateHearingForPartialAllocation;
import uk.gov.justice.core.courts.notification.EmailChannel;
import uk.gov.justice.listing.courts.Defendants;
@@ -56,6 +57,7 @@
import uk.gov.moj.cpp.progression.helper.HearingNotificationHelper;
import uk.gov.moj.cpp.progression.eventprocessorstore.persistence.repository.NotificationInfoJdbcRepository;
import uk.gov.moj.cpp.progression.service.ApplicationParameters;
+import uk.gov.moj.cpp.progression.service.CourtScheduleQueryAdapter;
import uk.gov.moj.cpp.progression.service.DefenceService;
import uk.gov.moj.cpp.progression.service.DocumentGeneratorService;
import uk.gov.moj.cpp.progression.service.ListingService;
@@ -151,6 +153,9 @@ public class ListHearingRequestedProcessorTest {
@Mock
private ApplicationParameters applicationParameters;
+ @Mock
+ private CourtScheduleQueryAdapter courtScheduleQueryAdapter;
+
private ObjectMapper objectMapper = new ObjectMapperProducer().objectMapper();
@Spy
@@ -835,6 +840,175 @@ public void shouldNotUpdateYouthWhenFirstListingAndNoDefendants() {
org.mockito.Mockito.verify(sender, org.mockito.Mockito.never()).send(any());
}
+ // ─── courtCentre.roomId / roomName strip for unallocated CROWN ───────────────────
+
+ @Test
+ public void shouldStripCourtCentreRoomWhenCrownHearingHasDraftSession() {
+ final UUID scheduleId = randomUUID();
+ final CourtHearingRequest crownHearingWithRoom = crownBookedSlotPayloadWithCourtCentreRoom(scheduleId);
+ final ListHearingRequested event = ListHearingRequested.listHearingRequested()
+ .withHearingId(randomUUID())
+ .withListNewHearing(crownHearingWithRoom)
+ .withSendNotificationToParties(false)
+ .build();
+ final JsonEnvelope envelope = envelopeFrom(
+ MetadataBuilderFactory.metadataWithRandomUUID("progression.event.list-hearing-requested"),
+ objectToJsonObjectConverter.convert(event));
+
+ when(courtScheduleQueryAdapter.anySessionIsDraft(any(JsonEnvelope.class), any())).thenReturn(true);
+ final ArgumentCaptor transformerArg = ArgumentCaptor.forClass(CourtHearingRequest.class);
+ when(listCourtHearingTransformer.transform(any(JsonEnvelope.class), any(List.class), transformerArg.capture(), any(UUID.class)))
+ .thenReturn(ListCourtHearing.listCourtHearing().build());
+
+ listHearingRequestedProcessor.handle(envelope);
+
+ // After the strip, the CourtHearingRequest handed to the downstream transformer must
+ // have null roomId / roomName on courtCentre, even though the input had them populated.
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomId(), is((UUID) null));
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomName(), is((String) null));
+ }
+
+ @Test
+ public void shouldNotStripCourtCentreRoomWhenCrownHearingHasOnlyNonDraftSessions() {
+ final UUID scheduleId = randomUUID();
+ final CourtHearingRequest crownHearingWithRoom = crownBookedSlotPayloadWithCourtCentreRoom(scheduleId);
+ // Capture the exact roomId / roomName on the way in so we can assert byte-for-byte
+ // preservation through the handler. (Previously this asserted only that
+ // toString().isEmpty() == false, which is trivially true for any UUID and would have
+ // passed even if the strip leaked through.)
+ final UUID expectedRoomId = crownHearingWithRoom.getCourtCentre().getRoomId();
+ final String expectedRoomName = crownHearingWithRoom.getCourtCentre().getRoomName();
+ final ListHearingRequested event = ListHearingRequested.listHearingRequested()
+ .withHearingId(randomUUID())
+ .withListNewHearing(crownHearingWithRoom)
+ .withSendNotificationToParties(false)
+ .build();
+ final JsonEnvelope envelope = envelopeFrom(
+ MetadataBuilderFactory.metadataWithRandomUUID("progression.event.list-hearing-requested"),
+ objectToJsonObjectConverter.convert(event));
+
+ when(courtScheduleQueryAdapter.anySessionIsDraft(any(JsonEnvelope.class), any())).thenReturn(false);
+ final ArgumentCaptor transformerArg = ArgumentCaptor.forClass(CourtHearingRequest.class);
+ when(listCourtHearingTransformer.transform(any(JsonEnvelope.class), any(List.class), transformerArg.capture(), any(UUID.class)))
+ .thenReturn(ListCourtHearing.listCourtHearing().build());
+
+ listHearingRequestedProcessor.handle(envelope);
+
+ // Hearing is allocated (no draft sessions): courtCentre.roomId / roomName must be
+ // preserved exactly. This is the regression we hit in ns-ste-ccm-72 where allocated
+ // CROWN hearings were losing their courtroom in the notification email because the
+ // adapter fail-safed to "treat as draft" on listing-query errors. Adapter now fails
+ // open (returns false on error), so non-draft paths flow through unchanged.
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomId(), is(expectedRoomId));
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomName(), is(expectedRoomName));
+ }
+
+ @Test
+ public void shouldSkipAdapterAndNotStripWhenJurisdictionIsMagistrates() {
+ final UUID roomId = randomUUID();
+ final CourtHearingRequest magsHearingWithRoom = CourtHearingRequest.courtHearingRequest()
+ .withHearingType(HearingType.hearingType().withId(randomUUID()).withDescription("First").build())
+ .withCourtCentre(CourtCentre.courtCentre()
+ .withId(randomUUID())
+ .withName("Court 1")
+ .withRoomId(roomId)
+ .withRoomName("Courtroom 1")
+ .build())
+ .withJurisdictionType(JurisdictionType.MAGISTRATES)
+ .withListDefendantRequests(singletonList(ListDefendantRequest.listDefendantRequest()
+ .withProsecutionCaseId(CASE_ID)
+ .withDefendantOffences(singletonList(randomUUID()))
+ .withDefendantId(MULTI_OFFENCE_DEFENDANT_ID)
+ .build()))
+ .withEarliestStartDateTime(ZonedDateTime.now())
+ .build();
+ final ListHearingRequested event = ListHearingRequested.listHearingRequested()
+ .withHearingId(randomUUID())
+ .withListNewHearing(magsHearingWithRoom)
+ .withSendNotificationToParties(false)
+ .build();
+ final JsonEnvelope envelope = envelopeFrom(
+ MetadataBuilderFactory.metadataWithRandomUUID("progression.event.list-hearing-requested"),
+ objectToJsonObjectConverter.convert(event));
+
+ final ArgumentCaptor transformerArg = ArgumentCaptor.forClass(CourtHearingRequest.class);
+ when(listCourtHearingTransformer.transform(any(JsonEnvelope.class), any(List.class), transformerArg.capture(), any(UUID.class)))
+ .thenReturn(ListCourtHearing.listCourtHearing().build());
+
+ listHearingRequestedProcessor.handle(envelope);
+
+ org.mockito.Mockito.verify(courtScheduleQueryAdapter, org.mockito.Mockito.never()).anySessionIsDraft(any(JsonEnvelope.class), any());
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomId(), is(roomId));
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomName(), is("Courtroom 1"));
+ }
+
+ @Test
+ public void shouldSkipAdapterAndNotStripWhenCrownHearingHasNoBookedSlots() {
+ final UUID roomId = randomUUID();
+ final CourtHearingRequest crownHearingNoBookedSlots = CourtHearingRequest.courtHearingRequest()
+ .withHearingType(HearingType.hearingType().withId(randomUUID()).withDescription("Trial").build())
+ .withCourtCentre(CourtCentre.courtCentre()
+ .withId(randomUUID())
+ .withName("Crown Court 1")
+ .withRoomId(roomId)
+ .withRoomName("Courtroom 5")
+ .build())
+ .withJurisdictionType(JurisdictionType.CROWN)
+ .withListDefendantRequests(singletonList(ListDefendantRequest.listDefendantRequest()
+ .withProsecutionCaseId(CASE_ID)
+ .withDefendantOffences(singletonList(randomUUID()))
+ .withDefendantId(MULTI_OFFENCE_DEFENDANT_ID)
+ .build()))
+ .withEarliestStartDateTime(ZonedDateTime.now())
+ .build();
+ final ListHearingRequested event = ListHearingRequested.listHearingRequested()
+ .withHearingId(randomUUID())
+ .withListNewHearing(crownHearingNoBookedSlots)
+ .withSendNotificationToParties(false)
+ .build();
+ final JsonEnvelope envelope = envelopeFrom(
+ MetadataBuilderFactory.metadataWithRandomUUID("progression.event.list-hearing-requested"),
+ objectToJsonObjectConverter.convert(event));
+
+ final ArgumentCaptor transformerArg = ArgumentCaptor.forClass(CourtHearingRequest.class);
+ when(listCourtHearingTransformer.transform(any(JsonEnvelope.class), any(List.class), transformerArg.capture(), any(UUID.class)))
+ .thenReturn(ListCourtHearing.listCourtHearing().build());
+
+ listHearingRequestedProcessor.handle(envelope);
+
+ // No bookedSlots means no court-schedule sessions to check; adapter must not be called.
+ org.mockito.Mockito.verify(courtScheduleQueryAdapter, org.mockito.Mockito.never()).anySessionIsDraft(any(JsonEnvelope.class), any());
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomId(), is(roomId));
+ assertThat(transformerArg.getValue().getCourtCentre().getRoomName(), is("Courtroom 5"));
+ }
+
+ private CourtHearingRequest crownBookedSlotPayloadWithCourtCentreRoom(final UUID courtScheduleId) {
+ return CourtHearingRequest.courtHearingRequest()
+ .withHearingType(HearingType.hearingType().withId(randomUUID()).withDescription("Trial").build())
+ .withCourtCentre(CourtCentre.courtCentre()
+ .withId(randomUUID())
+ .withName("Blackfriars Crown Court")
+ .withRoomId(randomUUID())
+ .withRoomName("Courtroom 01")
+ .build())
+ .withJurisdictionType(JurisdictionType.CROWN)
+ .withBookedSlots(singletonList(RotaSlot.rotaSlot()
+ .withCourtScheduleId(courtScheduleId.toString())
+ .withCourtRoomId(235)
+ .withRoomId(randomUUID().toString())
+ .withSession("AD")
+ .withStartTime(ZonedDateTime.now())
+ .withDuration(120)
+ .build()))
+ .withListDefendantRequests(singletonList(ListDefendantRequest.listDefendantRequest()
+ .withProsecutionCaseId(CASE_ID)
+ .withDefendantOffences(singletonList(randomUUID()))
+ .withDefendantId(MULTI_OFFENCE_DEFENDANT_ID)
+ .build()))
+ .withEarliestStartDateTime(ZonedDateTime.now())
+ .build();
+ }
+
private CourtHearingRequest receivePayloadOfListHearingRequestWithOneCaseMultipleDefendantsWithReferralReason() {
return CourtHearingRequest.courtHearingRequest()
.withHearingType(HearingType.hearingType()
diff --git a/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapterTest.java b/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapterTest.java
new file mode 100644
index 0000000000..fd4934375c
--- /dev/null
+++ b/progression-event/progression-event-processor/src/test/java/uk/gov/moj/cpp/progression/service/CourtScheduleQueryAdapterTest.java
@@ -0,0 +1,151 @@
+package uk.gov.moj.cpp.progression.service;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataWithRandomUUID;
+import static uk.gov.justice.services.messaging.JsonEnvelope.envelopeFrom;
+
+import uk.gov.justice.services.core.enveloper.Enveloper;
+import uk.gov.justice.services.core.requester.Requester;
+import uk.gov.justice.services.messaging.JsonEnvelope;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Function;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class CourtScheduleQueryAdapterTest {
+
+ private static final UUID SCHEDULE_ID_1 = UUID.fromString("11111111-1111-1111-1111-111111111111");
+ private static final UUID SCHEDULE_ID_2 = UUID.fromString("22222222-2222-2222-2222-222222222222");
+
+ @Mock
+ private Requester requester;
+
+ @Mock
+ private Enveloper enveloper;
+
+ @Mock
+ private Function