diff --git a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessSpec.java b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessSpec.java new file mode 100644 index 0000000000..916a65597b --- /dev/null +++ b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessSpec.java @@ -0,0 +1,77 @@ +package uk.gov.hmcts.ccd.datastore.befta; + +import java.util.List; + +final class DefinitionReadinessSpec { + + private final String caseTypeId; + private final String dataStoreReadinessEventId; + private final String accessRole; + private final String userEmail; + private final String userPasswordEnvironmentVariable; + private final List requiredFields; + private final List definitionEventIds; + + DefinitionReadinessSpec(String caseTypeId, + String dataStoreReadinessEventId, + String accessRole, + String userEmail, + String userPasswordEnvironmentVariable, + List requiredFields, + List definitionEventIds) { + this.caseTypeId = caseTypeId; + this.dataStoreReadinessEventId = dataStoreReadinessEventId; + this.accessRole = accessRole; + this.userEmail = userEmail; + this.userPasswordEnvironmentVariable = userPasswordEnvironmentVariable; + this.requiredFields = List.copyOf(requiredFields); + this.definitionEventIds = List.copyOf(definitionEventIds); + } + + String caseTypeId() { + return caseTypeId; + } + + String dataStoreReadinessEventId() { + return dataStoreReadinessEventId; + } + + String accessRole() { + return accessRole; + } + + String userEmail() { + return userEmail; + } + + String userPasswordEnvironmentVariable() { + return userPasswordEnvironmentVariable; + } + + List requiredFields() { + return requiredFields; + } + + List definitionEventIds() { + return definitionEventIds; + } + + static final class RequiredField { + + private final String id; + private final String type; + + RequiredField(String id, String type) { + this.id = id; + this.type = type; + } + + String id() { + return id; + } + + String type() { + return type; + } + } +} diff --git a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifier.java b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifier.java new file mode 100644 index 0000000000..976709f380 --- /dev/null +++ b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifier.java @@ -0,0 +1,97 @@ +package uk.gov.hmcts.ccd.datastore.befta; + +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class DefinitionReadinessVerifier { + + private DefinitionReadinessVerifier() { + } + + static void verify(Response response, DefinitionReadinessSpec spec) { + if (response.getStatusCode() != 200) { + throw new IllegalStateException("Could not verify " + spec.caseTypeId() + + " after data setup. Definition Store returned HTTP " + response.getStatusCode() + + ": " + response.getBody().asString()); + } + + verify(response.jsonPath(), spec); + } + + static void verify(JsonPath jsonPath, DefinitionReadinessSpec spec) { + verifyVisibleFields(jsonPath, spec); + spec.requiredFields().forEach(field -> verifyCrudAccess(jsonPath, spec, field)); + spec.definitionEventIds().forEach(eventId -> verifyRequiredEvent(jsonPath, spec, eventId)); + } + + static void verifyVisibleFields(JsonPath jsonPath, DefinitionReadinessSpec spec) { + spec.requiredFields().forEach(field -> verifyRequiredField(jsonPath, spec, field)); + } + + private static void verifyRequiredField(JsonPath jsonPath, + DefinitionReadinessSpec spec, + DefinitionReadinessSpec.RequiredField field) { + String fieldPath = String.format("case_fields.find { it.id == '%s' }", field.id()); + Object caseField = jsonPath.get(fieldPath); + if (!(caseField instanceof Map)) { + throw new IllegalStateException(spec.caseTypeId() + " is missing required " + field.type() + + " field " + field.id() + " after data setup."); + } + + String fieldType = jsonPath.getString(fieldPath + ".field_type.type"); + if (!field.type().equals(fieldType)) { + throw new IllegalStateException(spec.caseTypeId() + " field " + field.id() + + " must be type " + field.type() + " after data setup but was " + fieldType + "."); + } + } + + private static void verifyCrudAccess(JsonPath jsonPath, + DefinitionReadinessSpec spec, + DefinitionReadinessSpec.RequiredField field) { + String aclPath = String.format( + "case_fields.find { it.id == '%s' }.acls.find { it.role == '%s' }", + field.id(), + spec.accessRole() + ); + Object acl = jsonPath.get(aclPath); + if (!(acl instanceof Map)) { + throw new IllegalStateException(spec.caseTypeId() + " field " + field.id() + + " is missing " + spec.accessRole() + " ACL after data setup."); + } + + if (!Boolean.TRUE.equals(jsonPath.getBoolean(aclPath + ".create")) + || !Boolean.TRUE.equals(jsonPath.getBoolean(aclPath + ".read")) + || !Boolean.TRUE.equals(jsonPath.getBoolean(aclPath + ".update")) + || !Boolean.TRUE.equals(jsonPath.getBoolean(aclPath + ".delete"))) { + throw new IllegalStateException(spec.caseTypeId() + " field " + field.id() + + " must grant CRUD to " + spec.accessRole() + " after data setup."); + } + } + + private static void verifyRequiredEvent(JsonPath jsonPath, DefinitionReadinessSpec spec, String eventId) { + String eventPath = String.format("events.find { it.id == '%s' }", eventId); + Object event = jsonPath.get(eventPath); + if (!(event instanceof Map)) { + throw new IllegalStateException(spec.caseTypeId() + " is missing required event " + + eventId + " after data setup."); + } + + Set activeEventFields = jsonStringSet(jsonPath, eventPath + ".case_fields.case_field_id"); + spec.requiredFields().forEach(field -> { + if (!activeEventFields.contains(field.id())) { + throw new IllegalStateException(spec.caseTypeId() + " event " + eventId + + " is missing required " + field.type() + " field " + field.id() + " after data setup."); + } + }); + } + + private static Set jsonStringSet(JsonPath jsonPath, String path) { + List values = jsonPath.getList(path, String.class); + return values == null ? Collections.emptySet() : Set.copyOf(values); + } +} diff --git a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifierTest.java b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifierTest.java new file mode 100644 index 0000000000..8b6fb07b8d --- /dev/null +++ b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/DefinitionReadinessVerifierTest.java @@ -0,0 +1,172 @@ +package uk.gov.hmcts.ccd.datastore.befta; + +import io.restassured.path.json.JsonPath; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DefinitionReadinessVerifierTest { + + private static final DefinitionReadinessSpec SPEC = new DefinitionReadinessSpec( + "FT_MasterCaseType", + "createCase", + "caseworker-befta_master", + "master.caseworker@gmail.com", + "CCD_CASEWORKER_AUTOTEST_PASSWORD", + List.of( + new DefinitionReadinessSpec.RequiredField("RichTextAreaField", "RichTextArea"), + new DefinitionReadinessSpec.RequiredField("RichTextAreaMinField", "RichTextArea") + ), + List.of("createCase", "updateCase") + ); + + @Test + void shouldAcceptDefinitionWithRequiredConfiguration() { + assertThatCode(() -> DefinitionReadinessVerifier.verify(JsonPath.from(validDefinition()), SPEC)) + .doesNotThrowAnyException(); + } + + @Test + void shouldAcceptVisibleFieldsWithoutDefinitionAclOrEventMappings() { + assertThatCode(() -> DefinitionReadinessVerifier.verifyVisibleFields(JsonPath.from(visibleFields()), SPEC)) + .doesNotThrowAnyException(); + } + + @Test + void shouldRejectDefinitionMissingRequiredField() { + String definition = validDefinition().replace( + "\"id\": \"RichTextAreaMinField\"", + "\"id\": \"OtherField\"" + ); + + assertThatThrownBy(() -> DefinitionReadinessVerifier.verify(JsonPath.from(definition), SPEC)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("missing required RichTextArea field RichTextAreaMinField"); + } + + @Test + void shouldRejectDefinitionWhenFieldBaseTypeIsWrong() { + String definition = validDefinition().replace( + "\"type\": \"RichTextArea\"", + "\"type\": \"Text\"" + ); + + assertThatThrownBy(() -> DefinitionReadinessVerifier.verify(JsonPath.from(definition), SPEC)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("must be type RichTextArea after data setup but was Text"); + } + + @Test + void shouldRejectDefinitionMissingCaseworkerCrudAccess() { + String definition = validDefinition().replace("\"update\": true", "\"update\": false"); + + assertThatThrownBy(() -> DefinitionReadinessVerifier.verify(JsonPath.from(definition), SPEC)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("must grant CRUD to caseworker-befta_master"); + } + + @Test + void shouldRejectDefinitionMissingEventFieldMapping() { + String definition = validDefinition().replace( + "\"case_field_id\": \"RichTextAreaMinField\"", + "\"case_field_id\": \"OtherField\"" + ); + + assertThatThrownBy(() -> DefinitionReadinessVerifier.verify(JsonPath.from(definition), SPEC)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("is missing required RichTextArea field RichTextAreaMinField"); + } + + private String visibleFields() { + return """ + { + "case_fields": [ + { + "id": "RichTextAreaField", + "field_type": { + "id": "RichTextArea", + "type": "RichTextArea" + } + }, + { + "id": "RichTextAreaMinField", + "field_type": { + "id": "RichTextAreaMinField-3c359e9e-3b68-43a7-9948-2001ac9b4daf", + "type": "RichTextArea", + "min": 10 + } + } + ] + } + """; + } + + private String validDefinition() { + return """ + { + "case_fields": [ + { + "id": "RichTextAreaField", + "field_type": { + "id": "RichTextArea", + "type": "RichTextArea" + }, + "acls": [ + { + "role": "caseworker-befta_master", + "create": true, + "read": true, + "update": true, + "delete": true + } + ] + }, + { + "id": "RichTextAreaMinField", + "field_type": { + "id": "RichTextAreaMinField-3c359e9e-3b68-43a7-9948-2001ac9b4daf", + "type": "RichTextArea", + "min": 10 + }, + "acls": [ + { + "role": "caseworker-befta_master", + "create": true, + "read": true, + "update": true, + "delete": true + } + ] + } + ], + "events": [ + { + "id": "createCase", + "case_fields": [ + { + "case_field_id": "RichTextAreaField" + }, + { + "case_field_id": "RichTextAreaMinField" + } + ] + }, + { + "id": "updateCase", + "case_fields": [ + { + "case_field_id": "RichTextAreaField" + }, + { + "case_field_id": "RichTextAreaMinField" + } + ] + } + ] + } + """; + } +} diff --git a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/HighLevelDataSetupApp.java b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/HighLevelDataSetupApp.java index be3033612b..8f1a31e436 100644 --- a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/HighLevelDataSetupApp.java +++ b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/HighLevelDataSetupApp.java @@ -1,12 +1,46 @@ package uk.gov.hmcts.ccd.datastore.befta; +import uk.gov.hmcts.befta.BeftaMain; +import uk.gov.hmcts.befta.DefaultTestAutomationAdapter; +import uk.gov.hmcts.befta.auth.UserTokenProviderConfig; +import uk.gov.hmcts.befta.data.UserData; import uk.gov.hmcts.befta.dse.ccd.CcdEnvironment; import uk.gov.hmcts.befta.dse.ccd.DataLoaderToDefinitionStore; +import uk.gov.hmcts.befta.util.EnvironmentVariableUtils; +import uk.gov.hmcts.ccd.v2.V2; +import java.util.List; import java.util.Locale; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; public class HighLevelDataSetupApp extends DataLoaderToDefinitionStore { + private static final String MASTER_CASEWORKER_EMAIL = "master.caseworker@gmail.com"; + private static final String CASEWORKER_AUTOTEST_PASSWORD_ENV = "CCD_CASEWORKER_AUTOTEST_PASSWORD"; + private static final int DATA_STORE_READINESS_ATTEMPTS = 45; + private static final long DATA_STORE_READINESS_POLL_INTERVAL_MILLIS = 1_000L; + private static final DefinitionReadinessSpec RICH_TEXT_AREA_READINESS_SPEC = new DefinitionReadinessSpec( + "FT_MasterCaseType", + "createCase", + "caseworker-befta_master", + MASTER_CASEWORKER_EMAIL, + CASEWORKER_AUTOTEST_PASSWORD_ENV, + List.of( + new DefinitionReadinessSpec.RequiredField("RichTextAreaField", "RichTextArea"), + new DefinitionReadinessSpec.RequiredField("RichTextAreaMinField", "RichTextArea") + ), + List.of("createCase", "updateCase") + ); + private static final List REQUIRED_DEFINITIONS = List.of( + RICH_TEXT_AREA_READINESS_SPEC + ); + public HighLevelDataSetupApp(CcdEnvironment dataSetupEnvironment) { super(dataSetupEnvironment); } @@ -19,7 +53,97 @@ public static void main(String[] args) throws Throwable { @Override protected boolean shouldTolerateDataSetupFailure() { - return true; + return false; + } + + @Override + public synchronized void loadDataIfNotLoadedVeryRecently() { + super.loadDataIfNotLoadedVeryRecently(); + REQUIRED_DEFINITIONS.forEach(this::verifyDefinitionIsAvailable); + REQUIRED_DEFINITIONS.forEach(this::waitUntilDataStoreDefinitionIsReady); + } + + private void verifyDefinitionIsAvailable(DefinitionReadinessSpec spec) { + RestAssured.useRelaxedHTTPSValidation(); + + Response response = asAutoTestImporter() + .when() + .get("/api/data/case-type/{caseTypeId}", spec.caseTypeId()); + + DefinitionReadinessVerifier.verify(response, spec); + } + + private void waitUntilDataStoreDefinitionIsReady(DefinitionReadinessSpec spec) { + Supplier asReadinessUser = asReadinessUser(spec); + RuntimeException lastFailure = null; + + for (int attempt = 1; attempt <= DATA_STORE_READINESS_ATTEMPTS; attempt++) { + try { + Response response = asReadinessUser.get() + .given() + .pathParam("caseTypeId", spec.caseTypeId()) + .pathParam("triggerId", spec.dataStoreReadinessEventId()) + .accept(V2.MediaType.CASE_TYPE_UPDATE_VIEW_EVENT) + .header(V2.EXPERIMENTAL_HEADER, "true") + .when() + .get("/internal/case-types/{caseTypeId}/event-triggers/{triggerId}?ignore-warning=true"); + + verifyDataStoreStartTrigger(response, spec); + return; + } catch (RuntimeException e) { + lastFailure = e; + if (attempt < DATA_STORE_READINESS_ATTEMPTS) { + waitBeforeNextDataStoreReadinessAttempt(); + } + } + } + + throw new IllegalStateException("Data Store did not serve the required fields for " + + spec.caseTypeId() + " " + spec.dataStoreReadinessEventId() + " after " + + DATA_STORE_READINESS_ATTEMPTS + " attempts. The service is likely still using a cached case definition " + + "from before highLevelDataSetup completed.", lastFailure); + } + + private void verifyDataStoreStartTrigger(Response response, DefinitionReadinessSpec spec) { + if (response.getStatusCode() != 200) { + throw new IllegalStateException("Could not verify Data Store start trigger for " + + spec.caseTypeId() + " " + spec.dataStoreReadinessEventId() + + ". Data Store returned HTTP " + response.getStatusCode() + ": " + response.getBody().asString()); + } + + DefinitionReadinessVerifier.verifyVisibleFields(response.jsonPath(), spec); + } + + private Supplier asReadinessUser(DefinitionReadinessSpec spec) { + DefaultTestAutomationAdapter adapter = new DefaultTestAutomationAdapter(); + UserData caseworker = new UserData( + spec.userEmail(), + EnvironmentVariableUtils.getRequiredVariable(spec.userPasswordEnvironmentVariable()) + ); + + try { + adapter.authenticate(caseworker, UserTokenProviderConfig.DEFAULT_INSTANCE.getClientId()); + } catch (ExecutionException e) { + throw new IllegalStateException("Could not authenticate " + spec.userEmail() + + " for Data Store definition readiness check.", e); + } + + String s2sToken = adapter.getNewS2STokenWithEnvVars("CCD_API_GATEWAY_S2S_ID", "CCD_API_GATEWAY_S2S_KEY"); + + return () -> RestAssured.given(new RequestSpecBuilder() + .setBaseUri(BeftaMain.getConfig().getTestUrl()) + .build()) + .header("Authorization", "Bearer " + caseworker.getAccessToken()) + .header("ServiceAuthorization", s2sToken); + } + + private void waitBeforeNextDataStoreReadinessAttempt() { + try { + Thread.sleep(DATA_STORE_READINESS_POLL_INTERVAL_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for Data Store definition readiness.", e); + } } } diff --git a/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.1.td.json b/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.1.td.json index 1bd69991e9..40c7e44183 100644 --- a/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.1.td.json +++ b/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.1.td.json @@ -16,7 +16,7 @@ "user": { "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" }, "expectedResponse": { diff --git a/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.2.td.json b/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.2.td.json index 40a5bb8e81..61cbb76c2e 100644 --- a/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.2.td.json +++ b/src/aat/resources/features/F-127 - Even Enabling Condition/S-127.2.td.json @@ -16,7 +16,7 @@ "user": { "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" }, "expectedResponse": { diff --git a/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition.td.json b/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition.td.json index e6c1b5fb4f..288f48a7b3 100644 --- a/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition.td.json +++ b/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition.td.json @@ -13,7 +13,7 @@ "user": { "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" }, "request": { diff --git a/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition_NotMatch.td.json b/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition_NotMatch.td.json index e5dccd52f5..0eea572186 100644 --- a/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition_NotMatch.td.json +++ b/src/aat/resources/features/F-127 - Even Enabling Condition/common/FT_Create_Case_EventEnablingCondition_NotMatch.td.json @@ -13,7 +13,7 @@ "user": { "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" }, "request": { diff --git a/src/aat/resources/features/F-127 - Even Enabling Condition/common/S-127-GetToken_CaseCreate.td.json b/src/aat/resources/features/F-127 - Even Enabling Condition/common/S-127-GetToken_CaseCreate.td.json index bac913832d..2d8c41dde2 100644 --- a/src/aat/resources/features/F-127 - Even Enabling Condition/common/S-127-GetToken_CaseCreate.td.json +++ b/src/aat/resources/features/F-127 - Even Enabling Condition/common/S-127-GetToken_CaseCreate.td.json @@ -17,7 +17,7 @@ "users": { "invokingUser": { "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" } }, diff --git a/src/aat/resources/features/common/users/BeftaMasterCaseworker.td.json b/src/aat/resources/features/common/users/BeftaMasterCaseworker.td.json index bd3e6c32ba..79140beb42 100644 --- a/src/aat/resources/features/common/users/BeftaMasterCaseworker.td.json +++ b/src/aat/resources/features/common/users/BeftaMasterCaseworker.td.json @@ -2,5 +2,5 @@ "_guid_": "BeftaMasterCaseworker", "username": "master.caseworker@gmail.com", - "password": "[[$CCD_BEFTA_MASTER_CASEWORKER_PWD]]" + "password": "[[$CCD_CASEWORKER_AUTOTEST_PASSWORD]]" }