diff --git a/.github/workflows/ci-draft.yml b/.github/workflows/ci-draft.yml index b186b31..d2e188f 100644 --- a/.github/workflows/ci-draft.yml +++ b/.github/workflows/ci-draft.yml @@ -143,22 +143,22 @@ jobs: BASE_IMAGE=openjdk:21-jdk-slim JAR_FILENAME=${{ needs.Build.outputs.artefact_name }}.jar - # https://github.com/marketplace/actions/azure-pipelines-action - Deploy: - needs: [ Build, Artefact-Version ] - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Trigger ADO pipeline - uses: Azure/pipelines@v1.2 - with: - azure-devops-project-url: 'https://dev.azure.com/hmcts-cpp/cpp-apps' - azure-pipeline-name: 'cp-gh-artifact-to-acr' - azure-devops-token: ${{ secrets.HMCTS_ADO_PAT }} - azure-pipeline-variables: >- - { - "GROUP_ID" : "uk.gov.hmcts.cp", - "ARTIFACT_ID" : "${{ github.repository }}", - "ARTIFACT_VERSION" : "${{ needs.Artefact-Version.outputs.draft_version}}" - } +# # https://github.com/marketplace/actions/azure-pipelines-action +# Deploy: +# needs: [ Build, Artefact-Version ] +# runs-on: ubuntu-latest +# if: github.event_name == 'push' +# +# steps: +# - name: Trigger ADO pipeline +# uses: Azure/pipelines@v1.2 +# with: +# azure-devops-project-url: 'https://dev.azure.com/hmcts-cpp/cpp-apps' +# azure-pipeline-name: 'cp-gh-artifact-to-acr' +# azure-devops-token: ${{ secrets.HMCTS_ADO_PAT }} +# azure-pipeline-variables: >- +# { +# "GROUP_ID" : "uk.gov.hmcts.cp", +# "ARTIFACT_ID" : "${{ github.repository }}", +# "ARTIFACT_VERSION" : "${{ needs.Artefact-Version.outputs.draft_version}}" +# } diff --git a/build.gradle b/build.gradle index d10513d..8d33187 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,11 @@ configurations { integrationTestImplementation.extendsFrom testImplementation integrationTestRuntimeOnly.extendsFrom runtimeOnly + + // Ensure testRuntimeClasspath can be resolved for agent injection + testRuntimeClasspath { + canBeResolved = true + } } @@ -78,6 +83,11 @@ tasks.named('test') { useJUnitPlatform() systemProperty 'API_SPEC_VERSION', project.version failFast = true + // Mockito must be added as an agent, see: + // https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3 + jvmArgs += [ + "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('mockito-core') }}", '-Xshare:off' + ] testLogging { events "passed", "skipped", "failed" exceptionFormat = 'full' @@ -94,14 +104,40 @@ tasks.register('functional', Test) { group = "Verification" testClassesDirs = sourceSets.functionalTest.output.classesDirs classpath = sourceSets.functionalTest.runtimeClasspath + useJUnitPlatform() + failFast = true + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = 'full' + showStandardStreams = true + } + reports { + junitXml.required.set(true) // For CI tools (e.g. Jenkins, GitHub Actions) + html.required.set(true) // Human-readable browser report + } } - tasks.register('integration', Test) { description = "Runs integration tests" group = "Verification" testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath + useJUnitPlatform() failFast = true + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = 'full' + showStandardStreams = true + } + reports { + junitXml.required.set(true) // For CI tools (e.g. Jenkins, GitHub Actions) + html.required.set(true) // Human-readable browser report + } +} + +tasks.named('build') { + dependsOn tasks.named('test') + dependsOn tasks.named('integration') +// dependsOn tasks.named('functional') disabling until functional tests are implemented } tasks.named('jacocoTestReport') { @@ -113,11 +149,6 @@ tasks.named('jacocoTestReport') { } } -tasks.named('check') { - dependsOn tasks.named('integration') - dependsOn tasks.named('functional') -} - // check dependencies upon release ONLY tasks.named("dependencyUpdates").configure { def isNonStable = { String version -> @@ -200,7 +231,7 @@ application { } ext { - apiCourtScheduleVersion="0.3.0" + apiCourtScheduleVersion = "0.3.3" log4JVersion = "2.24.3" logbackVersion = "1.5.18" lombokVersion = "1.18.38" @@ -216,8 +247,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-json' - implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:3.1.11' - implementation 'com.azure.spring:spring-cloud-azure-trace-sleuth:4.20.0' + implementation platform('io.micrometer:micrometer-tracing-bom:latest.release') + implementation 'io.micrometer:micrometer-tracing' + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + implementation 'io.micrometer:micrometer-registry-azure-monitor' + implementation 'com.azure:azure-monitor-opentelemetry-exporter:1.0.0-beta.17' implementation 'net.logstash.logback:logstash-logback-encoder:8.1' implementation group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: log4JVersion @@ -232,6 +266,8 @@ dependencies { annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion testImplementation(platform('org.junit:junit-bom:5.12.2')) + testImplementation 'org.mockito:mockito-core:5.18.0' + testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.5.0', { exclude group: 'junit', module: 'junit' diff --git a/src/functionalTest/java/uk/gov/hmcts/cp/controllers/SampleFunctionalTest.java b/src/functionalTest/java/uk/gov/hmcts/cp/controllers/SampleFunctionalTest.java deleted file mode 100644 index a532758..0000000 --- a/src/functionalTest/java/uk/gov/hmcts/cp/controllers/SampleFunctionalTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package uk.gov.hmcts.cp.controllers; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.response.Response; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; - -import static io.restassured.RestAssured.given; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -class SampleFunctionalTest { - protected static final String CONTENT_TYPE_VALUE = "application/json"; - - @Value("${TEST_URL:http://localhost:8080}") - private String testUrl; - - @BeforeEach - public void setUp() { - RestAssured.baseURI = testUrl; - RestAssured.useRelaxedHTTPSValidation(); - } - - @Test - void functionalTest() { - Response response = given() - .contentType(ContentType.JSON) - .when() - .get() - .then() - .extract().response(); - - Assertions.assertEquals(200, response.statusCode()); - Assertions.assertTrue(response.asString().startsWith("Welcome")); - } -} diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerIT.java b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerIT.java new file mode 100644 index 0000000..0567721 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerIT.java @@ -0,0 +1,76 @@ +package uk.gov.hmcts.cp.controllers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@AutoConfigureMockMvc +class CourtScheduleControllerIT { + private static final Logger log = LoggerFactory.getLogger(CourtScheduleControllerIT.class); + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldReturnOkWhenValidUrnIsProvided() throws Exception { + String caseUrn = "test-case-urn"; + CourtScheduleResponse expectedResponse = CourtScheduleResponse.builder().build(); + + mockMvc.perform(get("/case/{case_urn}/courtschedule", caseUrn) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(result -> { + // You may need to adjust this depending on the actual fields in CourtScheduleResponse + String responseBody = result.getResponse().getContentAsString(); + log.info("Response: {}", responseBody); + JsonNode jsonBody = new ObjectMapper().readTree(result.getResponse().getContentAsString()); + + + assertEquals("courtSchedule", jsonBody.fieldNames().next()); + JsonNode courtSchedule = jsonBody.get("courtSchedule"); + assertTrue(courtSchedule.isArray()); + assertEquals(1, courtSchedule.size()); + + JsonNode hearings = courtSchedule.get(0).get("hearings"); + assertTrue(hearings.isArray()); + assertEquals(1, hearings.size()); + + JsonNode hearing = hearings.get(0); + UUID hearingId = UUID.fromString(hearing.get("hearingId").asText()); + assertNotNull(hearingId); + assertEquals("Requires interpreter", hearing.get("listNote").asText()); + assertEquals("Sentencing for theft case", hearing.get("hearingDescription").asText()); + assertEquals("Trial", hearing.get("hearingType").asText()); + + JsonNode courtSittings = hearing.get("courtSittings"); + assertTrue(courtSittings.isArray()); + assertEquals(1, courtSittings.size()); + + JsonNode sitting = courtSittings.get(0); + assertEquals("Central Criminal Court", sitting.get("courtHouse").asText()); + assertNotNull(sitting.get("sittingStart").asText()); + assertNotNull(sitting.get("sittingEnd").asText()); + UUID judiciaryId = UUID.fromString(sitting.get("judiciaryId").asText()); + assertNotNull(sitting.get("judiciaryId").asText()); + log.info("Response Object: {}", jsonBody); + }); + } +} \ No newline at end of file diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java deleted file mode 100644 index 9404ffe..0000000 --- a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java +++ /dev/null @@ -1,31 +0,0 @@ -package uk.gov.hmcts.cp.controllers; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -@SpringBootTest -@AutoConfigureMockMvc -class CourtScheduleControllerITest { - - @Autowired - private MockMvc mockMvc; - - @DisplayName("Should /case/{case_urn}/courtschedule request with 200 response code") - @Test - void shouldCallActuatorAndGet200() throws Exception { - mockMvc.perform(get("/case/123/courtschedule")) - .andDo(print()) - .andExpect(status().isOk()); - } -} diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationIT.java similarity index 97% rename from src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationTest.java rename to src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationIT.java index 23f81d6..d0a78a0 100644 --- a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationIT.java @@ -19,7 +19,7 @@ @ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc -class RootControllerIntegrationTest { +class RootControllerIntegrationIT { @Autowired private MockMvc mockMvc; diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java b/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java deleted file mode 100644 index adb8dd7..0000000 --- a/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package uk.gov.hmcts.cp.openapi; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Paths; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Built-in feature which saves service's swagger specs in temporary directory. - * Each CI run on master should automatically save and upload (if updated) - * documentation. - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureMockMvc -class OpenAPIPublisherTest { - - @Autowired - private MockMvc mvc; - - @DisplayName("Generate swagger documentation") - @Test - void generateDocs() throws Exception { - byte[] specs = mvc.perform(get("/v3/api-docs")) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsByteArray(); - - String tempDir = System.getProperty("java.io.tmpdir"); - String path = Paths.get(tempDir, "openapi-specs.json").toString(); - - try (OutputStream outputStream = Files.newOutputStream(Paths.get(path))) { - outputStream.write(specs); - } - assert Files.exists(Paths.get(path)); - } -} diff --git a/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java b/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java index 76d6f07..d435954 100644 --- a/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java +++ b/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; @@ -26,17 +27,19 @@ public ResponseEntity getCourtScheduleByCaseUrn(String ca CourtScheduleResponse courtScheduleResponse; try { sanitizedCaseUrn = sanitizeCaseUrn(caseUrn); - courtScheduleResponse = courtScheduleService.getCourtScheduleResponse(sanitizedCaseUrn); + courtScheduleResponse = courtScheduleService.getCourtScheduleByCaseUrn(sanitizedCaseUrn); } catch (ResponseStatusException e) { log.error(e.getMessage()); - return ResponseEntity.status(e.getStatusCode()).build(); + throw e; } log.debug("Found court schedule for caseUrn: {}", sanitizedCaseUrn); - return new ResponseEntity<>(courtScheduleResponse, HttpStatus.OK); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(courtScheduleResponse); } private String sanitizeCaseUrn(String urn) { - if (urn == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "caseUrn is required");; + if (urn == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "caseUrn is required"); return StringEscapeUtils.escapeHtml4(urn); } } diff --git a/src/main/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandler.java b/src/main/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandler.java new file mode 100644 index 0000000..27834a5 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandler.java @@ -0,0 +1,36 @@ +package uk.gov.hmcts.cp.controllers; + +import io.micrometer.tracing.Tracer; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.cp.openapi.model.ErrorResponse; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Objects; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private final Tracer tracer; + + public GlobalExceptionHandler(Tracer tracer) { + this.tracer = tracer; + } + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException e) { + ErrorResponse error = ErrorResponse.builder() + .error(String.valueOf(e.getStatusCode().value())) + .message(e.getReason() != null ? e.getReason() : e.getMessage()) + .timestamp(OffsetDateTime.now(ZoneOffset.UTC)) + .traceId(Objects.requireNonNull(tracer.currentSpan()).context().traceId()) + .build(); + + return ResponseEntity + .status(e.getStatusCode()) + .body(error); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepository.java b/src/main/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepository.java new file mode 100644 index 0000000..b676193 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepository.java @@ -0,0 +1,11 @@ +package uk.gov.hmcts.cp.repositories; + +import org.springframework.stereotype.Repository; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; + +@Repository +public interface CourtScheduleRepository { + + CourtScheduleResponse getCourtScheduleByCaseUrn(String caseUrn); + +} diff --git a/src/main/java/uk/gov/hmcts/cp/repositories/InMemoryCourtScheduleRepositoryImpl.java b/src/main/java/uk/gov/hmcts/cp/repositories/InMemoryCourtScheduleRepositoryImpl.java new file mode 100644 index 0000000..9b45914 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/repositories/InMemoryCourtScheduleRepositoryImpl.java @@ -0,0 +1,40 @@ +package uk.gov.hmcts.cp.repositories; + +import org.springframework.stereotype.Component; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +@Component +public class InMemoryCourtScheduleRepositoryImpl implements CourtScheduleRepository { + + public CourtScheduleResponse getCourtScheduleByCaseUrn(String caseUrn) { + CourtScheduleResponseCourtScheduleInnerHearingsInner courtScheduleHearing = CourtScheduleResponseCourtScheduleInnerHearingsInner.builder() + .hearingId(UUID.randomUUID().toString()) + .listNote("Requires interpreter") + .hearingDescription("Sentencing for theft case") + .hearingType("Trial") + .courtSittings(List.of( + CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner.builder() + .courtHouse("Central Criminal Court") + .sittingStart(OffsetDateTime.now()) + .sittingEnd(OffsetDateTime.now().plusMinutes(60)) + .judiciaryId(UUID.randomUUID().toString()) + .build()) + ).build(); + + return CourtScheduleResponse.builder() + .courtSchedule(List.of( + CourtScheduleResponseCourtScheduleInner.builder() + .hearings(List.of(courtScheduleHearing) + ).build() + ) + ).build(); + } + +} diff --git a/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java b/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java index aba0c8f..5e0677a 100644 --- a/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java +++ b/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java @@ -1,5 +1,6 @@ package uk.gov.hmcts.cp.services; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -7,47 +8,25 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; - -import java.time.OffsetDateTime; -import java.util.List; -import java.util.UUID; +import uk.gov.hmcts.cp.repositories.CourtScheduleRepository; @Service +@RequiredArgsConstructor public class CourtScheduleService { private static final Logger log = LoggerFactory.getLogger(CourtScheduleService.class); - public CourtScheduleResponse getCourtScheduleResponse(String caseUrn) throws ResponseStatusException { + private final CourtScheduleRepository courtScheduleRepository; + + public CourtScheduleResponse getCourtScheduleByCaseUrn(String caseUrn) throws ResponseStatusException { if (StringUtils.isEmpty(caseUrn)) { log.warn("No case urn provided"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "caseUrn is required"); } log.warn("NOTE: System configured to return stubbed Court Schedule details. Ignoring provided caseUrn : {}", caseUrn); - CourtScheduleResponse stubbedCourtScheduleResponse = CourtScheduleResponse.builder() - .courtSchedule(List.of( - CourtScheduleResponseCourtScheduleInner.builder() - .hearings(List.of( - CourtScheduleResponseCourtScheduleInnerHearingsInner.builder() - .hearingId(UUID.randomUUID().toString()) - .listNote("Requires interpreter") - .hearingDescription("Sentencing for theft case") - .hearingType("Trial") - .courtSittings(List.of( - CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner.builder() - .courtHouse("Central Criminal Court") - .sittingStart(OffsetDateTime.now()) - .sittingEnd(OffsetDateTime.now().plusMinutes(60)) - .judiciaryId(UUID.randomUUID().toString()) - .build()) - ).build() - ) - ).build() - ) - ).build(); + CourtScheduleResponse stubbedCourtScheduleResponse = courtScheduleRepository.getCourtScheduleByCaseUrn(caseUrn); log.debug("Court Schedule response: {}", stubbedCourtScheduleResponse); return stubbedCourtScheduleResponse; } + } diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java b/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java index 8a450f8..b903748 100644 --- a/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java +++ b/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java @@ -6,29 +6,40 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; +import uk.gov.hmcts.cp.repositories.CourtScheduleRepository; +import uk.gov.hmcts.cp.repositories.InMemoryCourtScheduleRepositoryImpl; import uk.gov.hmcts.cp.services.CourtScheduleService; -import java.time.OffsetDateTime; -import java.util.List; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class CourtScheduleControllerTest { private static final Logger log = LoggerFactory.getLogger(CourtScheduleControllerTest.class); + private CourtScheduleRepository courtScheduleRepository; + private CourtScheduleService courtScheduleService; + private CourtScheduleController courtScheduleController; + @BeforeEach void setUp() { + courtScheduleRepository = new InMemoryCourtScheduleRepositoryImpl(); + courtScheduleService = new CourtScheduleService(courtScheduleRepository); + courtScheduleController = new CourtScheduleController(courtScheduleService); } @Test void getJudgeById_ShouldReturnJudgesWithOkStatus() { - CourtScheduleController courtScheduleController = new CourtScheduleController(new CourtScheduleService()); UUID caseUrn = UUID.randomUUID(); log.info("Calling courtScheduleController.getCourtScheduleByCaseUrn with caseUrn: {}", caseUrn); ResponseEntity response = courtScheduleController.getCourtScheduleByCaseUrn(caseUrn.toString()); @@ -63,14 +74,24 @@ void getJudgeById_ShouldReturnJudgesWithOkStatus() { } @Test - void getJudgeById_ShouldReturnBadRequestStatus() { - CourtScheduleController courtScheduleController = new CourtScheduleController(new CourtScheduleService()); + void getCourtScheduleByCaseUrn_ShouldSanitizeCaseUrn() { + String unsanitizedCaseUrn = ""; + log.info("Calling courtScheduleController.getCourtScheduleByCaseUrn with unsanitized caseUrn: {}", unsanitizedCaseUrn); - log.info("Calling courtScheduleController.getCourtScheduleByCaseUrn with null caseUrn"); - ResponseEntity response = courtScheduleController.getCourtScheduleByCaseUrn(null); + ResponseEntity response = courtScheduleController.getCourtScheduleByCaseUrn(unsanitizedCaseUrn); + assertNotNull(response); log.debug("Received response: {}", response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } - assertNotNull(response); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + @Test + void getJudgeById_ShouldReturnBadRequestStatus() { + ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> { + courtScheduleController.getCourtScheduleByCaseUrn(null); + }); + assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(exception.getReason()).isEqualTo("caseUrn is required"); + assertThat(exception.getMessage()).isEqualTo("400 BAD_REQUEST \"caseUrn is required\""); } + } \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..55e685c --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/controllers/GlobalExceptionHandlerTest.java @@ -0,0 +1,43 @@ +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.cp.controllers.GlobalExceptionHandler; +import uk.gov.hmcts.cp.openapi.model.ErrorResponse; +import io.micrometer.tracing.TraceContext; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GlobalExceptionHandlerTest { + + @Test + void handleResponseStatusException_ShouldReturnErrorResponseWithCorrectFields() { + // Arrange + Tracer tracer = mock(Tracer.class); + Span span = mock(Span.class); + TraceContext context = mock(TraceContext.class); + + when(tracer.currentSpan()).thenReturn(span); + when(span.context()).thenReturn(context); + when(context.traceId()).thenReturn("test-trace-id"); + + GlobalExceptionHandler handler = new GlobalExceptionHandler(tracer); + + String reason = "Test error"; + ResponseStatusException ex = new ResponseStatusException(HttpStatus.NOT_FOUND, reason); + + // Act + var response = handler.handleResponseStatusException(ex); + + // Assert + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + ErrorResponse error = response.getBody(); + assertNotNull(error); + assertEquals("404", error.getError()); + assertEquals(reason, error.getMessage()); + assertNotNull(error.getTimestamp()); + assertEquals("test-trace-id", error.getTraceId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepositoryTest.java b/src/test/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepositoryTest.java new file mode 100644 index 0000000..9119c65 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/repositories/CourtScheduleRepositoryTest.java @@ -0,0 +1,53 @@ +package uk.gov.hmcts.cp.repositories; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CourtScheduleRepositoryTest { + + private CourtScheduleRepository courtScheduleRepository; + + @BeforeEach + void setUp() { + courtScheduleRepository = new InMemoryCourtScheduleRepositoryImpl(); + } + + @Test + void getCourtScheduleByCaseUrn_shouldReturnCourtScheduleResponse() { + UUID caseUrn = UUID.randomUUID(); + CourtScheduleResponse response = courtScheduleRepository.getCourtScheduleByCaseUrn(caseUrn.toString()); + + assertNotNull(response.getCourtSchedule()); + assertEquals(1, response.getCourtSchedule().size()); + + CourtScheduleResponseCourtScheduleInner schedule = response.getCourtSchedule().get(0); + assertNotNull(schedule.getHearings()); + assertEquals(1, schedule.getHearings().size()); + + CourtScheduleResponseCourtScheduleInnerHearingsInner hearing = schedule.getHearings().get(0); + assertNotNull(hearing.getHearingId()); + assertEquals("Requires interpreter", hearing.getListNote()); + assertEquals("Sentencing for theft case", hearing.getHearingDescription()); + assertEquals("Trial", hearing.getHearingType()); + assertNotNull(hearing.getCourtSittings()); + assertEquals(1, hearing.getCourtSittings().size()); + + CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner sitting = + hearing.getCourtSittings().get(0); + assertEquals("Central Criminal Court", sitting.getCourtHouse()); + assertNotNull(sitting.getSittingStart()); + assertTrue(sitting.getSittingEnd().isAfter(sitting.getSittingStart())); + assertNotNull(sitting.getJudiciaryId()); + } + +} \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java b/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java index a41471e..d703d94 100644 --- a/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java +++ b/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java @@ -3,13 +3,16 @@ import org.junit.jupiter.api.Test; import org.springframework.web.server.ResponseStatusException; import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.repositories.CourtScheduleRepository; +import uk.gov.hmcts.cp.repositories.InMemoryCourtScheduleRepositoryImpl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class CourtScheduleServiceTest { - private final CourtScheduleService courtScheduleService = new CourtScheduleService(); + private final CourtScheduleRepository courtScheduleRepository = new InMemoryCourtScheduleRepositoryImpl(); + private final CourtScheduleService courtScheduleService = new CourtScheduleService(courtScheduleRepository); @Test void shouldReturnStubbedCourtScheduleResponse_whenValidCaseUrnProvided() { @@ -17,7 +20,7 @@ void shouldReturnStubbedCourtScheduleResponse_whenValidCaseUrnProvided() { String validCaseUrn = "123-ABC-456"; // Act - CourtScheduleResponse response = courtScheduleService.getCourtScheduleResponse(validCaseUrn); + CourtScheduleResponse response = courtScheduleService.getCourtScheduleByCaseUrn(validCaseUrn); // Assert assertThat(response).isNotNull(); @@ -34,7 +37,7 @@ void shouldThrowBadRequestException_whenCaseUrnIsNull() { String nullCaseUrn = null; // Act & Assert - assertThatThrownBy(() -> courtScheduleService.getCourtScheduleResponse(nullCaseUrn)) + assertThatThrownBy(() -> courtScheduleService.getCourtScheduleByCaseUrn(nullCaseUrn)) .isInstanceOf(ResponseStatusException.class) .hasMessageContaining("400 BAD_REQUEST") .hasMessageContaining("caseUrn is required"); @@ -46,7 +49,7 @@ void shouldThrowBadRequestException_whenCaseUrnIsEmpty() { String emptyCaseUrn = ""; // Act & Assert - assertThatThrownBy(() -> courtScheduleService.getCourtScheduleResponse(emptyCaseUrn)) + assertThatThrownBy(() -> courtScheduleService.getCourtScheduleByCaseUrn(emptyCaseUrn)) .isInstanceOf(ResponseStatusException.class) .hasMessageContaining("400 BAD_REQUEST") .hasMessageContaining("caseUrn is required");