diff --git a/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoader.java b/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoader.java index 7840728..8276ee9 100644 --- a/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoader.java +++ b/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoader.java @@ -3,18 +3,16 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.parser.OpenAPIV3Parser; import io.swagger.v3.parser.core.models.SwaggerParseResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +@Slf4j public final class OpenAPIConfigurationLoader { - private static final Logger LOG = LoggerFactory.getLogger(OpenAPIConfigurationLoader.class); - // Match your Gradle include("*.openapi.yml") private static final String SPEC_PATH = "openapi/case-admin-doc-knowledge-api.openapi.yml"; @@ -28,7 +26,7 @@ public static OpenAPI loadOpenApiFromClasspath(final String path) { Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { if (inputStream == null) { - LOG.error("OpenAPI specification file not found on classpath: {}", path); + log.error("OpenAPI specification file not found on classpath: {}", path); throw new IllegalArgumentException("Missing resource: " + path); } @@ -37,12 +35,12 @@ public static OpenAPI loadOpenApiFromClasspath(final String path) { throw new IllegalArgumentException("OpenAPI specification is empty: " + path); } - SwaggerParseResult result = new OpenAPIV3Parser().readContents(yaml, null, null); + final SwaggerParseResult result = new OpenAPIV3Parser().readContents(yaml, null, null); if (result == null || result.getOpenAPI() == null) { - String messages = (result != null && result.getMessages() != null) + final String messages = (result != null && result.getMessages() != null) ? String.join("; ", result.getMessages()) : "Unknown parser error"; - LOG.error("Failed to parse OpenAPI spec at {}: {}", path, messages); + log.error("Failed to parse OpenAPI spec at {}: {}", path, messages); throw new IllegalStateException("Failed to parse OpenAPI spec at " + path + ": " + messages); } @@ -53,7 +51,9 @@ public static OpenAPI loadOpenApiFromClasspath(final String path) { } } - /** Convenience accessor using the default SPEC_PATH. */ + /** + * Convenience accessor using the default SPEC_PATH. + */ public OpenAPI openAPI() { return loadOpenApiFromClasspath(SPEC_PATH); } diff --git a/src/main/resources/openapi/case-admin-doc-knowledge-api.openapi.yml b/src/main/resources/openapi/case-admin-doc-knowledge-api.openapi.yml index be38e42..f8e2415 100644 --- a/src/main/resources/openapi/case-admin-doc-knowledge-api.openapi.yml +++ b/src/main/resources/openapi/case-admin-doc-knowledge-api.openapi.yml @@ -8,10 +8,6 @@ info: - The latest snapshot for a Case - An **as-of** view at a timestamp for reproducibility - A specific **version** of an answer - - Pipeline (context): IDPC documents are associated to a **Case ID**, uploaded to Azure Blob - Storage by a background Spring service, ingested, and then predefined queries/prompts are - executed by the LLM with results stored in the DB. version: 0.0.0 contact: email: no-reply@hmcts.com @@ -281,7 +277,7 @@ paths: schema: { $ref: '#/components/schemas/ErrorResponse' } # --------------------------- - # Answers (UPDATED) + # Answers # --------------------------- /answers/{caseId}/{queryId}: parameters: @@ -611,6 +607,3 @@ components: type: string format: date-time description: UTC timestamp when the error was created. - traceId: - type: string - description: Distributed trace id (from the current span) to help correlate logs/traces. diff --git a/src/test/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoaderTest.java b/src/test/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoaderTest.java deleted file mode 100644 index 5465295..0000000 --- a/src/test/java/uk/gov/hmcts/cp/config/OpenAPIConfigurationLoaderTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package uk.gov.hmcts.cp.config; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; -import io.swagger.v3.oas.models.media.Schema; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class OpenAPIConfigurationLoaderTest { - - private static final Logger log = LoggerFactory.getLogger(OpenAPIConfigurationLoaderTest.class); - - @Test - void openAPI_bean_should_have_expected_properties() { - OpenAPI openAPI = new OpenAPIConfigurationLoader().openAPI(); - assertNotNull(openAPI); - - Info info = openAPI.getInfo(); - assertNotNull(info); - assertEquals("Case Documents AI Responses API", info.getTitle()); - - // Resilient checks (wording may change slightly) - String desc = info.getDescription(); - assertNotNull(desc); - String lower = desc.toLowerCase(); - assertTrue(lower.contains("versioned"), "Description should mention versioned resources"); - assertTrue(lower.contains("as-of"), "Description should mention as-of views"); - assertTrue(lower.contains("case"), "Description should mention Case context"); - assertTrue(lower.contains("ingest"), "Description should mention ingestion pipeline"); - - String apiGitHubRepository = "api-cp-crime-caseadmin-case-document-knowledge"; - String expectedVersion = System.getProperty("API_SPEC_VERSION", "0.0.0"); - log.info("API version set to: {}", expectedVersion); - assertEquals(expectedVersion, info.getVersion()); - - License license = info.getLicense(); - assertNotNull(license); - assertEquals("MIT", license.getName()); - assertEquals("https://opensource.org/licenses/MIT", license.getUrl()); - - assertNotNull(info.getContact()); - assertEquals("no-reply@hmcts.com", info.getContact().getEmail()); - - assertNotNull(openAPI.getServers()); - assertFalse(openAPI.getServers().isEmpty()); - assertEquals( - "https://virtserver.swaggerhub.com/HMCTS-DTS/" + apiGitHubRepository + "/" + expectedVersion, - openAPI.getServers().get(0).getUrl() - ); - } - - @Test - void answerResponse_schema_should_require_userQuery_and_createdAt() { - OpenAPI openAPI = new OpenAPIConfigurationLoader().openAPI(); - Schema answer = openAPI.getComponents().getSchemas().get("AnswerResponse"); - assertNotNull(answer, "AnswerResponse schema missing"); - - Map props = answer.getProperties(); - assertNotNull(props, "AnswerResponse properties missing"); - assertTrue(props.containsKey("userQuery"), "AnswerResponse must contain userQuery"); - assertTrue(props.containsKey("createdAt"), "AnswerResponse must contain createdAt"); - - Schema createdAt = props.get("createdAt"); - assertNotNull(createdAt, "createdAt schema missing"); - assertEquals("string", createdAt.getType(), "createdAt must be a string"); - assertEquals("date-time", createdAt.getFormat(), "createdAt must have format=date-time"); - - assertNotNull(answer.getRequired(), "AnswerResponse.required missing"); - assertTrue(answer.getRequired().contains("userQuery"), "userQuery must be required"); - assertTrue(answer.getRequired().contains("createdAt"), "createdAt must be required"); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_missing_resource() { - Exception ex = assertThrows(IllegalArgumentException.class, - () -> OpenAPIConfigurationLoader.loadOpenApiFromClasspath("nonexistent-file.yaml")); - assertTrue(ex.getMessage().contains("Missing resource")); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_blank_path() { - assertThrows(IllegalArgumentException.class, - () -> OpenAPIConfigurationLoader.loadOpenApiFromClasspath(" ")); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_null_path() { - assertThrows(IllegalArgumentException.class, - () -> OpenAPIConfigurationLoader.loadOpenApiFromClasspath(null)); - } -} diff --git a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedApiContractsExistTest.java b/src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedApiContractsExistTest.java deleted file mode 100644 index daeda7a..0000000 --- a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedApiContractsExistTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package uk.gov.hmcts.cp.openapi.codegen; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; -import io.swagger.v3.oas.models.media.Schema; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.gov.hmcts.cp.config.OpenAPIConfigurationLoader; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class GeneratedApiContractsExistTest { - - private static final Logger log = LoggerFactory.getLogger(GeneratedApiContractsExistTest.class); - - @Test - void openAPI_bean_should_have_expected_properties() { - OpenAPIConfigurationLoader config = new OpenAPIConfigurationLoader(); - OpenAPI openAPI = config.openAPI(); - assertNotNull(openAPI); - - Info info = openAPI.getInfo(); - assertNotNull(info); - assertEquals("Case Documents AI Responses API", info.getTitle()); - - String desc = info.getDescription(); - assertNotNull(desc, "Info.description must be set"); - String lower = desc.toLowerCase(); - - boolean hasVersioned = lower.contains("versioned"); - boolean hasAsOf = lower.contains("as-of"); - boolean hasQueries = lower.contains("queries") || lower.contains("query"); - boolean hasAnswers = lower.contains("answers") || lower.contains("answer"); - boolean mentionsIngestion = lower.contains("ingest"); - boolean mentionsCase = lower.contains("case"); - - assertTrue(hasVersioned, "Description should mention versioned resources"); - assertTrue(hasAsOf, "Description should mention as-of views"); - assertTrue(hasQueries, "Description should mention queries"); - assertTrue(hasAnswers, "Description should mention answers"); - assertTrue(mentionsIngestion, "Description should mention ingestion"); - assertTrue(mentionsCase, "Description should mention Case context"); - - String apiGitHubRepository = "api-cp-crime-caseadmin-case-document-knowledge"; - String expectedVersion = System.getProperty("API_SPEC_VERSION", "0.0.0"); - log.info("API version set to: {}", expectedVersion); - assertEquals(expectedVersion, info.getVersion()); - - License license = info.getLicense(); - assertNotNull(license); - assertEquals("MIT", license.getName()); - assertEquals("https://opensource.org/licenses/MIT", license.getUrl()); - - assertNotNull(info.getContact()); - assertEquals("no-reply@hmcts.com", info.getContact().getEmail()); - - assertNotNull(openAPI.getServers()); - assertFalse(openAPI.getServers().isEmpty()); - assertEquals( - "https://virtserver.swaggerhub.com/HMCTS-DTS/" + apiGitHubRepository + "/" + expectedVersion, - openAPI.getServers().get(0).getUrl() - ); - } - - @Test - void answerResponse_schema_should_require_userQuery_and_createdAt() { - OpenAPI openAPI = new OpenAPIConfigurationLoader().openAPI(); - Schema answer = openAPI.getComponents().getSchemas().get("AnswerResponse"); - assertNotNull(answer, "AnswerResponse schema missing"); - - Map props = answer.getProperties(); - assertNotNull(props, "AnswerResponse properties missing"); - assertTrue(props.containsKey("userQuery"), "AnswerResponse must contain userQuery"); - assertTrue(props.containsKey("createdAt"), "AnswerResponse must contain createdAt"); - - Schema createdAt = props.get("createdAt"); - assertNotNull(createdAt, "createdAt schema missing"); - assertEquals("string", createdAt.getType(), "createdAt must be a string"); - assertEquals("date-time", createdAt.getFormat(), "createdAt must have format=date-time"); - - assertNotNull(answer.getRequired(), "AnswerResponse.required missing"); - assertTrue(answer.getRequired().contains("userQuery"), "userQuery must be required"); - assertTrue(answer.getRequired().contains("createdAt"), "createdAt must be required"); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_missing_resource() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> - OpenAPIConfigurationLoader.loadOpenApiFromClasspath("nonexistent-file.yaml") - ); - assertTrue(exception.getMessage().contains("Missing resource")); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_blank_path() { - assertThrows(IllegalArgumentException.class, () -> - OpenAPIConfigurationLoader.loadOpenApiFromClasspath(" ") - ); - } - - @Test - void loadOpenApiFromClasspath_should_throw_for_null_path() { - assertThrows(IllegalArgumentException.class, () -> - OpenAPIConfigurationLoader.loadOpenApiFromClasspath(null) - ); - } -} diff --git a/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedApiContractsExistTest.java b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedApiContractsExistTest.java new file mode 100644 index 0000000..ddcc18e --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedApiContractsExistTest.java @@ -0,0 +1,55 @@ +package uk.gov.hmcts.cp.openapi.model.cdk; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Schema; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.cp.config.OpenAPIConfigurationLoader; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class GeneratedApiContractsExistTest { + + @Test + void openAPI_bean_should_have_expected_properties() { + OpenAPI openAPI = new OpenAPIConfigurationLoader().openAPI(); + Info info = openAPI.getInfo(); + + String thisRepository = "api-cp-crime-caseadmin-case-document-knowledge"; + String expectedVersion = System.getProperty("API_SPEC_VERSION", "0.0.0"); + String expectedUrl = String.format("https://virtserver.swaggerhub.com/HMCTS-DTS/%s/%s", thisRepository, expectedVersion); + assertThat(openAPI.getServers().get(0).getUrl()).isEqualTo(expectedUrl); + + assertThat(info.getTitle()).isEqualTo("Case Documents AI Responses API"); + + assertThat(info.getDescription()).contains("Versioned"); + assertThat(info.getDescription()).contains("as-of"); + assertThat(info.getDescription()).contains("Case"); + + assertThat(info.getVersion()).isEqualTo(expectedVersion); + + assertThat(info.getLicense().getName()).isEqualTo("MIT"); + assertThat(info.getLicense().getUrl()).isEqualTo("https://opensource.org/licenses/MIT"); + + assertThat(info.getContact().getEmail()).isEqualTo("no-reply@hmcts.com"); + } + + @Test + void answerResponse_schema_should_require_userQuery_and_createdAt() { + OpenAPI openAPI = new OpenAPIConfigurationLoader().openAPI(); + Schema answerResponseSchema = openAPI.getComponents().getSchemas().get("AnswerResponse"); + + Map properties = answerResponseSchema.getProperties(); + assertThat(properties).containsKey("userQuery"); + assertThat(properties).containsKey("createdAt"); + + Schema createdAtSchema = properties.get("createdAt"); + assertThat(createdAtSchema.getType()).isEqualTo("string"); + assertThat(createdAtSchema.getFormat()).isEqualTo("date-time"); + + assertThat(answerResponseSchema.getRequired()).contains("userQuery"); + assertThat(answerResponseSchema.getRequired()).contains("createdAt"); + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedModelContractsExistTest.java b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedModelContractsExistTest.java similarity index 99% rename from src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedModelContractsExistTest.java rename to src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedModelContractsExistTest.java index e59405a..4412452 100644 --- a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/GeneratedModelContractsExistTest.java +++ b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedModelContractsExistTest.java @@ -1,4 +1,4 @@ -package uk.gov.hmcts.cp.openapi.codegen; +package uk.gov.hmcts.cp.openapi.model.cdk; import org.junit.jupiter.api.Test; diff --git a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/ExamplePayloadBindingTest.java b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedObjectMappingTest.java similarity index 81% rename from src/test/java/uk/gov/hmcts/cp/openapi/codegen/ExamplePayloadBindingTest.java rename to src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedObjectMappingTest.java index 8f590dd..201177c 100644 --- a/src/test/java/uk/gov/hmcts/cp/openapi/codegen/ExamplePayloadBindingTest.java +++ b/src/test/java/uk/gov/hmcts/cp/openapi/model/cdk/GeneratedObjectMappingTest.java @@ -1,25 +1,35 @@ -package uk.gov.hmcts.cp.openapi.codegen; +package uk.gov.hmcts.cp.openapi.model.cdk; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.jupiter.api.Test; -import uk.gov.hmcts.cp.openapi.model.cdk.AnswerResponse; -import uk.gov.hmcts.cp.openapi.model.cdk.AnswerWithLlmResponse; -import uk.gov.hmcts.cp.openapi.model.cdk.QueryStatusResponse; -import uk.gov.hmcts.cp.openapi.model.cdk.QuerySummary; import static org.assertj.core.api.Assertions.assertThat; import static uk.gov.hmcts.cp.openapi.model.cdk.QueryLifecycleStatus.ANSWER_AVAILABLE; -class ExamplePayloadBindingTest { +class GeneratedObjectMappingTest { private final ObjectMapper mapper = new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); @Test - void json_should_map_to_AnswerResponse_object() throws Exception { + void json_should_map_toErrorResponse_object() throws JsonProcessingException { + String json = "{\n" + + " \"error\": \"BAD_REQUEST\",\n" + + " \"message\": \"Bad client request\",\n" + + " \"timestamp\": \"2025-01-01T11:11:11Z\"\n" + + "}"; + ErrorResponse errorResponse = mapper.readValue(json, ErrorResponse.class); + assertThat(errorResponse.getError()).isEqualTo("BAD_REQUEST"); + assertThat(errorResponse.getMessage()).isEqualTo("Bad client request"); + assertThat(errorResponse.getTimestamp()).isEqualTo("2025-01-01T11:11:11Z"); + } + + @Test + void json_should_map_to_AnswerResponse_object() throws JsonProcessingException { String json = "{\n" + " \"queryId\": \"a1a6eeb3-06f5-4c33-ac48-c09d66991ca1\",\n" + " \"userQuery\": \"Summary of case based on witness statements\",\n" + @@ -37,7 +47,7 @@ void json_should_map_to_AnswerResponse_object() throws Exception { } @Test - void json_should_map_to_AnswerWithLimResponse_object() throws Exception { + void json_should_map_to_AnswerWithLimResponse_object() throws JsonProcessingException { String json = "{\n" + " \"queryId\": \"a1a6eeb3-06f5-4c33-ac48-c09d66991ca1\",\n" + " \"userQuery\": \"Summary of case based on witness statements\",\n" + @@ -57,7 +67,7 @@ void json_should_map_to_AnswerWithLimResponse_object() throws Exception { } @Test - void json_should_map_to_QueryStatusResponse_object() throws Exception { + void json_should_map_to_QueryStatusResponse_object() throws JsonProcessingException { String json = "{\n" + " \"asOf\": \"2025-05-01T12:00:00Z\",\n" + " \"queries\": [\n" +