diff --git a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/AbstractIT.java b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/AbstractIT.java index 616ab69dc..ec4743129 100644 --- a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/AbstractIT.java +++ b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/AbstractIT.java @@ -294,8 +294,8 @@ protected void stubProsecutionCases(final Hearing hearing) { hearing.getProsecutionCases().forEach(prosecutionCase -> stubGetProgressionProsecutionCases(prosecutionCase.getId())); } - protected void cleanDatabase(final String dbTableName) { + protected void cleanDatabase(final String dbTableName, final String... additionalTables) { final DatabaseCleaner databaseCleaner = new DatabaseCleaner(); - databaseCleaner.cleanViewStoreTables(CONTEXT_NAME, dbTableName); + databaseCleaner.cleanViewStoreTables(CONTEXT_NAME, dbTableName, additionalTables); } } diff --git a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java index 00ea86154..34217a8d0 100644 --- a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java +++ b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/it/CourtListRestrictionIT.java @@ -1,5 +1,6 @@ package uk.gov.moj.cpp.hearing.it; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasNoJsonPath; import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; import static java.util.Optional.of; @@ -11,6 +12,10 @@ import static org.hamcrest.Matchers.is; import static uk.gov.moj.cpp.hearing.it.PublishLatestCourtCentreHearingEventsIT.XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26; import static uk.gov.moj.cpp.hearing.steps.HearingEventStepDefinitions.OPEN_CASE_PROSECUTION_EVENT_DEFINITION_ID; +import static uk.gov.moj.cpp.hearing.utils.WebDavStub.awaitNewFile; +import static uk.gov.moj.cpp.hearing.utils.WebDavStub.awaitNewSentXmlForPubDisplay; +import static uk.gov.moj.cpp.hearing.utils.WebDavStub.countFilesAt; +import static uk.gov.moj.cpp.hearing.utils.WebDavStub.countSentXmlForPubDisplay; import static uk.gov.moj.cpp.hearing.utils.WebDavStub.getFileForPath; import static uk.gov.moj.cpp.hearing.utils.WebDavStub.getSentXmlForPubDisplay; @@ -35,9 +40,30 @@ public class CourtListRestrictionIT extends AbstractPublishLatestCourtCentreHear private ZonedDateTime eventTime; + /** + * Clean every table that can carry state across tests in this class. All five tests bind + * their hearings to the SAME static {@code caseId} (see + * {@link AbstractPublishLatestCourtCentreHearingIT}), so residual + * {@code is_court_list_restricted=true} on {@code ha_case} (the {@code ProsecutionCase} + * entity's table — see its {@code @Table} annotation) or {@code ha_defendant} from one + * test poisons the next. The {@code court_list_publish_status} row is also dropped so the + * FIRST publish in this class cannot inherit {@code EXPORT_SUCCESSFUL} from a prior test + * class's publish — without this, + * {@code verifyCourtListPublishStatusReturnedWhenQueryingFromAPI} returns on the stale + * status before the current publish has produced a file. + */ + private void cleanRestrictionTables() { + cleanDatabase("ha_hearing", + "ha_case", + "ha_defendant", + "ha_hearing_day", + "ha_hearing_event", + "court_list_publish_status"); + } + @BeforeEach public void setUpTest() { - cleanDatabase("ha_hearing"); + cleanRestrictionTables(); eventTime = new UtcClock().now().minusMinutes(5L); } @@ -54,7 +80,7 @@ public void setUpTest() { */ @AfterEach public void tearDownTest() { - cleanDatabase("ha_hearing"); + cleanRestrictionTables(); } @Test @@ -71,6 +97,10 @@ public void shouldRequestToPublishCourtListWithCaseRestriction() throws Exceptio withJsonPath("$.caseIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(true))))); + // Wait for the restriction projection to land before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails", hasSize(0))); + JsonObject publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(); @@ -96,11 +126,20 @@ public void shouldRequestToPublishCourtListWithCaseRestriction() throws Exceptio withJsonPath("$.caseIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(false))))); + // Wait for the un-restriction projection to land before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails", hasSize(1))); + publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); + final int webPageCountBeforeSecondPublish = countFilesAt(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); + final int pubDisplayCountBeforeSecondPublish = countSentXmlForPubDisplay(); + courtCentreId = sendPublishCourtListCommand(publishCourtListJsonObject, courtCentreId); publishCourtListSteps.verifyCourtListPublishStatusReturnedWhenQueryingFromAPI(courtCentreId); + awaitNewFile(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26, webPageCountBeforeSecondPublish); + awaitNewSentXmlForPubDisplay(pubDisplayCountBeforeSecondPublish); filePayload = getFileForPath(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); filePayloadForPubDisplay = getSentXmlForPubDisplay(); @@ -127,6 +166,10 @@ public void shouldRequestToPublishCourtListWithDefendantRestrictionOnOff() throw withJsonPath("$.defendantIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(true))))); + // Wait for defendant restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails[0].defendants", hasSize(0))); + final JsonObject publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(); @@ -155,8 +198,17 @@ public void shouldRequestToPublishCourtListWithDefendantRestrictionOnOff() throw withJsonPath("$.defendantIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(false))))); + // Wait for defendant un-restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails[0].defendants", hasSize(1))); + + final int webPageCountBeforeSecondPublish = countFilesAt(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); + final int pubDisplayCountBeforeSecondPublish = countSentXmlForPubDisplay(); + courtCentreId = sendPublishCourtListCommand(publishCourtListJsonObject, courtCentreId); publishCourtListSteps.verifyCourtListPublishStatusReturnedWhenQueryingFromAPI(courtCentreId); + awaitNewFile(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26, webPageCountBeforeSecondPublish); + awaitNewSentXmlForPubDisplay(pubDisplayCountBeforeSecondPublish); filePayload = getFileForPath(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); filePayloadForPubDisplay = getSentXmlForPubDisplay(); @@ -191,6 +243,10 @@ public void shouldRequestToPublishCourtListForApplicationRestrictionOnOff() thro withJsonPath("$.courtApplicationIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(true))))); + // Wait for application restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails", hasSize(0))); + JsonObject publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(); @@ -216,11 +272,20 @@ public void shouldRequestToPublishCourtListForApplicationRestrictionOnOff() thro withJsonPath("$.courtApplicationIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(false))))); + // Wait for application un-restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails", hasSize(1))); + publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); + final int webPageCountBeforeSecondPublish = countFilesAt(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); + final int pubDisplayCountBeforeSecondPublish = countSentXmlForPubDisplay(); + courtCentreId = sendPublishCourtListCommand(publishCourtListJsonObject, courtCentreId); publishCourtListSteps.verifyCourtListPublishStatusReturnedWhenQueryingFromAPI(courtCentreId); + awaitNewFile(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26, webPageCountBeforeSecondPublish); + awaitNewSentXmlForPubDisplay(pubDisplayCountBeforeSecondPublish); filePayload = getFileForPath(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); filePayloadForPubDisplay = getSentXmlForPubDisplay(); @@ -249,6 +314,10 @@ public void shouldRequestToPublishCourtListForApplicationApplicantRestrictionOnO withJsonPath("$.courtApplicationApplicantIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(true))))); + // Wait for applicant restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + hasNoJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails[0].defendants[0].firstName")); + JsonObject publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(); @@ -274,11 +343,20 @@ public void shouldRequestToPublishCourtListForApplicationApplicantRestrictionOnO withJsonPath("$.courtApplicationApplicantIds", hasSize(1)), withJsonPath("$.restrictCourtList", is(false))))); + // Wait for applicant un-restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails[0].defendants[0].firstName", org.hamcrest.CoreMatchers.notNullValue())); + publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); + final int webPageCountBeforeSecondPublish = countFilesAt(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); + final int pubDisplayCountBeforeSecondPublish = countSentXmlForPubDisplay(); + courtCentreId = sendPublishCourtListCommand(publishCourtListJsonObject, courtCentreId); publishCourtListSteps.verifyCourtListPublishStatusReturnedWhenQueryingFromAPI(courtCentreId); + awaitNewFile(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26, webPageCountBeforeSecondPublish); + awaitNewSentXmlForPubDisplay(pubDisplayCountBeforeSecondPublish); filePayload = getFileForPath(XHIBIT_GATEWAY_SEND_WEB_PAGE_TO_XHIBIT_FILE_NAME_26); filePayloadForPubDisplay = getSentXmlForPubDisplay(); @@ -308,6 +386,10 @@ public void shouldRequestToPublishCourtListWhenYoungDefendantIsRestrictedOnIniti caseId, randomUUID(), courtRoom2Id, randomUUID().toString(), OPEN_CASE_PROSECUTION_EVENT_DEFINITION_ID, eventTime, of(hearingTypeId), courtCentreId, eventTime.toLocalDate()); + // Wait for the young-defendant restriction to land in the projection before publishing + courtListRestrictionSteps.waitForRestrictionProjection(courtCentreId, eventTime.toLocalDate(), + withJsonPath("$.court.courtSites[0].courtRooms[0].cases.casesDetails[0].defendants", hasSize(0))); + final JsonObject publishCourtListJsonObject = buildPublishCourtListJsonString(courtCentreId, "26"); final PublishCourtListSteps publishCourtListSteps = new PublishCourtListSteps(); diff --git a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/steps/CourtListRestrictionSteps.java b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/steps/CourtListRestrictionSteps.java index 65423c2ed..cf1ddea89 100644 --- a/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/steps/CourtListRestrictionSteps.java +++ b/hearing-integration-test/src/test/java/uk/gov/moj/cpp/hearing/steps/CourtListRestrictionSteps.java @@ -3,11 +3,22 @@ import static com.google.common.collect.Lists.newArrayList; import static com.jayway.jsonpath.matchers.JsonPathMatchers.isJson; import static com.jayway.jsonpath.matchers.JsonPathMatchers.withJsonPath; +import static java.text.MessageFormat.format; import static java.util.UUID.fromString; import static java.util.UUID.randomUUID; +import static java.util.concurrent.TimeUnit.SECONDS; +import static javax.ws.rs.core.Response.Status.OK; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static uk.gov.justice.services.common.http.HeaderConstants.USER_ID; +import static uk.gov.justice.services.test.utils.core.http.BaseUriProvider.getBaseUri; +import static uk.gov.justice.services.test.utils.core.http.RequestParamsBuilder.requestParams; +import static uk.gov.justice.services.test.utils.core.http.RestPoller.poll; +import static uk.gov.justice.services.test.utils.core.matchers.ResponsePayloadMatcher.payload; +import static uk.gov.justice.services.test.utils.core.matchers.ResponseStatusMatcher.status; +import static uk.gov.moj.cpp.hearing.utils.WireMockStubUtils.setupAsAuthorizedAndSystemUser; import static uk.gov.justice.hearing.courts.CourtListRestricted.courtListRestricted; import static uk.gov.justice.services.test.utils.core.messaging.MetadataBuilderFactory.metadataWithRandomUUID; import static uk.gov.moj.cpp.hearing.it.UseCases.asDefault; @@ -32,6 +43,10 @@ import uk.gov.moj.cpp.hearing.test.CommandHelpers; import java.security.NoSuchAlgorithmException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.Optional; @@ -42,8 +57,11 @@ import io.restassured.path.json.JsonPath; import com.fasterxml.jackson.databind.ObjectMapper; +import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.jupiter.api.BeforeEach; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class CourtListRestrictionSteps extends AbstractIT { @@ -87,6 +105,103 @@ public JsonPath hearingEventsCourtListRestrictedReceived(final Matcher> matche } } + /** + * Polls the publish-side query {@code hearing.latest-hearings-by-court-centres} until the + * restriction projection has reached the expected state. The publish flow internally consumes + * the same query — once it reflects the toggle, the next {@code publish-court-list} command is + * guaranteed to see the same state. + *
+ * Required because {@link #hearingEventsCourtListRestrictedReceived(Matcher)} only confirms the + * hearing event was emitted; the listener that projects it into the JPA entity runs in a + * separate transaction and may lag behind the publish command if not waited for. + *
+ * A hearing-visibility precondition ({@code courtRoomId notNullValue}) is prepended to the + * caller's matcher to prevent the poll from short-circuiting on the empty/not-yet-projected + * state — without this, lenient matchers such as {@code hasNoJsonPath(...)} or + * {@code withJsonPath(..., hasSize(0))} would match an empty {@code {}} response and return + * before the restriction event has actually been processed. + */ + public void waitForRestrictionProjection(final String courtCentreId, + final LocalDate hearingDate, + final Matcher super com.jayway.jsonpath.ReadContext> expectedPayload) { + setupAsAuthorizedAndSystemUser(USER_ID_VALUE_AS_ADMIN); + final String queryPart = format(ENDPOINT_PROPERTIES.getProperty("hearing.latest-hearings-by-court-centres"), courtCentreId, hearingDate); + final String searchCourtListUrl = String.format("%s/%s", getBaseUri(), queryPart); + + poll(requestParams(searchCourtListUrl, "application/vnd.hearing.latest-hearings-by-court-centres+json") + .withHeader(USER_ID, getLoggedInSystemUserHeader())) + .timeout(60, SECONDS) + .pollInterval(1, SECONDS) + .until(status().is(OK), payload().isJson(allOf( + withJsonPath("$.court.courtSites[0].courtRooms[0].courtRoomId", notNullValue()), + expectedPayload))); + } + + /** + * Polls the view-store DB directly until the just-created hearing has BOTH + * {@code ha_hearing} AND {@code ha_hearing_day} rows for the given courtCentreId/date. + * MUST be called after {@code createHearingEvent*} and BEFORE any {@code hide*FromXhibit} + * call or any {@code sendPublishCourtListCommand}. + * + *
1. The listener silent-drop race. + * Without this wait, the {@code public.listing.court-list-restricted} → ... → + * {@code hearing.event.court-list-restricted} chain can reach + * {@link uk.gov.moj.cpp.hearing.event.listener.CourtListRestrictionEventListener} before the + * hearing-creation projection has committed to {@code ha_hearing}. The listener does + * {@code hearingRepository.findOptionalBy(hearingId)} and, if the row is missing, silently + * returns — the message is consumed and never replayed, the restriction is lost, the + * subsequent publish reads the un-restricted hearing, and the assertion on the redacted XML + * fails. + * + *
2. The pub-display empty-XML race. + * Even when {@code ha_hearing} is populated, the publish's pub-display query + * ({@code findHearingsByDateAndCourtCentreList}) INNER-JOINs {@code ha_hearing.hearingDays} + * filtered by date. If {@code ha_hearing_day} hasn't been projected yet when the publish + * runs, the pub-display query returns thin/empty data and the publish writes an XML with + * empty fields (empty courtname, cppurn, defendant fields, no {@code currentstatus} block). + * The web-page publish (which goes via {@code ha_hearing_event}) is unaffected and writes + * correct XML, so the test sees a mismatch where the same publish call produces correct + * web-page XML but stub pub-display XML. + * + *
Polling JDBC directly is more robust than polling either REST endpoint because both
+ * publish-side queries are gated on the same two tables; once both are populated, both
+ * publishes will see fresh data.
+ */
+ public void waitForHearingVisible(final String courtCentreId, final LocalDate hearingDate) {
+ Awaitility.await()
+ .atMost(60, SECONDS)
+ .pollInterval(500, java.util.concurrent.TimeUnit.MILLISECONDS)
+ .until(() -> hearingProjectedFor(courtCentreId, hearingDate));
+ }
+
+ private boolean hearingProjectedFor(final String courtCentreId, final LocalDate hearingDate) {
+ // Both publish paths need all three tables. Web-page goes via ha_hearing_event;
+ // pub-display additionally INNER-JOINs ha_hearing_day.
+ final String sql = String.format(
+ "SELECT count(1) FROM ha_hearing h " +
+ "INNER JOIN ha_hearing_day day ON day.hearing_id = h.id " +
+ "INNER JOIN ha_hearing_event ev ON ev.hearing_id = h.id " +
+ "WHERE h.court_centre_id = '%s' " +
+ "AND day.date = '%s' " +
+ "AND ev.event_date = '%s' " +
+ "AND ev.deleted = false",
+ courtCentreId, hearingDate, hearingDate);
+ try (final Connection connection = testJdbcConnectionProvider.getViewStoreConnection("hearing");
+ final Statement statement = connection.createStatement();
+ final ResultSet resultSet = statement.executeQuery(sql)) {
+ if (resultSet.next()) {
+ return resultSet.getInt(1) > 0;
+ }
+ } catch (final SQLException e) {
+ HEARING_VISIBILITY_LOGGER.warn("Failed to query view store for visibility check: {}", e.getMessage());
+ }
+ return false;
+ }
+
+ private static final Logger HEARING_VISIBILITY_LOGGER = LoggerFactory.getLogger(CourtListRestrictionSteps.class);
+
private void sendListingPublicEvent(final JsonObject restrictCourtListDataObject) {
sendMessage(
getPublicTopicInstance().createProducer(),
@@ -99,11 +214,12 @@ public CommandHelpers.InitiateHearingCommandHelper createHearingEvent(final UUID
final UUID eventDefinitionId, final ZonedDateTime eventTime, final Optional