From 8381125d88dab9c5f16717f531a9f744643e80f0 Mon Sep 17 00:00:00 2001 From: Baris Ozturkmen Date: Tue, 14 Jul 2026 11:42:55 +0100 Subject: [PATCH] SPRDT-987 cover remaining move-to-past-date reject scenarios Ports the last two move-hearing-to-past-date reject scenarios that only existed as black-box tests in cpp-apitests, so listing-side coverage is complete and the apitests deterministic tier can eventually be deleted: - ListingCommandApiTest.shouldRejectMoveWhenRangeContainsNoSittingDay: a Saturday..Sunday span passes date validation but expands to zero sitting days -> 422 INVALID_DATE_RANGE with the no-sitting-day message. Unit level because the rule is pure date logic in workingDaysBetween(). - MoveHearingToPastDateIT.shouldRejectMoveWith400WhenStartTimeMissing: startTime omitted -> schema 400. IT level because the framework's JSON schema interceptor rejects before the handler runs, so a unit test cannot reach it. Mirrors the existing missing courtCentreId/courtRoomId tests via a new whenHearingIsMovedWithMissingStartTime() step. --- .../command/api/ListingCommandApiTest.java | 32 +++++++++++++++++++ .../listing/it/MoveHearingToPastDateIT.java | 10 ++++++ .../steps/MoveHearingToPastDateSteps.java | 7 ++++ 3 files changed, 49 insertions(+) diff --git a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java index 49858a044..fcc0c1f1f 100644 --- a/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java +++ b/listing-command/listing-command-api/src/test/java/uk/gov/moj/cpp/listing/command/api/ListingCommandApiTest.java @@ -7,6 +7,7 @@ import static uk.gov.justice.services.messaging.JsonObjects.createArrayBuilder; import static uk.gov.justice.services.messaging.JsonObjects.createReader; import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -606,6 +607,17 @@ private static LocalDate lastWorkingDayBeforeToday() { return day; } + // most recent Saturday strictly before today - with the following Sunday it forms a pure-weekend + // span that is always past, ordered, and within 6 months (endDate is at latest today, when today + // is Sunday), so only the no-sitting-day rule can reject it + private static LocalDate mostRecentSaturday() { + LocalDate day = LocalDate.now().minusDays(1); + while (day.getDayOfWeek() != java.time.DayOfWeek.SATURDAY) { + day = day.minusDays(1); + } + return day; + } + private static final UUID MOVE_COURT_ROOM_ID = randomUUID(); // startTime is an absolute UTC instant (e.g. 2026-07-02T10:00:00.000Z); the request carries no @@ -674,6 +686,26 @@ public void shouldRejectMoveWhenEndDateBeforeStartDate() { verify(sender, never()).send(any()); } + @Test + public void shouldRejectMoveWhenRangeContainsNoSittingDay() { + // Saturday..Sunday span: passes validateMoveDates (past, ordered, within 6 months) but + // expands to zero sitting days - courts do not sit at weekends. + final LocalDate saturday = mostRecentSaturday(); + stubMovePayload(randomUUID(), randomUUID(), saturday, "10:00"); + given(payload.containsKey("endTime")).willReturn(true); + given(payload.isNull("endTime")).willReturn(false); + given(payload.getString("endTime")).willReturn(saturday.plusDays(1) + "T10:00:00.000Z"); + + final MoveHearingToPastDateException thrown = assertThrows(MoveHearingToPastDateException.class, + () -> listingCommandApi.handleMoveHearingToPastDate(envelope)); + + assertThat(thrown.getHttpStatus(), is(422)); + assertThat(thrown.getErrorCode(), is("INVALID_DATE_RANGE")); + // distinguishes the no-sitting-day INVALID_DATE_RANGE from the end-before-start one + assertThat(thrown.getResponseBody().getString("message"), containsString("sitting")); + verify(sender, never()).send(any()); + } + @Test public void shouldMoveMagistratesHearingToPastDateEnrichWithSlotDetailsAndSend() { final UUID hearingId = randomUUID(); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/MoveHearingToPastDateIT.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/MoveHearingToPastDateIT.java index d2376d508..c5d94c316 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/MoveHearingToPastDateIT.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/it/MoveHearingToPastDateIT.java @@ -191,6 +191,16 @@ void shouldRejectMoveWith400WhenCourtRoomIdMissing() { assertThat(response.getStatus(), is(400)); } + @Test + void shouldRejectMoveWith400WhenStartTimeMissing() { + final HearingsData hearingsData = hearingsDataWithAllocationDataAndJudiciary(MAGISTRATES_JURISDICTION); + final MoveHearingToPastDateSteps moveSteps = new MoveHearingToPastDateSteps(hearingsData); + + final Response response = moveSteps.whenHearingIsMovedWithMissingStartTime(); + + assertThat(response.getStatus(), is(400)); + } + @Test void shouldMoveCrownHearingToPastDateListingSideOnlyWithoutCallingCourtScheduler() { final MoveHearingToPastDateSteps moveSteps = givenAListedHearing(CROWN_JURISDICTION); diff --git a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/MoveHearingToPastDateSteps.java b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/MoveHearingToPastDateSteps.java index 81a91bab0..8f63ab6f5 100644 --- a/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/MoveHearingToPastDateSteps.java +++ b/listing-integration-test/src/test/java/uk/gov/moj/cpp/listing/steps/MoveHearingToPastDateSteps.java @@ -74,6 +74,13 @@ public Response whenHearingIsMovedWithMissingCourtRoom(final LocalDate date) { return postMove(hearingId, payload); } + public Response whenHearingIsMovedWithMissingStartTime() { + // startTime omitted (schema-mandatory); both ids present so the 400 is unambiguously the + // missing startTime. No date parameter - the payload carries no instant at all. + final String payload = "{\"courtCentreId\":\"" + courtCentreId + "\",\"courtRoomId\":\"" + courtRoomId + "\"}"; + return postMove(hearingId, payload); + } + /** A multi-day move over [startTime, endTime], scoped to a specific room. */ public Response whenHearingIsMovedToPastDateRange(final LocalDate startDate, final LocalDate endDate, final String courtRoomId) { final String payload = "{\"courtCentreId\":\"" + courtCentreId