diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d025e51 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@ONSdigital/census-response-management \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5f1dcc2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +# Motivation and Context + + +# What has changed + + + + +# How to test? + + + + +# Links + + + + +# Screenshots (if appropriate): diff --git a/.github/workflows/check-labels.yml b/.github/workflows/check-labels.yml new file mode 100644 index 0000000..ce34efc --- /dev/null +++ b/.github/workflows/check-labels.yml @@ -0,0 +1,24 @@ +--- +name: Label Checker +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + +jobs: + + check_labels: + name: Check labels + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: docker://onsdigital/github-pr-label-checker:v1.6.13 + with: + one_of: breaking change,feature,patch + none_of: do not merge,work in progress + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml new file mode 100644 index 0000000..d9aa016 --- /dev/null +++ b/.github/workflows/mega-linter.yml @@ -0,0 +1,52 @@ +# MegaLinter GitHub Action configuration file +# More info at https://megalinter.io +--- +name: MegaLinter + +# Trigger mega-linter at every push. Action will also be visible from +# Pull Requests to main +on: + pull_request: + branches: + - main + +env: + APPLY_FIXES: none + APPLY_FIXES_EVENT: pull_request + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + megalinter: + name: MegaLinter + runs-on: ubuntu-latest + + # Give the default GITHUB_TOKEN write permission to comment the Megalinter output onto pull requests + # and read permission to the codebase + permissions: + contents: read + pull-requests: write + + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + # MegaLinter + - name: MegaLinter + + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/latest/flavors/ + uses: oxsecurity/megalinter/flavors/formatters@v8 + + id: ml + + # All available variables are described in documentation + # https://megalinter.io/latest/config-file/ + env: + VALIDATE_ALL_CODEBASE: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..205833c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/* +target/* +*.iml +.java-version +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dfe8711 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM eclipse-temurin:21-jre-alpine + +ARG JAR_FILE=census-rm-case-api*.jar +CMD ["/opt/java/openjdk/bin/java", "-jar", "/opt/census-rm-case-api.jar"] + +# Create a system group and user without forcing UID/GID +RUN addgroup --system case-api && \ + adduser --system --ingroup case-api case-api + +USER case-api + +COPY target/$JAR_FILE /opt/census-rm-case-api.jar + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aef850b --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# Set the container runtime based on architecture, default to docker for amd64 and podman for arm64 +DOCKER ?= $(shell if [ "$$(uname -m)" = "arm64" ]; then echo podman; else echo docker; fi) + +install: + CONTAINER_CLI=$(DOCKER) mvn clean install + +build: install docker-build + +build-no-test: install-no-test docker-build + +install-no-test: + CONTAINER_CLI=$(DOCKER) mvn clean install -Dmaven.test.skip=true -Dexec.skip=true -Djacoco.skip=true + +format: + mvn spotless:apply + +format-check: + mvn spotless:check + +check: + mvn spotless:check pmd:check + +test: + CONTAINER_CLI=$(DOCKER) mvn clean verify jacoco:report + +docker-build: + $(DOCKER) build . --platform linux/amd64 -t census-rm-case-api:latest + +rebuild-java-healthcheck: + $(MAKE) -C src/test/resources/java_healthcheck rebuild-java-healthcheck diff --git a/README.md b/README.md index 19f145c..79c2ba6 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# census-rm-case-api \ No newline at end of file +# census-rm-case-api + +# Overview +This case api service provides a range of Restful endpoints that - +* Retrieve case details by case id, case ref, UPRN or QID +* Retrieve a QID by case id +* Create and return a new Uac Qid Link + +The service relies on, and makes no changes to the case schema maintained by census-rm-ddl + +# Endpoints +## Case details: + +* `GET /cases/uprn/` (returns a list) +* `GET /cases/` +* `GET /cases/qid/` +* `GET /cases/ref/` +* `GET /cases/case-details/` +* `GET /cases/postcode/` + + + +All below endpoints include an optional `caseevents` boolean query parameter (default = "false"), that can be used to specify that the JSON response includes an array of associated case events. For example: +* `GET /cases/uprn/?caseevnets=true` +* `GET /cases/?caseevents=true` +* `GET /cases/ref/?caseevents=true` + +If this query parameter is omitted these case events **will not** be returned with the case details. + +### Example Case JSON Response +```json +{ + "abpCode": "RD06", + "addressLevel": "U", + "addressLine1": "Flat 53 Francombe House", + "addressLine2": "Commercial Road", + "addressLine3": "", + "caseEvents": [], + "caseRef": "31283399", + "addressType": "HH", + "collectionExerciseId": "77c26716-5936-43e8-b56b-f5ca71765603", + "createdDateTime": "2019-10-25T08:34:34.680556Z", + "estabType": "Household", + "id": "040f4608-d054-4ae9-b12f-1eee7e0fa284", + "lad": "E06000023", + "latitude": "51.4463421", + "longitude": "-2.5924477", + "lsoa": "E01014542", + "msoa": "E02003043", + "oa": "E00073438", + "organisationName": "", + "postcode": "XX1 0XX", + "region": "E12000009", + "surveyType": "CENSUS", + "townName": "Windleybury", + "uprn": "10008677190", + "estabUprn": "103434302134" +} +``` +## QID Get and Update: +* `GET /qids/` +* `PUT /qids/link` +### Example QIDs JSON Request +```json +{ + "transactionId": "040f4608-d054-4ae9-b12f-1eee7e0fa395", + "channel": "RM", + "qidLink": { + "questionnaireId" : "Q123", + "caseId": "040f4608-d054-4ae9-b12f-1eee7e0fa173" + } +} +``` + +# Configuration + +By default settings in src/main/resources/application.yml are used to configure [census-rm-case-api](https://github.com/ONSdigital/census-rm-case-api) + +For production the configuration is overridden by the K8S apply script + +# How to run +The service requires several other services to be running started from census-rm-docker-dev + +# How to debug census-rm-case-api locally + +## Running as a docker image +* Start census-rm-docker-dev services with the following line in section caseapi | environment in rm-services.yml +* - JAVA_OPTS=-Xmx512m -Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8162 +* In IntelliJ, create a "Remote" configuration, set port = 8162 and run in debug mode + +## Running inside IntelliJ +* Stop the census-rm-case-api service if already running +* In IntelliJ, create a SpringBoot Run configuration and run in debug mode + +# Testing +## In isolation +From the project root directory, run "mvn clean install", this - +* Runs all unit tests +* Builds a new local docker image +* Brings up this image with all required services and runs all integration tests + +## With Acceptance Tests +* From census-rm-acceptance-tests, run "make test" \ No newline at end of file diff --git a/exclude-pmd.properties b/exclude-pmd.properties new file mode 100644 index 0000000..db1491a --- /dev/null +++ b/exclude-pmd.properties @@ -0,0 +1,4 @@ +# TODO: as a team, define our own custom PMD checker so that we don't have to keep adding piecemeal +# to this file + +config.uk.gov.ons.census.caseapisvc.AppConfig=AccessorMethodGeneration \ No newline at end of file diff --git a/healhtcheck.sh b/healhtcheck.sh new file mode 100644 index 0000000..3a3f391 --- /dev/null +++ b/healhtcheck.sh @@ -0,0 +1 @@ +find /tmp/case-api-healthy -mmin -1 | egrep '.*' \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6e0a8df --- /dev/null +++ b/pom.xml @@ -0,0 +1,387 @@ + + + 4.0.0 + + uk.gov.ons.census + census-rm-case-api + + 21 + 21 + docker + + 1.0-SNAPSHOT + + + + + + podman + + + env.CONTAINER_CLI + podman + + + + podman + + + + + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + + + + + uk.gov.ons.census + census-rm-common-entity-model + 0.0.1-SNAPSHOT + + + uk.gov.ons.census + census-rm-shared-sample-validation + 0.0.1-SNAPSHOT + + + com.google.cloud + spring-cloud-gcp-dependencies + 5.3.0 + pom + import + + + + + + + + + uk.gov.ons.census + census-rm-common-entity-model + 0.0.1-SNAPSHOT + + + uk.gov.ons.census + census-rm-shared-sample-validation + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + com.google.cloud + spring-cloud-gcp-starter-pubsub + + + org.springframework + spring-web + + + io.micrometer + micrometer-registry-stackdriver + 1.12.1 + + + io.micrometer + micrometer-core + 1.12.1 + + + org.postgresql + postgresql + + + org.projectlombok + lombok + 1.18.30 + provided + + + ch.qos.logback + logback-classic + 1.5.3 + + + net.logstash.logback + logstash-logback-encoder + 7.4 + + + ch.qos.logback + logback-core + 1.5.3 + + + io.hypersistence + hypersistence-utils-hibernate-63 + 3.7.0 + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.0 + + + javax.xml.bind + jaxb-api + 2.3.0 + + + net.sourceforge.pmd + pmd-java + 7.0.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.skyscreamer + tools + + + com.vaadin.external.google + android-json + + + + + com.mashape.unirest + unirest-java + 1.4.9 + test + + + org.jeasy + easy-random-core + 4.0.0 + test + + + + + clean install + + + com.google.cloud.artifactregistry + artifactregistry-maven-wagon + 2.2.1 + + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.23.0 + + exclude-pmd.properties + 3 + true + true + false + + /category/java/bestpractices.xml + /category/java/security.xml + + + + + compile + + check + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + integration-test + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + + Docker Compose Up + pre-integration-test + + exec + + + ${container.cli} + compose -f src/test/resources/docker-compose.yml up -d + + + + Docker Compose Down + post-integration-test + + exec + + + ${container.cli} + compose -f src/test/resources/docker-compose.yml down -v + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + uk.gov.ons.census.caseapisvc.Application + + + + + repackage + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + 1.22.0 + + + + + + + check + + verify + + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + verify + + report + + + + jacoco-check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 75% + + + + + + + + + + maven-compiler-plugin + + 21 + 21 + UTF-8 + + -XDcompilePolicy=simple + + + + + + org.projectlombok + lombok + 1.18.30 + + + + + + + diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/Application.java b/src/main/java/uk/gov/ons/census/caseapisvc/Application.java new file mode 100644 index 0000000..886c616 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/Application.java @@ -0,0 +1,13 @@ +package uk.gov.ons.census.caseapisvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; + +@SpringBootApplication +@EntityScan("uk.gov.ons.census.common.model.entity") +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClient.java b/src/main/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClient.java new file mode 100644 index 0000000..b45dfd5 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClient.java @@ -0,0 +1,43 @@ +package uk.gov.ons.census.caseapisvc.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import uk.gov.ons.census.caseapisvc.model.dto.UacQidCreatedPayloadDTO; + +@Component +public class UacQidServiceClient { + + @Value("${uacservice.connection.scheme}") + private String scheme; + + @Value("${uacservice.connection.host}") + private String host; + + @Value("${uacservice.connection.port}") + private String port; + + public UacQidCreatedPayloadDTO generateUacQid(int questionnaireType) { + + RestTemplate restTemplate = new RestTemplate(); + UriComponents uriComponents = createUriComponents(questionnaireType); + ResponseEntity responseEntity = + restTemplate.exchange( + uriComponents.toUri(), HttpMethod.GET, null, UacQidCreatedPayloadDTO.class); + return responseEntity.getBody(); + } + + private UriComponents createUriComponents(int questionnaireType) { + return UriComponentsBuilder.newInstance() + .scheme(scheme) + .host(host) + .port(port) + .queryParam("questionnaireType", questionnaireType) + .build() + .encode(); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/config/AppConfig.java b/src/main/java/uk/gov/ons/census/caseapisvc/config/AppConfig.java new file mode 100644 index 0000000..b152c76 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/config/AppConfig.java @@ -0,0 +1,79 @@ +package uk.gov.ons.census.caseapisvc.config; + +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import com.google.cloud.spring.pubsub.support.PublisherFactory; +import com.google.cloud.spring.pubsub.support.SubscriberFactory; +import com.google.cloud.spring.pubsub.support.converter.SimplePubSubMessageConverter; +import io.micrometer.stackdriver.StackdriverConfig; +import io.micrometer.stackdriver.StackdriverMeterRegistry; +import jakarta.annotation.PostConstruct; +import java.time.Duration; +import java.util.TimeZone; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + @Value("${management.stackdriver.metrics.export.project-id}") + private String stackdriverProjectId; + + @Value("${management.stackdriver.metrics.export.enabled}") + private boolean stackdriverEnabled; + + @Value("${management.stackdriver.metrics.export.step}") + private String stackdriverStep; + + @Value("${logging.profile}") + private String loggingProfile; + + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Bean + public PubSubTemplate pubSubTemplate( + PublisherFactory publisherFactory, + SubscriberFactory subscriberFactory, + SimplePubSubMessageConverter simplePubSubMessageConverter) { + PubSubTemplate pubSubTemplate = new PubSubTemplate(publisherFactory, subscriberFactory); + pubSubTemplate.setMessageConverter(simplePubSubMessageConverter); + return pubSubTemplate; + } + + @Bean + public SimplePubSubMessageConverter messageConverter() { + return new SimplePubSubMessageConverter(); + } + + @Bean + StackdriverConfig stackdriverConfig() { + return new StackdriverConfig() { + @Override + public Duration step() { + return Duration.parse(stackdriverStep); + } + + @Override + public boolean enabled() { + return stackdriverEnabled; + } + + @Override + public String projectId() { + return stackdriverProjectId; + } + + @Override + public String get(String key) { + return null; + } + }; + } + + @Bean + StackdriverMeterRegistry meterRegistry(StackdriverConfig stackdriverConfig) { + return StackdriverMeterRegistry.builder(stackdriverConfig).build(); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpoint.java b/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpoint.java new file mode 100644 index 0000000..6ba7b3b --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpoint.java @@ -0,0 +1,248 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import io.micrometer.core.annotation.Timed; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import uk.gov.ons.census.caseapisvc.model.dto.*; +import uk.gov.ons.census.caseapisvc.service.CaseService; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.Event; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@RestController +@RequestMapping(value = "/cases") +@Timed +public class CaseEndpoint { + private final CaseService caseService; + + @Autowired + public CaseEndpoint(CaseService caseService) { + this.caseService = caseService; + } + + @GetMapping(value = "/{id}") + public CaseContainerDTO findCaseById( + @PathVariable("id") UUID id, + @RequestParam(value = "caseEvents", required = false, defaultValue = "false") + boolean caseEvents) { + + return buildCaseContainerDTO(caseService.findById(id), caseEvents); + } + + @GetMapping(value = "/ref/{reference}") + public CaseContainerDTO findCaseByReference( + @PathVariable("reference") long reference, + @RequestParam(value = "caseEvents", required = false, defaultValue = "false") + boolean caseEvents) { + + return buildCaseContainerDTO(caseService.findByReference(reference), caseEvents); + } + + @GetMapping(value = "/uprn/{uprn}") + public List findCasesByUPRN( + @PathVariable("uprn") String uprn, + @RequestParam(value = "caseEvents", required = false, defaultValue = "false") + boolean caseEvents, + @RequestParam(value = "validAddressOnly", required = false, defaultValue = "false") + boolean validAddressOnly) { + + List caseContainerDTOs = new LinkedList<>(); + + for (Case caze : caseService.findByUPRN(uprn, validAddressOnly)) { + caseContainerDTOs.add(buildCaseContainerDTO(caze, caseEvents)); + } + + return caseContainerDTOs; + } + + @GetMapping(value = "/postcode/{postcode}") + public List getCasesByPostcode(@PathVariable("postcode") String postcode) { + List cases = caseService.findByPostcode(postcode); + List caseContainerDTOs = new LinkedList<>(); + for (Case caze : cases) { + caseContainerDTOs.add(buildCaseContainerDTO(caze, false)); + } + return caseContainerDTOs; + } + + @GetMapping(value = "/qid/{qid}") + public CaseContainerDTO findCaseByQid(@PathVariable("qid") String qid) { + Case caze = caseService.findCaseByQid(qid); + CaseContainerDTO caseContainerDTO = new CaseContainerDTO(); + caseContainerDTO.setCaseId(caze.getId()); + caseContainerDTO.setAddressType(caze.getAddressType()); + + return caseContainerDTO; + } + + @GetMapping(value = "/case-details/{caseId}") + public CaseDetailsDTO getAllCaseDetailsByCaseId(@PathVariable("caseId") UUID caseId) { + Case caze = caseService.findById(caseId); + return buildCaseDetailsDTO(caze); + } + + private CaseContainerDTO buildCaseContainerDTO(Case caze, boolean includeCaseEvents) { + + CaseContainerDTO caseContainerDTO = mapCase(caze); + + List caseEvents = new LinkedList<>(); + + if (includeCaseEvents) { + List uacQidLinks = caze.getUacQidLinks(); + + for (UacQidLink uacQidLink : uacQidLinks) { + List events = uacQidLink.getEvents(); + + for (Event event : events) { + caseEvents.add(mapCaseEvent(event)); + } + } + if (caze.getEvents() != null) { + for (Event event : caze.getEvents()) { + caseEvents.add(mapCaseEvent(event)); + } + } + } + + caseContainerDTO.setCaseEvents(caseEvents); + + return caseContainerDTO; + } + + private CaseContainerDTO mapCase(Case caze) { + CaseContainerDTO caseContainerDTO = new CaseContainerDTO(); + caseContainerDTO.setCaseRef(caze.getCaseRef().toString()); + caseContainerDTO.setCaseId(caze.getId()); + caseContainerDTO.setInvalid(caze.isInvalid()); + caseContainerDTO.setCreatedDateTime(caze.getCreatedAt()); + caseContainerDTO.setLastUpdated(caze.getLastUpdatedAt()); + caseContainerDTO.setUprn(caze.getUprn()); + caseContainerDTO.setPostcode(caze.getPostcode()); + caseContainerDTO.setEstabType(caze.getEstabType()); + caseContainerDTO.setEstabUprn(caze.getEstabUprn()); + caseContainerDTO.setCollectionExerciseId( + caze.getCollectionExercise() != null ? caze.getCollectionExercise().getId() : null); + caseContainerDTO.setCaseType(caze.getCaseType()); + caseContainerDTO.setCreatedDateTime(caze.getCreatedAt()); + caseContainerDTO.setAddressLine1(caze.getAddressLine1()); + caseContainerDTO.setAddressLine2(caze.getAddressLine2()); + caseContainerDTO.setAddressLine3(caze.getAddressLine3()); + caseContainerDTO.setTownName(caze.getTownName()); + caseContainerDTO.setPostcode(caze.getPostcode()); + caseContainerDTO.setOrganisationName(caze.getOrganisationName()); + caseContainerDTO.setAddressLevel(caze.getAddressLevel()); + caseContainerDTO.setAbpCode(caze.getAbpCode()); + caseContainerDTO.setRegion(caze.getRegion()); + caseContainerDTO.setLatitude(caze.getLatitude()); + caseContainerDTO.setLongitude(caze.getLongitude()); + caseContainerDTO.setOa(caze.getOa()); + caseContainerDTO.setLsoa(caze.getLsoa()); + caseContainerDTO.setLastUpdated(caze.getLastUpdatedAt()); + caseContainerDTO.setMsoa(caze.getMsoa()); + caseContainerDTO.setSecureEstablishment(caze.isSecureEstablishment()); + caseContainerDTO.setAddressType(caze.getAddressType()); + caseContainerDTO.setLad(caze.getLad()); + return caseContainerDTO; + } + + private CaseDetailsDTO mapCaseDetails(Case caze) { + CaseDetailsDTO caseDetailsDTO = new CaseDetailsDTO(); + caseDetailsDTO.setCaseId(caze.getId()); + caseDetailsDTO.setCaseRef(caze.getCaseRef()); + caseDetailsDTO.setUprn(caze.getUprn()); + caseDetailsDTO.setEstabUprn(caze.getEstabUprn()); + caseDetailsDTO.setCaseType(caze.getCaseType()); + caseDetailsDTO.setAddressType(caze.getAddressType()); + caseDetailsDTO.setEstabType(caze.getEstabType()); + caseDetailsDTO.setAddressLevel(caze.getAddressLevel()); + caseDetailsDTO.setAbpCode(caze.getAbpCode()); + caseDetailsDTO.setOrganisationName(caze.getOrganisationName()); + caseDetailsDTO.setAddressLine1(caze.getAddressLine1()); + caseDetailsDTO.setAddressLine2(caze.getAddressLine2()); + caseDetailsDTO.setAddressLine3(caze.getAddressLine3()); + caseDetailsDTO.setTownName(caze.getTownName()); + caseDetailsDTO.setPostcode(caze.getPostcode()); + caseDetailsDTO.setLongitude(caze.getLongitude()); + caseDetailsDTO.setLatitude(caze.getLatitude()); + caseDetailsDTO.setOa(caze.getOa()); + caseDetailsDTO.setLsoa(caze.getLsoa()); + caseDetailsDTO.setMsoa(caze.getMsoa()); + caseDetailsDTO.setLad(caze.getLad()); + caseDetailsDTO.setRegion(caze.getRegion()); + caseDetailsDTO.setHtcWillingness(caze.getHtcWillingness()); + caseDetailsDTO.setHtcDigital(caze.getHtcDigital()); + caseDetailsDTO.setFieldCoordinatorId(caze.getFieldCoordinatorId()); + caseDetailsDTO.setFieldOfficerId(caze.getFieldOfficerId()); + caseDetailsDTO.setTreatmentCode(caze.getTreatmentCode()); + caseDetailsDTO.setCeExpectedCapacity(caze.getCeExpectedCapacity()); + caseDetailsDTO.setCollectionExerciseId(caze.getCollectionExercise().getId()); + caseDetailsDTO.setCreatedDateTime(caze.getCreatedAt()); + ; + caseDetailsDTO.setReceiptReceived(caze.isReceiptReceived()); + caseDetailsDTO.setRefusalReceived(caze.getRefusalReceived()); + caseDetailsDTO.setInvalid(caze.isInvalid()); + caseDetailsDTO.setLastUpdated(caze.getLastUpdatedAt()); + caseDetailsDTO.setPrintBatch(caze.getPrintBatch()); + caseDetailsDTO.setSurveyLaunched(caze.isSurveyLaunched()); + + caseDetailsDTO.setCaseId(caze.getId()); + caseDetailsDTO.setCreatedDateTime(caze.getCreatedAt()); + caseDetailsDTO.setLastUpdated(caze.getLastUpdatedAt()); + return caseDetailsDTO; + } + + private CaseEventDTO mapCaseEvent(Event event) { + CaseEventDTO caseEventDTO = new CaseEventDTO(); + caseEventDTO.setDescription(event.getDescription()); + caseEventDTO.setDateTime(event.getDateTime()); + caseEventDTO.setId(event.getId()); + caseEventDTO.setType(EventTypeDTO.valueOf(event.getType().name())); + return caseEventDTO; + } + + private CaseDetailsEventDTO mapCaseDetailsEvent(Event event) { + CaseDetailsEventDTO caseDetailsEventDTO = new CaseDetailsEventDTO(); + caseDetailsEventDTO.setEventChannel(event.getChannel()); + caseDetailsEventDTO.setEventPayload(event.getPayload()); + caseDetailsEventDTO.setEventDescription(event.getDescription()); + caseDetailsEventDTO.setEventDate(event.getDateTime()); + caseDetailsEventDTO.setEventSource(event.getSource()); + caseDetailsEventDTO.setEventTransactionId(event.getCorrelationId()); + caseDetailsEventDTO.setEventType(event.getType().toString()); + caseDetailsEventDTO.setRmEventProcessed(event.getProcessedAt()); + caseDetailsEventDTO.setId(event.getId()); + caseDetailsEventDTO.setMessageTimestamp(event.getMessageTimestamp()); + + return caseDetailsEventDTO; + } + + private CaseDetailsDTO buildCaseDetailsDTO(Case caze) { + + CaseDetailsDTO caseDetailsDTO = mapCaseDetails(caze); + + List caseEvents = new LinkedList<>(); + + List uacQidLinks = caze.getUacQidLinks(); + + for (UacQidLink uacQidLink : uacQidLinks) { + List events = uacQidLink.getEvents(); + + for (Event event : events) { + // RM_UAC_CREATED event redacted remove UACs + // if (!event.getEventType().equals(EventType.RM_UAC_CREATED)) { + caseEvents.add(mapCaseDetailsEvent(event)); + // } + } + } + caseDetailsDTO.setEvents(caseEvents); + + return caseDetailsDTO; + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpoint.java b/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpoint.java new file mode 100644 index 0000000..5b15241 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpoint.java @@ -0,0 +1,57 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import io.micrometer.core.annotation.Timed; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.model.dto.NewQidLink; +import uk.gov.ons.census.caseapisvc.model.dto.QidLink; +import uk.gov.ons.census.caseapisvc.service.CaseService; +import uk.gov.ons.census.caseapisvc.service.UacQidService; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@RestController +@RequestMapping(value = "/qids") +@Timed +public class QidEndpoint { + private final UacQidService uacQidService; + private final CaseService caseService; + + @Autowired + public QidEndpoint(UacQidService uacQidService, CaseService caseService) { + this.uacQidService = uacQidService; + this.caseService = caseService; + } + + @GetMapping(value = "/{qid}") + public QidLink getUacQidLinkByQid(@PathVariable("qid") String qid) { + UacQidLink uacQidLink = uacQidService.findUacQidLinkByQid(qid); + QidLink qidDetails = new QidLink(); + qidDetails.setQuestionnaireId(uacQidLink.getQid()); + if (uacQidLink.getCaze() != null) { + qidDetails.setCaseId(uacQidLink.getCaze().getId()); + } + return qidDetails; + } + + // As we don't have a subscription for the questionnaire links, it is not possible to test this. + @PutMapping(value = "/link") + public void putQidLinkToCase(@RequestBody NewQidLink newQidLink) { + + // Below commented shall be uncommented when the subscription for the questionnaire link is + // available + // UacQidLink uacQidLink = + // uacQidService.findUacQidLinkByQid(newQidLink.getQidLink().getQuestionnaireId()); + // Case caseToLink = caseService.findById(newQidLink.getQidLink().getCaseId()); + // + // uacQidService.buildAndSendQuestionnaireLinkedEvent(uacQidLink, caseToLink, newQidLink); + throw new ResponseStatusException( + HttpStatus.NOT_IMPLEMENTED, + "Questionnaire Id Link is not available, request cannot be fulfilled"); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseContainerDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseContainerDTO.java new file mode 100644 index 0000000..1b5b49b --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseContainerDTO.java @@ -0,0 +1,69 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; +import lombok.Data; + +@Data +public class CaseContainerDTO { + private String caseRef; + + @JsonProperty("id") + private UUID caseId; + + private String estabType; + + private String uprn; + + private String estabUprn; + + private UUID collectionExerciseId; + + private String surveyType; + + private String addressType; + + private String caseType; + + private OffsetDateTime createdDateTime; + + private String addressLine1; + + private String addressLine2; + + private String addressLine3; + + private String townName; + + private String postcode; + + private String organisationName; + + private String addressLevel; + + private String abpCode; + + private String region; + + private String latitude; + + private String longitude; + + private String oa; + + private String lsoa; + + private OffsetDateTime lastUpdated; + + private String msoa; + + private String lad; + + private List caseEvents; + + private Boolean secureEstablishment; + + private boolean invalid; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsDTO.java new file mode 100644 index 0000000..5b75caa --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsDTO.java @@ -0,0 +1,89 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; +import lombok.Data; +import uk.gov.ons.census.common.model.entity.RefusalType; + +@Data +public class CaseDetailsDTO { + + @JsonProperty("id") + private UUID caseId; + + private Long caseRef; + + private String uprn; + + private String estabUprn; + + private String caseType; + + private String addressType; + + private String estabType; + + private String addressLevel; + + private String abpCode; + + private String organisationName; + + private String addressLine1; + + private String addressLine2; + + private String addressLine3; + + private String townName; + + private String postcode; + + private String latitude; + + private String longitude; + + private String oa; + + private String lsoa; + + private String msoa; + + private String lad; + + private String region; + + private String htcWillingness; + + private String htcDigital; + + private String fieldCoordinatorId; + + private String fieldOfficerId; + + private String treatmentCode; + + private Integer ceExpectedCapacity; + + private int ceActualResponses; + + private UUID collectionExerciseId; + + private OffsetDateTime createdDateTime; + + List events; + + private boolean receiptReceived; + + private RefusalType refusalReceived; + + private boolean invalid; + + private OffsetDateTime lastUpdated; + + private String printBatch; + + private boolean surveyLaunched; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsEventDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsEventDTO.java new file mode 100644 index 0000000..20771e4 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseDetailsEventDTO.java @@ -0,0 +1,29 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import java.time.OffsetDateTime; +import java.util.UUID; +import lombok.Data; + +@Data +public class CaseDetailsEventDTO { + + private UUID id; + + private String eventType; + + private String eventDescription; + + private OffsetDateTime eventDate; + + private String eventChannel = "RM"; + + private UUID eventTransactionId; + + private OffsetDateTime rmEventProcessed; + + private String eventSource; + + private String eventPayload; + + private OffsetDateTime messageTimestamp; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseEventDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseEventDTO.java new file mode 100644 index 0000000..c31577e --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/CaseEventDTO.java @@ -0,0 +1,20 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.UUID; +import lombok.Data; + +@Data +public class CaseEventDTO { + + private UUID id; + + @JsonProperty("eventType") + private EventTypeDTO type; + + private String description; + + @JsonProperty("createdDateTime") + private OffsetDateTime dateTime; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventDTO.java new file mode 100644 index 0000000..f46f9a9 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventDTO.java @@ -0,0 +1,13 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EventDTO { + private EventHeaderDTO header; + private PayloadDTO payload; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventHeaderDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventHeaderDTO.java new file mode 100644 index 0000000..bce8c64 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventHeaderDTO.java @@ -0,0 +1,17 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import java.time.OffsetDateTime; +import java.util.UUID; +import lombok.Data; + +@Data +public class EventHeaderDTO { + private String version; + private String topic; + private String source; + private String channel; + private OffsetDateTime dateTime; + private UUID messageId; + private UUID correlationId; + private String originatingUser; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventTypeDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventTypeDTO.java new file mode 100644 index 0000000..13fdecb --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/EventTypeDTO.java @@ -0,0 +1,22 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +public enum EventTypeDTO { + NEW_CASE, + RECEIPT, + REFUSAL, + INVALID_CASE, + EQ_LAUNCH, + UAC_AUTHENTICATION, + PRINT_FULFILMENT, + EXPORT_FILE, + DEACTIVATE_UAC, + UPDATE_SAMPLE, + UPDATE_SAMPLE_SENSITIVE, + SMS_FULFILMENT, + ACTION_RULE_SMS_REQUEST, + EMAIL_FULFILMENT, + ACTION_RULE_EMAIL_REQUEST, + ACTION_RULE_SMS_CONFIRMATION, + ACTION_RULE_EMAIL_CONFIRMATION, + ERASE_DATA +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/NewQidLink.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/NewQidLink.java new file mode 100644 index 0000000..f6a629d --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/NewQidLink.java @@ -0,0 +1,13 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.UUID; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class NewQidLink { + UUID transactionId; + String channel; + QidLink qidLink; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/PayloadDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/PayloadDTO.java new file mode 100644 index 0000000..6e6b4b6 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/PayloadDTO.java @@ -0,0 +1,11 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.Data; + +@Data +@JsonInclude(Include.NON_NULL) +public class PayloadDTO { + private UacDTO uac; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/QidLink.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/QidLink.java new file mode 100644 index 0000000..822ac90 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/QidLink.java @@ -0,0 +1,12 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.UUID; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class QidLink { + String questionnaireId; + UUID caseId; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacDTO.java new file mode 100644 index 0000000..2edc965 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacDTO.java @@ -0,0 +1,22 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.UUID; +import lombok.Data; + +@Data +@JsonInclude(Include.NON_NULL) +public class UacDTO { + // Uac block for QUESTIONNAIRE_LINKED events + private String uacHash; + private String uac; + private Boolean active; + private String questionnaireId; + private String caseType; + private String region; + private UUID caseId; + private UUID collectionExerciseId; + private String formType; + private UUID individualCaseId; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacQidCreatedPayloadDTO.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacQidCreatedPayloadDTO.java new file mode 100644 index 0000000..cb4513e --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/dto/UacQidCreatedPayloadDTO.java @@ -0,0 +1,11 @@ +package uk.gov.ons.census.caseapisvc.model.dto; + +import java.util.UUID; +import lombok.Data; + +@Data +public class UacQidCreatedPayloadDTO { + private String uac; + private String qid; + private UUID caseId; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/CaseRepository.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/CaseRepository.java new file mode 100644 index 0000000..9300100 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/CaseRepository.java @@ -0,0 +1,30 @@ +package uk.gov.ons.census.caseapisvc.model.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import uk.gov.ons.census.common.model.entity.Case; + +public interface CaseRepository extends JpaRepository { + + @Override + Optional findById(UUID id); + + Optional findByCaseRef(long reference); + + Optional> findByUprn(String uprn); + + Optional> findByUprnAndInvalidFalse(String uprn); + + @Query( + """ + SELECT c + FROM Case c + WHERE UPPER(REPLACE(c.postcode, ' ', '')) = UPPER(REPLACE(:postcode, ' ', '')) + ORDER BY c.organisationName, c.addressLine1, c.caseType, c.addressLevel +""") + List findByPostcode(@Param("postcode") String postcode); +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/EventRepository.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/EventRepository.java new file mode 100644 index 0000000..321c0ab --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/EventRepository.java @@ -0,0 +1,7 @@ +package uk.gov.ons.census.caseapisvc.model.repository; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import uk.gov.ons.census.common.model.entity.Event; + +public interface EventRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/UacQidLinkRepository.java b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/UacQidLinkRepository.java new file mode 100644 index 0000000..034687a --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/model/repository/UacQidLinkRepository.java @@ -0,0 +1,11 @@ +package uk.gov.ons.census.caseapisvc.model.repository; + +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +public interface UacQidLinkRepository extends JpaRepository { + + Optional findByQid(String qid); +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/service/CaseService.java b/src/main/java/uk/gov/ons/census/caseapisvc/service/CaseService.java new file mode 100644 index 0000000..cd86833 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/service/CaseService.java @@ -0,0 +1,85 @@ +package uk.gov.ons.census.caseapisvc.service; + +import java.util.List; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.model.repository.CaseRepository; +import uk.gov.ons.census.caseapisvc.model.repository.UacQidLinkRepository; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@Service +public class CaseService { + + private final CaseRepository caseRepository; + private final UacQidLinkRepository uacQidLinkRepository; + + public CaseService(CaseRepository caseRepository, UacQidLinkRepository uacQidLinkRepository) { + this.caseRepository = caseRepository; + this.uacQidLinkRepository = uacQidLinkRepository; + } + + public Case findById(UUID id) { + + return caseRepository + .findById(id) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("Case Id '%s' not found", id))); + } + + public Case findByReference(long reference) { + + return caseRepository + .findByCaseRef(reference) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, + String.format("Case Reference '%s' not found", reference))); + } + + public List findByUPRN(String uprn, boolean validAddressOnly) { + + if (validAddressOnly) { + return caseRepository + .findByUprnAndInvalidFalse(uprn) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("UPRN '%s' not found", uprn))); + } else { + return caseRepository + .findByUprn(uprn) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("UPRN '%s' not found", uprn))); + } + } + + public Case findCaseByQid(String qid) { + UacQidLink uacQidLink = + uacQidLinkRepository + .findByQid(qid) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("QID '%s' not found", qid))); + + if (uacQidLink.getCaze() == null) { + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("Case for QID '%s' not found", qid)); + } + + return uacQidLink.getCaze(); + } + + public List findByPostcode(String postcode) { + List cazeList = caseRepository.findByPostcode(postcode); + return cazeList; + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/service/UacQidService.java b/src/main/java/uk/gov/ons/census/caseapisvc/service/UacQidService.java new file mode 100644 index 0000000..49d77b7 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/service/UacQidService.java @@ -0,0 +1,173 @@ +package uk.gov.ons.census.caseapisvc.service; + +import java.time.OffsetDateTime; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.client.UacQidServiceClient; +import uk.gov.ons.census.caseapisvc.model.dto.EventDTO; +import uk.gov.ons.census.caseapisvc.model.dto.EventHeaderDTO; +import uk.gov.ons.census.caseapisvc.model.dto.NewQidLink; +import uk.gov.ons.census.caseapisvc.model.dto.PayloadDTO; +import uk.gov.ons.census.caseapisvc.model.dto.UacDTO; +import uk.gov.ons.census.caseapisvc.model.dto.UacQidCreatedPayloadDTO; +import uk.gov.ons.census.caseapisvc.model.repository.UacQidLinkRepository; +import uk.gov.ons.census.caseapisvc.utility.PubSubHelper; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@Service +public class UacQidService { + private static final String ADDRESS_LEVEL_ESTAB = "E"; + + private static final String COUNTRY_CODE_ENGLAND = "E"; + private static final String COUNTRY_CODE_WALES = "W"; + private static final String COUNTRY_CODE_NORTHERN_IRELAND = "N"; + + private static final String CASE_TYPE_HOUSEHOLD = "HH"; + private static final String CASE_TYPE_SPG = "SPG"; + private static final String CASE_TYPE_CE = "CE"; + + private final UacQidServiceClient uacQidServiceClient; + private final UacQidLinkRepository uacQidLinkRepository; + + private final PubSubHelper pubSubHelper; + + @Value("${queueconfig.questionnaire-link-topic}") + String questionnaireLinkedTopic; + + @Value("${spring.cloud.gcp.pubsub.project-id}") + String pubsubProject; + + @Autowired + public UacQidService( + UacQidServiceClient uacQidServiceClient, + UacQidLinkRepository uacQidLinkRepository, + PubSubHelper pubSubHelper) { + this.uacQidServiceClient = uacQidServiceClient; + this.uacQidLinkRepository = uacQidLinkRepository; + this.pubSubHelper = pubSubHelper; + } + + public UacQidCreatedPayloadDTO createAndLinkUacQid(UUID caseId, int questionnaireType) { + UacQidCreatedPayloadDTO uacQidCreatedPayload = + uacQidServiceClient.generateUacQid(questionnaireType); + uacQidCreatedPayload.setCaseId(caseId); + return uacQidCreatedPayload; + } + + public UacQidLink findUacQidLinkByQid(String qid) { + return uacQidLinkRepository + .findByQid(qid) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, String.format("QID '%s' not found", qid))); + } + + public static int calculateQuestionnaireType( + String caseType, String region, String addressLevel, String surveyType) { + return calculateQuestionnaireType(caseType, region, addressLevel, surveyType, false); + } + + public static int calculateQuestionnaireType( + String caseType, String region, String addressLevel, String surveyType, boolean individual) { + + if ("CCS".equals(surveyType)) { + return 71; + } + + String country = region.substring(0, 1); + if (!COUNTRY_CODE_ENGLAND.equals(country) + && !COUNTRY_CODE_WALES.equals(country) + && !COUNTRY_CODE_NORTHERN_IRELAND.equals(country)) { + throw new IllegalArgumentException( + String.format("Unknown Country for treatment code %s", caseType)); + } + + if (individual) { + switch (country) { + case COUNTRY_CODE_ENGLAND: + return 21; + case COUNTRY_CODE_WALES: + return 22; + case COUNTRY_CODE_NORTHERN_IRELAND: + return 24; + default: + } + } else if (isHouseholdCaseType(caseType) || isSpgCaseType(caseType)) { + switch (country) { + case COUNTRY_CODE_ENGLAND: + return 1; + case COUNTRY_CODE_WALES: + return 2; + case COUNTRY_CODE_NORTHERN_IRELAND: + return 4; + default: + } + } else if (isCE1RequestForEstabCeCase(caseType, addressLevel, individual)) { + switch (country) { + case COUNTRY_CODE_ENGLAND: + return 31; + case COUNTRY_CODE_WALES: + return 32; + case COUNTRY_CODE_NORTHERN_IRELAND: + return 34; + default: + } + } else { + throw new IllegalArgumentException( + String.format( + "Unexpected combination of Case Type, Address level and individual request. treatment code: '%s', address level: '%s', individual request: '%s'", + caseType, addressLevel, individual)); + } + + throw new RuntimeException( + String.format( + "Unprocessable combination of Case Type, Address level and individual request. treatment code: '%s', address level: '%s', individual request: '%s'", + caseType, addressLevel, individual)); + } + + private static boolean isCE1RequestForEstabCeCase( + String treatmentCode, String addressLevel, boolean individual) { + return isCeCaseType(treatmentCode) && ADDRESS_LEVEL_ESTAB.equals(addressLevel) && !individual; + } + + private static boolean isSpgCaseType(String caseType) { + return CASE_TYPE_SPG.equals(caseType); + } + + private static boolean isHouseholdCaseType(String caseType) { + return CASE_TYPE_HOUSEHOLD.equals(caseType); + } + + private static boolean isCeCaseType(String caseType) { + return CASE_TYPE_CE.equals(caseType); + } + + public void buildAndSendQuestionnaireLinkedEvent( + UacQidLink uacQidLink, Case caseToLink, NewQidLink newQidLink) { + UacDTO uacDTO = new UacDTO(); + uacDTO.setCaseId(caseToLink.getId()); + uacDTO.setQuestionnaireId(uacQidLink.getQid()); + + EventDTO event = new EventDTO(); + EventHeaderDTO eventHeader = new EventHeaderDTO(); + eventHeader.setChannel(newQidLink.getChannel()); + eventHeader.setDateTime(OffsetDateTime.now()); + eventHeader.setTopic(questionnaireLinkedTopic); + eventHeader.setMessageId(newQidLink.getTransactionId()); + + PayloadDTO payloadDTO = new PayloadDTO(); + payloadDTO.setUac(uacDTO); + + event.setHeader(eventHeader); + event.setPayload(payloadDTO); + + // String topic = toProjectTopicName(questionnaireLinkedTopic, pubsubProject).toString(); + // pubSubHelper.publishAndConfirm(topic, event); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/Constants.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/Constants.java new file mode 100644 index 0000000..7f9b14b --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/Constants.java @@ -0,0 +1,10 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import java.util.Set; + +public class Constants { + public static final String OUTBOUND_EVENT_SCHEMA_VERSION = "0.5.0"; + public static final Set ALLOWED_INBOUND_EVENT_SCHEMA_VERSIONS = + Set.of("v0.3_RELEASE", "0.4.0-DRAFT", "0.4.0", "0.5.0-DRAFT", "0.5.0", "0.6.0-DRAFT"); + public static final String EVENT_SCHEMA_VERSION = "0.5.0"; +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/EventHelper.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/EventHelper.java new file mode 100644 index 0000000..3456858 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/EventHelper.java @@ -0,0 +1,36 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import static uk.gov.ons.census.caseapisvc.utility.Constants.EVENT_SCHEMA_VERSION; + +import java.time.OffsetDateTime; +import java.util.UUID; +import uk.gov.ons.census.caseapisvc.model.dto.EventHeaderDTO; + +public class EventHelper { + + private static final String EVENT_SOURCE = "CASE_API"; + private static final String EVENT_CHANNEL = "RM"; + + private EventHelper() { + throw new IllegalStateException("Utility class EventHelper should not be instantiated"); + } + + public static EventHeaderDTO createEventDTO( + String topic, String eventChannel, String eventSource) { + EventHeaderDTO eventHeader = new EventHeaderDTO(); + + eventHeader.setVersion(EVENT_SCHEMA_VERSION); + eventHeader.setChannel(eventChannel); + eventHeader.setSource(eventSource); + eventHeader.setDateTime(OffsetDateTime.now()); + eventHeader.setMessageId(UUID.randomUUID()); + eventHeader.setCorrelationId(UUID.randomUUID()); + eventHeader.setTopic(topic); + + return eventHeader; + } + + public static EventHeaderDTO createEventDTO(String topic) { + return createEventDTO(topic, EVENT_CHANNEL, EVENT_SOURCE); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/JsonHelper.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/JsonHelper.java new file mode 100644 index 0000000..f349ca6 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/JsonHelper.java @@ -0,0 +1,39 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import static uk.gov.ons.census.caseapisvc.utility.Constants.ALLOWED_INBOUND_EVENT_SCHEMA_VERSIONS; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import uk.gov.ons.census.caseapisvc.model.dto.EventDTO; + +public class JsonHelper { + private static final ObjectMapper objectMapper = ObjectMapperFactory.objectMapper(); + + public static String convertObjectToJson(Object obj) { + try { + return objectMapper.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed converting Object To Json", e); + } + } + + public static EventDTO convertJsonBytesToEvent(byte[] bytes) { + EventDTO event; + try { + event = objectMapper.readValue(bytes, EventDTO.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (!ALLOWED_INBOUND_EVENT_SCHEMA_VERSIONS.contains((event.getHeader().getVersion()))) { + throw new RuntimeException( + String.format( + "Unsupported message version. Got %s but RM only supports %s", + event.getHeader().getVersion(), + String.join(", ", ALLOWED_INBOUND_EVENT_SCHEMA_VERSIONS))); + } + + return event; + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/MessageDateHelper.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/MessageDateHelper.java new file mode 100644 index 0000000..ccd60f1 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/MessageDateHelper.java @@ -0,0 +1,18 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import org.springframework.messaging.Message; + +public class MessageDateHelper { + public static OffsetDateTime getMessageTimeStamp(Message message) { + + if (message.getHeaders().getTimestamp() == null) { + throw new RuntimeException("Message Headers missing Timestamp"); + } + + return OffsetDateTime.ofInstant( + Instant.ofEpochMilli(message.getHeaders().getTimestamp()), ZoneId.of("UTC")); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/ObjectMapperFactory.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/ObjectMapperFactory.java new file mode 100644 index 0000000..613733e --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/ObjectMapperFactory.java @@ -0,0 +1,17 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +public class ObjectMapperFactory { + public static ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .registerModule(new Jdk8Module()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/PubSubHelper.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/PubSubHelper.java new file mode 100644 index 0000000..239ffe7 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/PubSubHelper.java @@ -0,0 +1,31 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import java.util.concurrent.ExecutionException; +import org.springframework.stereotype.Component; +import uk.gov.ons.census.caseapisvc.model.dto.EventDTO; + +@Component +public class PubSubHelper { + private final PubSubTemplate pubSubTemplate; + + private static final ObjectMapper objectMapper = ObjectMapperFactory.objectMapper(); + + public PubSubHelper(PubSubTemplate pubSubTemplate) { + this.pubSubTemplate = pubSubTemplate; + } + + public void publishAndConfirm(String topic, EventDTO payload) { + try { + pubSubTemplate.publish(topic, objectMapper.writeValueAsBytes(payload)).get(); + } catch (ExecutionException e) { + throw new RuntimeException("Error publishing message to PubSub topic ", e); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error mapping event to JSON", e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/uk/gov/ons/census/caseapisvc/utility/RedactHelper.java b/src/main/java/uk/gov/ons/census/caseapisvc/utility/RedactHelper.java new file mode 100644 index 0000000..267f9c9 --- /dev/null +++ b/src/main/java/uk/gov/ons/census/caseapisvc/utility/RedactHelper.java @@ -0,0 +1,104 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.util.StringUtils; + +public class RedactHelper { + + private static final String REDACTION_FAILURE = "Failed to redact sensitive data"; + private static final String REDACTION_TEXT = "REDACTED"; + private static final ObjectMapper objectMapper = ObjectMapperFactory.objectMapper(); + + private static final ThingToRedact[] THINGS_TO_REDACT = { + new ThingToRedact("setUac", String.class), + new ThingToRedact("setPhoneNumber", String.class), + new ThingToRedact("setEmail", String.class), + new ThingToRedact("getPersonalisation", Map.class) + }; + + public static Object redact(Object rootObjectToRedact) { + if (rootObjectToRedact == null) { + return null; // can't redact null! + } + + try { + Object rootObjectToRedactDeepCopy = + objectMapper.readValue( + objectMapper.writeValueAsString(rootObjectToRedact), rootObjectToRedact.getClass()); + recursivelyRedact( + rootObjectToRedactDeepCopy, rootObjectToRedactDeepCopy.getClass().getPackageName()); + return rootObjectToRedactDeepCopy; + } catch (JsonProcessingException e) { + throw new RuntimeException(REDACTION_FAILURE, e); + } + } + + private static void recursivelyRedact(Object object, String packageName) { + Arrays.stream(object.getClass().getMethods()) + .filter(item -> Modifier.isPublic(item.getModifiers())) + .forEach( + method -> { + if (method.getName().startsWith("get") + && method.getReturnType().getPackageName().equals(packageName)) { + try { + Object invokeResult = method.invoke(object); + if (invokeResult != null) { + recursivelyRedact(invokeResult, packageName); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(REDACTION_FAILURE, e); + } + } + + for (ThingToRedact thingToRedact : THINGS_TO_REDACT) { + redactMethod(object, method, thingToRedact); + } + }); + } + + private static void redactMethod(Object object, Method method, ThingToRedact thingToRedact) { + if (!method.getName().equals(thingToRedact.getMethodName())) { + return; + } + + if (thingToRedact.getThingToRedactType() == Map.class + && method.getReturnType().equals(Map.class)) { + try { + Map sensitiveData = (Map) method.invoke(object); + if (sensitiveData == null) { + return; + } + for (String key : sensitiveData.keySet()) { + if (StringUtils.hasText((sensitiveData.get(key)))) { + sensitiveData.put(key, REDACTION_TEXT); + } + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(REDACTION_FAILURE, e); + } + } else if (thingToRedact.getThingToRedactType() == String.class + && method.getParameterTypes().length == 1 + && method.getParameterTypes()[0].equals(String.class)) { + try { + method.invoke(object, REDACTION_TEXT); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(REDACTION_FAILURE, e); + } + } + } + + @Data + @AllArgsConstructor + private static class ThingToRedact { + private String methodName; + private Class thingToRedactType; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..8c076e8 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,75 @@ +server: + port: 8161 + +info: + app: + name: Case API + version: 1.0 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/rm?readOnly=true + username: appuser + password: postgres + driverClassName: org.postgresql.Driver + hikari: + maximumPoolSize: 50 + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: validate + properties: + hibernate: + default_schema: cases + jdbc: + lob: + non_contextual_creation: true + cloud: + gcp: + pubsub: + emulator-host: localhost:8538 + project-id: our-project + subscriber: + flow-control: + max-outstanding-element-count: 100 + + +logging: + profile: DEV + level: + root: INFO + com.google.cloud.spring.pubsub.integration.inbound.PubSubInboundChannelAdapter: ERROR + +exceptionmanager: + connection: + scheme: http + host: localhost + port: 8666 + +management: + endpoints: + enabled-by-default: false + endpoint: + health: + enabled: true + metrics: + tags: + application: Case API + pod: ${HOSTNAME} + stackdriver: + metrics: + export: + project-id: dummy-project-id + enabled: false + step: PT1M + +uacservice: + connection: + scheme: http + host: localhost + port: 8164 + + +queueconfig: + questionnaire-link-topic: rm-internal-questionnaire-uac-linked + questionnaire-link-subcription: rm-internal-questionnaire-uac-linked-service \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..f30bf5a --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + UTC + ${ISO8601_DATE_FORMAT} + created + + + {"service":"Case Api"} + + + event + + + context + + + + level + + + + 20 + 1000 + 30 + true + + + + + included + + + data + + + true + + prefix + + + + + + + + + + + + DEBUG + + + + + + ${CONSOLE_LOG_PATTERN} + + + DEBUG + + + + + localhost + DAEMON + ${SYSLOG_PATTERN} + + WARN + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..0f5bbd4 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS cases; \ No newline at end of file diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClientTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClientTest.java new file mode 100644 index 0000000..055d645 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/client/UacQidServiceClientTest.java @@ -0,0 +1,69 @@ +package uk.gov.ons.census.caseapisvc.client; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import uk.gov.ons.census.caseapisvc.model.dto.UacQidCreatedPayloadDTO; + +class UacQidServiceClientTest { + + @Test + void generateUacQid_callsRestTemplateWithCorrectUri() { + // Arrange + UacQidServiceClient client = new UacQidServiceClient(); + + // Inject @Value fields manually + setField(client, "scheme", "http"); + setField(client, "host", "localhost"); + setField(client, "port", "8080"); + + UacQidCreatedPayloadDTO payload = new UacQidCreatedPayloadDTO(); + ResponseEntity responseEntity = ResponseEntity.ok(payload); + + try (MockedConstruction mocked = + mockConstruction( + RestTemplate.class, + (mock, context) -> + when(mock.exchange( + any(), eq(HttpMethod.GET), eq(null), eq(UacQidCreatedPayloadDTO.class))) + .thenReturn(responseEntity))) { + + // Act + UacQidCreatedPayloadDTO result = client.generateUacQid(99); + + // Assert + assertSame(payload, result); + + RestTemplate restTemplate = mocked.constructed().get(0); + + verify(restTemplate) + .exchange( + argThat( + uri -> { + String s = uri.toString(); + return s.equals("http://localhost:8080?questionnaireType=99"); + }), + eq(HttpMethod.GET), + eq(null), + eq(UacQidCreatedPayloadDTO.class)); + } + } + + // Helper to set private fields + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = UacQidServiceClient.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointIT.java b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointIT.java new file mode 100644 index 0000000..16e26ab --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointIT.java @@ -0,0 +1,523 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.*; +import static uk.gov.ons.census.caseapisvc.testutils.DataUtils.*; + +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import com.mashape.unirest.http.Unirest; +import com.mashape.unirest.http.exceptions.UnirestException; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.jeasy.random.EasyRandom; +import org.jeasy.random.EasyRandomParameters; +import org.junit.jupiter.api.BeforeEach; +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.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import uk.gov.ons.census.caseapisvc.model.dto.CaseContainerDTO; +import uk.gov.ons.census.caseapisvc.model.dto.CaseDetailsDTO; +import uk.gov.ons.census.caseapisvc.model.repository.*; +import uk.gov.ons.census.common.model.entity.*; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class CaseEndpointIT { + + private static final Logger log = LoggerFactory.getLogger(CaseEndpointIT.class); + + private static final String TEST_UPRN = "123456789012345"; + private static final String TEST_UPRN_EXISTS = "123456789012345"; + private static final String TEST_UPRN_DOES_NOT_EXIST = "999999999999999"; + + private static final String TEST_CASE_ID_1_EXISTS = "c0d4f87d-9d19-4393-80c9-9eb94f69c460"; + private static final String TEST_CASE_ID_2_EXISTS = "3e948f6a-00bb-466d-88a7-b0990a827b53"; + + private static final String TEST_CASE_ID_DOES_NOT_EXIST = "590179eb-f8ce-4e2d-8cb6-ca4013a2ccf1"; + private static final String TEST_INVALID_CASE_ID = "anything"; + + private static final String TEST_REFERENCE_DOES_NOT_EXIST = "9999999999"; + // private static final String ADDRESS_TYPE_TEST = "addressTypeTest"; + private static final String TEST_TOWN = "Tenby"; + private static final String TEST_POSTCODE_NO_SPACE = "AB12BC"; + private static final String TEST_POSTCODE_WITH_SPACE = "AB1 2BC"; + + private static final String ADDRESS_TYPE_TEST = "addressTypeTest"; + + @LocalServerPort private int port; + + @Autowired private CaseRepository caseRepository; + @Autowired private EventRepository eventRepository; + @Autowired private CollectionExerciseRepository collectionExerciseRepository; + @Autowired private SurveyRepository surveyRepository; + @Autowired private UacQidLinkRepository uacQidLinkRepository; + + private EasyRandom easyRandom; + + @BeforeEach + @Transactional + public void setUp() { + try { + clearDown(); + } catch (Exception e) { + // this is expected behaviour, where the event rows are deleted, then the case-processor image + // puts a new + // event row on and the case table clear down fails. 2nd run should clear it down + clearDown(); + } + + easyRandom = new EasyRandom(new EasyRandomParameters().randomizationDepth(1)); + } + + public void clearDown() { + eventRepository.deleteAllInBatch(); + uacQidLinkRepository.deleteAllInBatch(); + caseRepository.deleteAllInBatch(); + collectionExerciseRepository.deleteAllInBatch(); + surveyRepository.deleteAllInBatch(); + } + + @Test + public void shouldRetrieveMultipleCasesWithEventsWhenSearchingByUPRN() throws Exception { + setupTestCaseWithEvent(String.valueOf(UUID.randomUUID())); + setupTestCaseWithEvent(String.valueOf(UUID.randomUUID())); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/uprn/%s", port, TEST_UPRN_EXISTS)) + .header("accept", "application/json") + .queryString("caseEvents", "true") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + List actualData = extractCaseContainerDTOsFromResponse(response); + + assertThat(actualData.size()).isEqualTo(2); + + CaseContainerDTO case1 = actualData.get(0); + CaseContainerDTO case2 = actualData.get(1); + + assertThat(case1.getUprn()).isEqualTo(TEST_UPRN_EXISTS); + assertThat(case1.getCaseEvents().size()).isEqualTo(1); + + assertThat(case2.getUprn()).isEqualTo(TEST_UPRN_EXISTS); + assertThat(case2.getCaseEvents().size()).isEqualTo(1); + } + + @Test + public void getCaseByIdMinusEvents() { + setupTestCaseWithEvent(TEST_CASE_ID_1_EXISTS); + + RestTemplate restTemplate = new RestTemplate(); + String url = "http://localhost:" + port + "/cases/" + TEST_CASE_ID_1_EXISTS; + ResponseEntity foundCaseResponse = restTemplate.getForEntity(url, Case.class); + + Case actualCase = foundCaseResponse.getBody(); + assertThat(actualCase.getId()).isEqualTo(UUID.fromString(TEST_CASE_ID_1_EXISTS)); + } + + @Test + public void shouldRetrieveACaseWithEventsWhenSearchingByCaseId() throws Exception { + setupTestCaseWithEvent(TEST_CASE_ID_1_EXISTS); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/%s", port, TEST_CASE_ID_1_EXISTS)) + .header("accept", "application/json") + .queryString("caseEvents", "true") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseId()).isEqualTo(UUID.fromString(TEST_CASE_ID_1_EXISTS)); + assertThat(actualData.getCaseEvents().size()).isEqualTo(1); + } + + @Test + public void shouldRetrieveACaseWithoutEventsWhenSearchingByCaseId() throws Exception { + createOneTestCaseWithoutEvents(); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/%s", port, TEST_CASE_ID_1_EXISTS)) + .header("accept", "application/json") + .queryString("caseEvents", "false") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseId()).isEqualTo(UUID.fromString(TEST_CASE_ID_1_EXISTS)); + assertThat(actualData.getCaseEvents().size()).isEqualTo(0); + } + + @Test + public void shouldRetrieveACaseWithoutEventsByDefaultWhenSearchingByCaseId() throws Exception { + createOneTestCaseWithoutEvents(); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/%s", port, TEST_CASE_ID_1_EXISTS)) + .header("accept", "application/json") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseId()).isEqualTo(UUID.fromString(TEST_CASE_ID_1_EXISTS)); + assertThat(actualData.getCaseEvents().size()).isEqualTo(0); + } + + @Test + public void shouldReturn404WhenCaseIdNotFound() throws UnirestException { + HttpResponse jsonResponse = + Unirest.get(createUrl("http://localhost:%d/cases/%s", port, TEST_CASE_ID_DOES_NOT_EXIST)) + .header("accept", "application/json") + .asJson(); + + assertThat(jsonResponse.getStatus()).isEqualTo(NOT_FOUND.value()); + } + + @Test + public void shouldReturn400WhenInvalidCaseId() throws UnirestException { + HttpResponse jsonResponse = + Unirest.get(createUrl("http://localhost:%d/cases/%s", port, TEST_INVALID_CASE_ID)) + .header("accept", "application/json") + .asJson(); + + assertThat(jsonResponse.getStatus()).isEqualTo(BAD_REQUEST.value()); + } + + @Test + public void shouldRetrieveACaseWithEventsWhenSearchingByCaseReference() throws Exception { + Case expectedCase = setupTestCaseWithEvent(TEST_CASE_ID_1_EXISTS); + String expectedCaseRef = Long.toString(expectedCase.getCaseRef()); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/ref/%s", port, expectedCaseRef)) + .header("accept", "application/json") + .queryString("caseEvents", "true") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseRef()).isEqualTo(expectedCaseRef); + assertThat(actualData.getCaseEvents().size()).isEqualTo(1); + } + + @Test + public void shouldRetrieveACaseWithoutEventsWhenSearchingByCaseReference() throws Exception { + Case expectedCase = createOneTestCaseWithoutEvents(); + String expectedCaseRef = Long.toString(expectedCase.getCaseRef()); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/ref/%s", port, expectedCaseRef)) + .header("accept", "application/json") + .queryString("caseEvents", "false") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseRef()).isEqualTo(expectedCaseRef); + assertThat(actualData.getCaseEvents().size()).isEqualTo(0); + } + + @Test + public void shouldRetrieveACaseWithoutEventsByDefaultWhenSearchingByCaseReference() + throws Exception { + Case expectedCase = setupTestCaseWithoutEvents(String.valueOf(UUID.randomUUID())); + String expectedCaseRef = Long.toString(expectedCase.getCaseRef()); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/ref/%s", port, expectedCaseRef)) + .header("accept", "application/json") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseContainerDTO actualData = extractCaseContainerDTOFromResponse(response); + + assertThat(actualData.getCaseRef()).isEqualTo(expectedCaseRef); + assertThat(actualData.getCaseEvents().size()).isEqualTo(0); + } + + @Test + public void shouldReturn404WhenCaseReferenceNotFound() throws Exception { + HttpResponse jsonResponse = + Unirest.get( + createUrl("http://localhost:%d/cases/ref/%s", port, TEST_REFERENCE_DOES_NOT_EXIST)) + .header("accept", "application/json") + .asJson(); + + assertThat(jsonResponse.getStatus()).isEqualTo(NOT_FOUND.value()); + } + + @Test + public void getCasesByPostcode() throws IOException, UnirestException { + String case_1 = String.valueOf(UUID.randomUUID()); + setupTestCaseWithEvent(case_1); + String case_2 = String.valueOf(UUID.randomUUID()); + setupTestCaseWithEvent(case_2); + Optional caseObj1 = + Optional.of( + caseRepository + .findById(UUID.fromString(case_1)) + .orElseThrow(() -> new RuntimeException("Case not found!"))); + + Optional caseObj2 = + Optional.of( + caseRepository + .findById(UUID.fromString(case_2)) + .orElseThrow(() -> new RuntimeException("Case not found!"))); + + HttpResponse response = + Unirest.get(createUrl("http://localhost:%d/cases/postcode/%s", port, TEST_POSTCODE)) + .header("accept", "application/json") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + List actualData = extractCaseContainerDTOsFromResponse(response); + + assertThat(actualData.size()).isEqualTo(2); + + CaseContainerDTO case1 = actualData.get(0); + CaseContainerDTO case2 = actualData.get(1); + + assertThat(case1.getPostcode()).isEqualTo(TEST_POSTCODE); + assertThat(case2.getPostcode()).isEqualTo(TEST_POSTCODE); + } + + @Test + public void getAllCaseDetails() throws IOException, UnirestException { + Case caze = createOneTestCaseWithEvent(); + + HttpResponse response = + Unirest.get( + createUrl("http://localhost:%d/cases/case-details/%s", port, TEST_CASE_ID_1_EXISTS)) + .header("accept", "application/json") + .asJson(); + + assertThat(response.getStatus()).isEqualTo(OK.value()); + + CaseDetailsDTO actualCaseDetails = extractCaseDetailsDTOsFromResponse(response); + + assertThat(actualCaseDetails.getCaseId()).isEqualTo(caze.getId()); + } + + public static CaseDetailsDTO extractCaseDetailsDTOsFromResponse(HttpResponse response) + throws IOException { + return mapper.readValue(response.getBody().getObject().toString(), CaseDetailsDTO.class); + } + + private Case createOneTestCaseWithoutEvents() { + return setupTestCaseWithoutEvents(TEST_CASE_ID_1_EXISTS); + } + + private void createTwoTestCasesWithEvents() { + setupTestCaseWithEvent(TEST_CASE_ID_1_EXISTS); + setupTestCaseWithEvent(TEST_CASE_ID_2_EXISTS); + } + + private Case createOneTestCaseWithEvent() { + return setupTestCaseWithEvent(TEST_CASE_ID_1_EXISTS); + } + + private void createTwoTestCasesWithoutEvents() { + setupTestCaseWithoutEvents(TEST_CASE_ID_1_EXISTS); + setupTestCaseWithoutEvents(TEST_CASE_ID_2_EXISTS); + } + + private Case setupTestCaseWithEvent(String caseId) { + + Survey junkSurvey = new Survey(); + junkSurvey.setId(UUID.randomUUID()); + junkSurvey.setName("Junk survey"); + junkSurvey.setSampleSeparator('j'); + surveyRepository.saveAndFlush(junkSurvey); + + CollectionExercise junkCollectionExercise = new CollectionExercise(); + junkCollectionExercise.setId(UUID.randomUUID()); + junkCollectionExercise.setName("Junk collex"); + junkCollectionExercise.setSurvey(junkSurvey); + junkCollectionExercise.setReference("MVP012021"); + junkCollectionExercise.setStartDate(OffsetDateTime.now()); + junkCollectionExercise.setEndDate(OffsetDateTime.now().plusDays(2)); + junkCollectionExercise.setMetadata(null); + collectionExerciseRepository.saveAndFlush(junkCollectionExercise); + + Case caze = easyRandom.nextObject(Case.class); + caze.setId(UUID.fromString(caseId)); + caze.setEvents(null); + caze.setUprn(TEST_UPRN_EXISTS); + caze.setReceiptReceived(false); + caze.setPostcode(TEST_POSTCODE); + caze.setCollectionExercise(junkCollectionExercise); + caseRepository.saveAndFlush(caze); + + UacQidLink uacQidLink = new UacQidLink(); + uacQidLink.setId(UUID.randomUUID()); + uacQidLink.setActive(true); + uacQidLink.setQid(easyRandom.nextObject(String.class)); + uacQidLink.setUac("test_uac_1"); + uacQidLink.setUacHash("fakeHash_1"); + uacQidLink.setCaze(caze); + uacQidLinkRepository.save(uacQidLink); + + caze = + caseRepository + .findById(UUID.fromString(caseId)) + .orElseThrow(() -> new RuntimeException("Case not found!")); + caze.setUacQidLinks(List.of(uacQidLink)); + caseRepository.saveAndFlush(caze); + + Event event = new Event(); + event.setId(UUID.randomUUID()); + event.setCaze(null); + event.setType(EventType.NEW_CASE); + event.setUacQidLink(uacQidLink); + event.setChannel("RM"); + event.setCorrelationId(UUID.randomUUID()); + event.setPayload("{}"); + event.setDescription("description"); + event.setMessageId(UUID.randomUUID()); + event.setCreatedBy(""); + event.setMessageTimestamp(OffsetDateTime.now()); + event.setProcessedAt(OffsetDateTime.now()); + event.setSource(""); + event.setDateTime(OffsetDateTime.now()); + + eventRepository.save(event); + + return caseRepository + .findById(UUID.fromString(caseId)) + .orElseThrow(() -> new RuntimeException("Case not found!")); + } + + private void setupTestUacQidLink(String qid, Case caze) { + UacQidLink uacQidLink = new UacQidLink(); + uacQidLink.setId(UUID.randomUUID()); + uacQidLink.setCaze(caze); + uacQidLink.setQid(qid); + + uacQidLinkRepository.saveAndFlush(uacQidLink); + } + + private Case setupTestCaseWithoutEvents(String id) { + Case caze = getACase(id); + Survey junkSurvey = new Survey(); + junkSurvey.setId(UUID.randomUUID()); + junkSurvey.setName("Junk survey"); + junkSurvey.setSampleSeparator('j'); + surveyRepository.saveAndFlush(junkSurvey); + + CollectionExercise junkCollectionExercise = new CollectionExercise(); + junkCollectionExercise.setId(UUID.randomUUID()); + junkCollectionExercise.setName("Junk collex"); + junkCollectionExercise.setSurvey(junkSurvey); + junkCollectionExercise.setReference("MVP012021"); + junkCollectionExercise.setStartDate(OffsetDateTime.now()); + junkCollectionExercise.setEndDate(OffsetDateTime.now().plusDays(2)); + junkCollectionExercise.setMetadata(null); + collectionExerciseRepository.saveAndFlush(junkCollectionExercise); + + caze.setCollectionExercise(junkCollectionExercise); + + return saveAndRetrieveCase(caze); + } + + private Case saveAndRetrieveCase(Case caze) { + + caseRepository.save(caze); + + UacQidLink uacQidLink = new UacQidLink(); + uacQidLink.setId(UUID.randomUUID()); + uacQidLink.setActive(true); + uacQidLink.setQid("Q123"); + uacQidLink.setUac("test_uac"); + uacQidLink.setUacHash("fakeHash"); + uacQidLink.setCaze(caze); + uacQidLinkRepository.save(uacQidLink); + + caze = + caseRepository + .findById(caze.getId()) + .orElseThrow(() -> new RuntimeException("Case not found!")); + caze.setUacQidLinks(List.of(uacQidLink)); + caseRepository.saveAndFlush(caze); + + return caseRepository + .findById(caze.getId()) + .orElseThrow(() -> new RuntimeException("Case not found!")); + } + + private String createUrl(String urlFormat, int port, String param1) { + return String.format(urlFormat, port, param1); + } + + private Case getACase(String caseId) { + Case caze = new Case(); // easyRandom.nextObject(Case.class); + caze.setId(UUID.fromString(caseId)); + caze.setCaseRef(1L); + caze.setEvents(null); + caze.setUprn(TEST_UPRN_EXISTS); + caze.setReceiptReceived(false); + caze.setAddressType(ADDRESS_TYPE_TEST); + caze.setUacQidLinks(null); + return caze; + } + + private Case setupTestCaseWithInvalid(String caseId) { + Case caze = getACase(caseId); + caze.setInvalid(true); + + return saveAndRetreiveCase(caze); + } + + private Case setupUnitTestCaseWithTreatmentCode(String caseId, String treatmentCode) { + Case caze = getACase(caseId); + caze.setCaseType("HH"); + caze.setTreatmentCode(treatmentCode); + caze.setRegion("E1000"); + caze.setAddressLevel("U"); + + return saveAndRetreiveCase(caze); + } + + private Case setUpSPGUnitCaseWithTreatmentCode(String caseId, String treatmentCode) { + Case caze = getACase(caseId); + caze.setTreatmentCode(treatmentCode); + caze.setRegion("E1000"); + caze.setAddressLevel("U"); + caze.setCaseType("SPG"); + + return saveAndRetreiveCase(caze); + } + + private Case saveAndRetreiveCase(Case caze) { + caseRepository.saveAndFlush(caze); + + return caseRepository + .findById(caze.getId()) + .orElseThrow(() -> new RuntimeException("Case not found!")); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointUnitTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointUnitTest.java new file mode 100644 index 0000000..a2e1b58 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/CaseEndpointUnitTest.java @@ -0,0 +1,179 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.ons.census.caseapisvc.model.dto.*; +import uk.gov.ons.census.caseapisvc.service.CaseService; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.Event; +import uk.gov.ons.census.common.model.entity.EventType; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@ExtendWith(MockitoExtension.class) +class CaseEndpointUnitTest { + + @Mock private CaseService caseService; + + @InjectMocks private uk.gov.ons.census.caseapisvc.endpoint.CaseEndpoint caseEndpoint; + + private Case caze; + private UUID caseId; + + @BeforeEach + void setup() { + caseId = UUID.randomUUID(); + caze = mock(Case.class); + + when(caze.getId()).thenReturn(caseId); + } + + // ------------------------------------------------------------------------- + // findCaseById + // ------------------------------------------------------------------------- + @Test + void testFindCaseById_NoEvents() { + when(caseService.findById(caseId)).thenReturn(caze); + when(caze.getCaseRef()).thenReturn(12345L); + + CaseContainerDTO dto = caseEndpoint.findCaseById(caseId, false); + + assertEquals(caseId, dto.getCaseId()); + assertEquals("12345", dto.getCaseRef()); + assertTrue(dto.getCaseEvents().isEmpty()); + verify(caseService).findById(caseId); + } + + // ------------------------------------------------------------------------- + // findCaseByReference + // ------------------------------------------------------------------------- + @Test + void testFindCaseByReference() { + + when(caseService.findByReference(12345L)).thenReturn(caze); + when(caze.getCaseRef()).thenReturn(12345L); + + CaseContainerDTO dto = caseEndpoint.findCaseByReference(12345L, false); + + assertEquals(caseId, dto.getCaseId()); + assertEquals("12345", dto.getCaseRef()); + verify(caseService).findByReference(12345L); + } + + // ------------------------------------------------------------------------- + // findCasesByUPRN + // ------------------------------------------------------------------------- + @Test + void testFindCasesByUPRN_WithEvents() { + // Mock event + Event event = mock(Event.class); + when(event.getId()).thenReturn(UUID.randomUUID()); + when(event.getDescription()).thenReturn("TEST_EVENT"); + when(event.getDateTime()).thenReturn(OffsetDateTime.now()); + when(event.getType()).thenReturn(EventType.NEW_CASE); + + // Mock UAC/QID link + UacQidLink link = mock(UacQidLink.class); + when(link.getEvents()).thenReturn(List.of(event)); + + when(caze.getUacQidLinks()).thenReturn(List.of(link)); + when(caze.getEvents()).thenReturn(List.of(event)); + + when(caseService.findByUPRN("123456789", true)).thenReturn(List.of(caze)); + + List result = caseEndpoint.findCasesByUPRN("123456789", true, true); + + assertEquals(1, result.size()); + assertEquals(2, result.get(0).getCaseEvents().size()); // 1 from UAC, 1 from Case + } + + // ------------------------------------------------------------------------- + // getCasesByPostcode + // ------------------------------------------------------------------------- + @Test + void testGetCasesByPostcode() { + when(caseService.findByPostcode("AB12CD")).thenReturn(List.of(caze)); + + List result = caseEndpoint.getCasesByPostcode("AB12CD"); + + assertEquals(1, result.size()); + assertEquals(caseId, result.get(0).getCaseId()); + verify(caseService).findByPostcode("AB12CD"); + } + + // ------------------------------------------------------------------------- + // findCaseByQid + // ------------------------------------------------------------------------- + @Test + void testFindCaseByQid() { + when(caze.getAddressType()).thenReturn("HH"); + when(caseService.findCaseByQid("Q123")).thenReturn(caze); + + CaseContainerDTO dto = caseEndpoint.findCaseByQid("Q123"); + + assertEquals(caseId, dto.getCaseId()); + assertEquals("HH", dto.getAddressType()); + verify(caseService).findCaseByQid("Q123"); + } + + // ------------------------------------------------------------------------- + // getAllCaseDetailsByCaseId + // ------------------------------------------------------------------------- + @Test + void testGetAllCaseDetailsByCaseId() { + when(caze.getCollectionExercise()) + .thenReturn(mock(uk.gov.ons.census.common.model.entity.CollectionExercise.class)); + when(caze.getCollectionExercise().getId()).thenReturn(UUID.randomUUID()); + when(caseService.findById(caseId)).thenReturn(caze); + + CaseDetailsDTO dto = caseEndpoint.getAllCaseDetailsByCaseId(caseId); + + assertEquals(caseId, dto.getCaseId()); + verify(caseService).findById(caseId); + } + + // ------------------------------------------------------------------------- + // buildCaseContainerDTO: event mapping + // ------------------------------------------------------------------------- + @Test + void testBuildCaseContainerDTO_MapsEventsCorrectly() { + Event event = mock(Event.class); + when(event.getId()).thenReturn(UUID.randomUUID()); + when(event.getDescription()).thenReturn("DESC"); + when(event.getDateTime()).thenReturn(OffsetDateTime.now()); + when(event.getType()).thenReturn(EventType.NEW_CASE); + + UacQidLink link = mock(UacQidLink.class); + when(link.getEvents()).thenReturn(List.of(event)); + + when(caze.getUacQidLinks()).thenReturn(List.of(link)); + when(caze.getEvents()).thenReturn(List.of(event)); + + CaseContainerDTO dto = invokeBuildCaseContainerDTO(caze, true); + + assertEquals(2, dto.getCaseEvents().size()); + assertEquals("DESC", dto.getCaseEvents().get(0).getDescription()); + } + + // Helper to call private method via reflection + private CaseContainerDTO invokeBuildCaseContainerDTO(Case caze, boolean includeEvents) { + try { + var method = + uk.gov.ons.census.caseapisvc.endpoint.CaseEndpoint.class.getDeclaredMethod( + "buildCaseContainerDTO", Case.class, boolean.class); + method.setAccessible(true); + return (CaseContainerDTO) method.invoke(caseEndpoint, caze, includeEvents); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointIT.java b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointIT.java new file mode 100644 index 0000000..9e80dc6 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointIT.java @@ -0,0 +1,104 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +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.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.model.dto.NewQidLink; +import uk.gov.ons.census.caseapisvc.model.dto.QidLink; +import uk.gov.ons.census.caseapisvc.service.CaseService; +import uk.gov.ons.census.caseapisvc.service.UacQidService; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(QidEndpoint.class) +@ActiveProfiles("test") +class QidEndpointIT { + + @Autowired private MockMvc mockMvc; + + @MockBean private UacQidService uacQidService; + + @MockBean private CaseService caseService; + + @Autowired private ObjectMapper objectMapper; + + // ------------------------------------------------------------------------- + // GET /qids/{qid} + // ------------------------------------------------------------------------- + @Test + void testGetUacQidLinkByQid() throws Exception { + UUID caseId = UUID.randomUUID(); + + UacQidLink link = mock(UacQidLink.class); + Case caze = mock(Case.class); + + when(link.getQid()).thenReturn("123456789012"); + when(link.getCaze()).thenReturn(caze); + when(caze.getId()).thenReturn(caseId); + + when(uacQidService.findUacQidLinkByQid("123456789012")).thenReturn(link); + + mockMvc + .perform(get("/qids/123456789012")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.questionnaireId").value("123456789012")) + .andExpect(jsonPath("$.caseId").value(caseId.toString())); + } + + // ------------------------------------------------------------------------- + // PUT /qids/link + // ------------------------------------------------------------------------- + @Test + void testPutQidLinkToCase() throws Exception { + UUID caseId = UUID.randomUUID(); + + // Mock incoming JSON + NewQidLink newQidLink = new NewQidLink(); + QidLink qidLink = new QidLink(); + qidLink.setQuestionnaireId("111222333444"); + qidLink.setCaseId(caseId); + newQidLink.setQidLink(qidLink); + + // Mock service layer + UacQidLink link = mock(UacQidLink.class); + Case caze = mock(Case.class); + + when(uacQidService.findUacQidLinkByQid("111222333444")).thenReturn(link); + + when(caseService.findById(caseId)).thenReturn(caze); + + var result = + mockMvc + .perform( + put("/qids/link") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newQidLink))) + .andExpect(status().isNotImplemented()); + + // Extract the exception thrown by the controller + Exception resolved = result.andReturn().getResolvedException(); + + ResponseStatusException ex = (ResponseStatusException) resolved; + Assertions.assertNotNull(ex); + assertThat(ex.getStatusCode().value()).isEqualTo(501); + assertThat(ex.getReason()) + .isEqualTo("Questionnaire Id Link is not available, request cannot be fulfilled"); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointTest.java new file mode 100644 index 0000000..7680291 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/endpoint/QidEndpointTest.java @@ -0,0 +1,93 @@ +package uk.gov.ons.census.caseapisvc.endpoint; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.model.dto.NewQidLink; +import uk.gov.ons.census.caseapisvc.model.dto.QidLink; +import uk.gov.ons.census.caseapisvc.service.CaseService; +import uk.gov.ons.census.caseapisvc.service.UacQidService; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +class QidEndpointTest { + + private UacQidService uacQidService; + private CaseService caseService; + private QidEndpoint endpoint; + + @BeforeEach + void setup() { + uacQidService = mock(UacQidService.class); + caseService = mock(CaseService.class); + endpoint = new QidEndpoint(uacQidService, caseService); + } + + // ------------------------------------------------------------------------- + // GET /qids/{qid} + // ------------------------------------------------------------------------- + @Test + void getUacQidLinkByQid_withCase() { + UUID caseId = UUID.randomUUID(); + + UacQidLink link = mock(UacQidLink.class); + Case caze = mock(Case.class); + + when(link.getQid()).thenReturn("Q123"); + when(link.getCaze()).thenReturn(caze); + when(caze.getId()).thenReturn(caseId); + + when(uacQidService.findUacQidLinkByQid("Q123")).thenReturn(link); + + QidLink dto = endpoint.getUacQidLinkByQid("Q123"); + + assertEquals("Q123", dto.getQuestionnaireId()); + assertEquals(caseId, dto.getCaseId()); + } + + @Test + void getUacQidLinkByQid_withoutCase() { + UacQidLink link = mock(UacQidLink.class); + when(link.getQid()).thenReturn("Q123"); + when(link.getCaze()).thenReturn(null); + + when(uacQidService.findUacQidLinkByQid("Q123")).thenReturn(link); + + QidLink dto = endpoint.getUacQidLinkByQid("Q123"); + + assertEquals("Q123", dto.getQuestionnaireId()); + assertNull(dto.getCaseId()); + } + + // ------------------------------------------------------------------------- + // PUT /qids/link + // ------------------------------------------------------------------------- + @Test + void putQidLinkToCase_dispatchesEvent() { + UUID caseId = UUID.randomUUID(); + + NewQidLink newQidLink = new NewQidLink(); + QidLink qidLink = new QidLink(); + qidLink.setQuestionnaireId("Q999"); + qidLink.setCaseId(caseId); + newQidLink.setQidLink(qidLink); + + UacQidLink link = mock(UacQidLink.class); + Case caze = mock(Case.class); + + when(uacQidService.findUacQidLinkByQid("Q999")).thenReturn(link); + when(caseService.findById(caseId)).thenReturn(caze); + + // Expect the NOT_IMPLEMENTED exception + assertThrows(ResponseStatusException.class, () -> endpoint.putQidLinkToCase(newQidLink)); + + // Below commented shall be uncommented when the subscription for the questionnaire link is + // available + // Verify event dispatch still happened before the exception + // verify(uacQidService).buildAndSendQuestionnaireLinkedEvent(link, caze, newQidLink); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/CollectionExerciseRepository.java b/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/CollectionExerciseRepository.java new file mode 100644 index 0000000..e9a1415 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/CollectionExerciseRepository.java @@ -0,0 +1,11 @@ +package uk.gov.ons.census.caseapisvc.model.repository; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; +import uk.gov.ons.census.common.model.entity.CollectionExercise; + +@Component +@ActiveProfiles("test") +public interface CollectionExerciseRepository extends JpaRepository {} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/SurveyRepository.java b/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/SurveyRepository.java new file mode 100644 index 0000000..e69d4b9 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/model/repository/SurveyRepository.java @@ -0,0 +1,11 @@ +package uk.gov.ons.census.caseapisvc.model.repository; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; +import uk.gov.ons.census.common.model.entity.Survey; + +@Component +@ActiveProfiles("test") +public interface SurveyRepository extends JpaRepository {} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/service/CaseServiceTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/service/CaseServiceTest.java new file mode 100644 index 0000000..96686ee --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/service/CaseServiceTest.java @@ -0,0 +1,165 @@ +package uk.gov.ons.census.caseapisvc.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static uk.gov.ons.census.caseapisvc.testutils.DataUtils.createMultipleCasesWithEvents; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.model.repository.CaseRepository; +import uk.gov.ons.census.caseapisvc.model.repository.UacQidLinkRepository; +import uk.gov.ons.census.common.model.entity.Case; + +@ExtendWith(MockitoExtension.class) +class CaseServiceTest { + @Mock CaseRepository caseRepository; + @Mock UacQidLinkRepository uacQidLinkRepository; + + @InjectMocks CaseService caseService; + + private static final int TEST_CASE_REFERENCE_ID_EXISTS = 123; + private static final String RM_TELEPHONE_CAPTURE_HOUSEHOLD_INDIVIDUAL = "RM_TC_HI"; + private static final UUID TEST_CASE_ID_EXISTS = UUID.randomUUID(); + private static final UUID TEST_CASE_ID_DOES_NOT_EXIST = UUID.randomUUID(); + + private static final String TEST_UPRN = "123"; + public static final String TEST_QID = "test_qid"; + + @Test + public void getMultipleCasesWhenUPRNExists() { + when(caseRepository.findByUprn(anyString())) + .thenReturn(Optional.of(createMultipleCasesWithEvents().toList())); + + List actualCases = caseService.findByUPRN(TEST_UPRN, eq(false)); + assertThat(actualCases.size()).isEqualTo(2); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(caseRepository).findByUprn(captor.capture()); + String actualCaseId = captor.getValue(); + assertThat(actualCaseId).isEqualTo(TEST_UPRN); + } + + // ---------------------------------------------------------------------- + // findById + // ---------------------------------------------------------------------- + @Test + void findById_returnsCase() { + UUID id = UUID.randomUUID(); + Case caze = new Case(); + when(caseRepository.findById(id)).thenReturn(Optional.of(caze)); + + Case result = caseService.findById(id); + + assertSame(caze, result); + } + + @Test + void findById_throwsWhenNotFound() { + UUID id = UUID.randomUUID(); + when(caseRepository.findById(id)).thenReturn(Optional.empty()); + + ResponseStatusException ex = + assertThrows(ResponseStatusException.class, () -> caseService.findById(id)); + + assertTrue(ex.getReason().contains(id.toString())); + } + + // ---------------------------------------------------------------------- + // findByReference + // ---------------------------------------------------------------------- + @Test + void findByReference_returnsCase() { + long ref = 123L; + Case caze = new Case(); + when(caseRepository.findByCaseRef(ref)).thenReturn(Optional.of(caze)); + + Case result = caseService.findByReference(ref); + + assertSame(caze, result); + } + + @Test + void findByReference_throwsWhenNotFound() { + long ref = 123L; + when(caseRepository.findByCaseRef(ref)).thenReturn(Optional.empty()); + + assertThrows(ResponseStatusException.class, () -> caseService.findByReference(ref)); + } + + // ---------------------------------------------------------------------- + // findByUPRN + // ---------------------------------------------------------------------- + @Test + void findByUPRN_validAddressOnly_returnsList() { + String uprn = "UPRN123"; + List cases = List.of(new Case()); + when(caseRepository.findByUprnAndInvalidFalse(uprn)).thenReturn(Optional.of(cases)); + + List result = caseService.findByUPRN(uprn, true); + + assertEquals(cases, result); + } + + @Test + void findByUPRN_validAddressOnly_throwsWhenNotFound() { + String uprn = "UPRN123"; + when(caseRepository.findByUprnAndInvalidFalse(uprn)).thenReturn(Optional.empty()); + + assertThrows(ResponseStatusException.class, () -> caseService.findByUPRN(uprn, true)); + } + + @Test + void findByUPRN_allAddresses_returnsList() { + String uprn = "UPRN123"; + List cases = List.of(new Case()); + when(caseRepository.findByUprn(uprn)).thenReturn(Optional.of(cases)); + + List result = caseService.findByUPRN(uprn, false); + + assertEquals(cases, result); + } + + @Test + void findByUPRN_allAddresses_throwsWhenNotFound() { + String uprn = "UPRN123"; + when(caseRepository.findByUprn(uprn)).thenReturn(Optional.empty()); + + assertThrows(ResponseStatusException.class, () -> caseService.findByUPRN(uprn, false)); + } + + @Test + void getCaseByCaseId() { + Case caze = new Case(); + caze.setId(UUID.randomUUID()); + Optional caseOpt = Optional.of(caze); + when(caseRepository.findById(any())).thenReturn(caseOpt); + + Case returnedCase = caseService.findById(caze.getId()); + Assertions.assertThat(returnedCase).isEqualTo(caze); + verify(caseRepository).findById(caze.getId()); + } + + // ---------------------------------------------------------------------- + // findByPostcode + // ---------------------------------------------------------------------- + @Test + void findByPostcode_returnsList() { + String postcode = "SM1 1AA"; + List cases = List.of(new Case()); + when(caseRepository.findByPostcode(postcode)).thenReturn(cases); + + List result = caseService.findByPostcode(postcode); + + assertEquals(cases, result); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/service/UacQidServiceTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/service/UacQidServiceTest.java new file mode 100644 index 0000000..784c8aa --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/service/UacQidServiceTest.java @@ -0,0 +1,154 @@ +package uk.gov.ons.census.caseapisvc.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.ons.census.caseapisvc.client.UacQidServiceClient; +import uk.gov.ons.census.caseapisvc.model.dto.EventDTO; +import uk.gov.ons.census.caseapisvc.model.dto.NewQidLink; +import uk.gov.ons.census.caseapisvc.model.dto.UacQidCreatedPayloadDTO; +import uk.gov.ons.census.caseapisvc.model.repository.UacQidLinkRepository; +import uk.gov.ons.census.caseapisvc.utility.PubSubHelper; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +public class UacQidServiceTest { + + private UacQidServiceClient uacQidServiceClient; + private UacQidLinkRepository uacQidLinkRepository; + private PubSubHelper pubSubHelper; + + private UacQidService service; + + @BeforeEach + void setup() { + uacQidServiceClient = mock(UacQidServiceClient.class); + uacQidLinkRepository = mock(UacQidLinkRepository.class); + pubSubHelper = mock(PubSubHelper.class); + + service = new UacQidService(uacQidServiceClient, uacQidLinkRepository, pubSubHelper); + + service.questionnaireLinkedTopic = "questionnaire-linked"; + service.pubsubProject = "test-project"; + } + + // ---------------------------------------------------------------------- + // createAndLinkUacQid + // ---------------------------------------------------------------------- + @Test + void createAndLinkUacQid_setsCaseId() { + UUID caseId = UUID.randomUUID(); + UacQidCreatedPayloadDTO dto = new UacQidCreatedPayloadDTO(); + when(uacQidServiceClient.generateUacQid(1)).thenReturn(dto); + + UacQidCreatedPayloadDTO result = service.createAndLinkUacQid(caseId, 1); + + assertEquals(caseId, result.getCaseId()); + verify(uacQidServiceClient).generateUacQid(1); + } + + // ---------------------------------------------------------------------- + // findUacQidLinkByQid + // ---------------------------------------------------------------------- + @Test + void findUacQidLinkByQid_returnsLink() { + UacQidLink link = new UacQidLink(); + when(uacQidLinkRepository.findByQid("QID123")).thenReturn(Optional.of(link)); + + UacQidLink result = service.findUacQidLinkByQid("QID123"); + + assertSame(link, result); + } + + @Test + void findUacQidLinkByQid_throwsWhenNotFound() { + when(uacQidLinkRepository.findByQid("QID123")).thenReturn(Optional.empty()); + + assertThrows(ResponseStatusException.class, () -> service.findUacQidLinkByQid("QID123")); + } + + // ---------------------------------------------------------------------- + // calculateQuestionnaireType + // ---------------------------------------------------------------------- + @Test + void calculateQuestionnaireType_householdEngland() { + int result = UacQidService.calculateQuestionnaireType("HH", "E123", "U", "CENSUS"); + assertEquals(1, result); + } + + @Test + void calculateQuestionnaireType_individualWales() { + int result = UacQidService.calculateQuestionnaireType("HH", "W123", "U", "CENSUS", true); + assertEquals(22, result); + } + + @Test + void calculateQuestionnaireType_ceEstabNI() { + int result = UacQidService.calculateQuestionnaireType("CE", "N123", "E", "CENSUS"); + assertEquals(34, result); + } + + @Test + void calculateQuestionnaireType_ccsAlways71() { + int result = UacQidService.calculateQuestionnaireType("HH", "E123", "U", "CCS"); + assertEquals(71, result); + } + + @Test + void calculateQuestionnaireType_invalidCountry_throws() { + assertThrows( + IllegalArgumentException.class, + () -> UacQidService.calculateQuestionnaireType("HH", "X999", "U", "CENSUS")); + } + + // ---------------------------------------------------------------------- + // buildAndSendQuestionnaireLinkedEvent + // ---------------------------------------------------------------------- + @Test + void buildAndSendQuestionnaireLinkedEvent_sendsMessageToPubSub() { + // Arrange + UacQidLink link = new UacQidLink(); + link.setQid("QID123"); + + Case caze = new Case(); + UUID caseId = UUID.randomUUID(); + caze.setId(caseId); + UUID tranxId = UUID.randomUUID(); + + NewQidLink newQidLink = new NewQidLink(); + newQidLink.setChannel("WEB"); + newQidLink.setTransactionId(tranxId); + + // Capture arguments + ArgumentCaptor topicCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(EventDTO.class); + + try { + // Act + service.buildAndSendQuestionnaireLinkedEvent(link, caze, newQidLink); + } catch (HttpServerErrorException.NotImplemented ex) { + // Assert topic + verify(pubSubHelper).publishAndConfirm(topicCaptor.capture(), eventCaptor.capture()); + // verify(messageSender).sendMessage(topicCaptor.capture(), eventCaptor.capture()); + String topic = topicCaptor.getValue(); + assertEquals("questionnaire-linked", topic); + + // Assert event payload + EventDTO event = eventCaptor.getValue(); + assertEquals("WEB", event.getHeader().getChannel()); + assertEquals(tranxId, event.getHeader().getMessageId()); + assertEquals("questionnaire-linked", event.getHeader().getTopic()); + assertNotNull(event.getHeader().getDateTime()); + + assertEquals(caseId, event.getPayload().getUac().getCaseId()); + assertEquals("QID123", event.getPayload().getUac().getQuestionnaireId()); + } + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/DataUtils.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/DataUtils.java new file mode 100644 index 0000000..0b20052 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/DataUtils.java @@ -0,0 +1,116 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import static java.time.OffsetDateTime.now; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; +import org.json.JSONArray; +import uk.gov.ons.census.caseapisvc.model.dto.CaseContainerDTO; +import uk.gov.ons.census.caseapisvc.model.dto.UacQidCreatedPayloadDTO; +import uk.gov.ons.census.common.model.entity.Case; +import uk.gov.ons.census.common.model.entity.Event; +import uk.gov.ons.census.common.model.entity.EventType; +import uk.gov.ons.census.common.model.entity.UacQidLink; + +public class DataUtils { + + private static final UUID TEST1_CASE_ID = UUID.fromString("2e083ab1-41f7-4dea-a3d9-77f48458b5ca"); + private static final long TEST1_CASE_REFERENCE_ID = 1234567890; + + private static final UUID TEST2_CASE_ID = UUID.fromString("3e948f6a-00bb-466d-88a7-b0990a827b53"); + private static final long TEST2_CASE_REFERENCE_ID = 1234567890; + + private static final String TEST_UPRN = "123"; + private static final String TEST_ESTAB_UPRN = "4567"; + + public static final String CREATED_UAC = "created UAC"; + public static final String TEST_POSTCODE = "AB1 2BC"; + + public static final ObjectMapper mapper; + + static { + mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + } + + public static Case createSingleCaseWithEvents() { + return createCase(TEST1_CASE_ID, TEST1_CASE_REFERENCE_ID); + } + + public static Stream createMultipleCasesWithEvents() { + return Stream.of( + createCase(TEST1_CASE_ID, TEST1_CASE_REFERENCE_ID), + createCase(TEST2_CASE_ID, TEST2_CASE_REFERENCE_ID)); + } + + private static Case createCase(UUID id, long caseRef) { + List uacQidLinks = new LinkedList<>(); + List events = new LinkedList<>(); + + UacQidLink uacQidLink = createUacQidLink(); + + Event event = new Event(); + event.setId(UUID.randomUUID()); + event.setDescription("Case created"); + event.setUacQidLink(uacQidLink); + event.setType(EventType.NEW_CASE); + events.add(event); + + uacQidLinks.add(uacQidLink); + + Case caze = new Case(); + caze.setId(id); + caze.setCaseRef(caseRef); + caze.setUacQidLinks(uacQidLinks); + caze.setEvents(events); + + caze.setUprn(TEST_UPRN); + caze.setEstabUprn(TEST_ESTAB_UPRN); + caze.setPostcode(TEST_POSTCODE); + caze.setCreatedAt(now()); + caze.setLastUpdatedAt(now()); + + return caze; + } + + public static UacQidCreatedPayloadDTO createUacQidCreatedPayload(String qid) { + UacQidCreatedPayloadDTO uacQidCreatedPayloadDTO = new UacQidCreatedPayloadDTO(); + uacQidCreatedPayloadDTO.setQid(qid); + uacQidCreatedPayloadDTO.setUac(CREATED_UAC); + return uacQidCreatedPayloadDTO; + } + + public static CaseContainerDTO extractCaseContainerDTOFromResponse( + HttpResponse response) throws IOException { + return mapper.readValue(response.getBody().getObject().toString(), CaseContainerDTO.class); + } + + public static List extractCaseContainerDTOsFromResponse( + HttpResponse response) throws IOException { + List dtos = new LinkedList<>(); + JSONArray elements = response.getBody().getArray(); + + for (int i = 0; i < elements.length(); i++) { + dtos.add(mapper.readValue(elements.get(i).toString(), CaseContainerDTO.class)); + } + + return dtos; + } + + public static UacQidLink createUacQidLink() { + UacQidLink uacQidLink = new UacQidLink(); + uacQidLink.setId(UUID.randomUUID()); + uacQidLink.setUac("any UAC"); + uacQidLink.setQid("any QID"); + uacQidLink.setEvents(Collections.emptyList()); + return uacQidLink; + } + + public static String createUrl(String urlFormat, String param1) { + return String.format(urlFormat, param1); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/JunkDataHelper.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/JunkDataHelper.java new file mode 100644 index 0000000..a2d17c2 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/JunkDataHelper.java @@ -0,0 +1,97 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import java.time.OffsetDateTime; +import java.util.Random; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; +import uk.gov.ons.census.caseapisvc.model.dto.EventHeaderDTO; +import uk.gov.ons.census.caseapisvc.model.repository.*; +import uk.gov.ons.census.common.model.entity.*; + +@Component +@ActiveProfiles("test") +public class JunkDataHelper { + private static final Random RANDOM = new Random(); + + @Autowired private CaseRepository caseRepository; + @Autowired private CollectionExerciseRepository collectionExerciseRepository; + @Autowired private SurveyRepository surveyRepository; + + public Case setupJunkCase() { + Case junkCase = new Case(); + junkCase.setId(UUID.randomUUID()); + junkCase.setInvalid(false); + junkCase.setCollectionExercise(setupJunkCollex()); + junkCase.setCaseRef(RANDOM.nextLong()); + junkCase.setAbpCode("abp"); + junkCase.setAddressLevel("U"); + junkCase.setAddressLine1("10 test street"); + junkCase.setAddressType("HH"); + junkCase.setCaseType("HH"); + junkCase.setEstabType("HOUSEHOLD"); + junkCase.setEstabUprn("000000"); + junkCase.setPrintBatch("1"); + junkCase.setFieldCoordinatorId("fcor_id"); + junkCase.setFieldOfficerId("foff_id"); + junkCase.setHtcDigital("0"); + junkCase.setHtcWillingness("0"); + junkCase.setLad("0000"); + junkCase.setLatitude("0.0.0.0.0.0"); + junkCase.setLongitude("0.1278"); + junkCase.setLsoa("0000"); + junkCase.setRegion("EN"); + junkCase.setOa("0000"); + junkCase.setMsoa("0000"); + junkCase.setPostcode("CFXX XXX"); + junkCase.setTreatmentCode("BLJF_FEJG"); + junkCase.setUprn("000000"); + junkCase.setTownName("Best Town"); + caseRepository.save(junkCase); + + return junkCase; + } + + public CollectionExercise setupJunkCollex() { + Survey junkSurvey = new Survey(); + junkSurvey.setId(UUID.randomUUID()); + junkSurvey.setName("Junk survey"); + junkSurvey.setSampleSeparator('j'); + surveyRepository.saveAndFlush(junkSurvey); + + CollectionExercise junkCollectionExercise = new CollectionExercise(); + junkCollectionExercise.setId(UUID.randomUUID()); + junkCollectionExercise.setName("Junk collex"); + junkCollectionExercise.setSurvey(junkSurvey); + junkCollectionExercise.setReference("MVP012021"); + junkCollectionExercise.setStartDate(OffsetDateTime.now()); + junkCollectionExercise.setEndDate(OffsetDateTime.now().plusDays(2)); + junkCollectionExercise.setMetadata(null); + collectionExerciseRepository.saveAndFlush(junkCollectionExercise); + + return junkCollectionExercise; + } + + public void junkify(EventHeaderDTO eventHeaderDTO) { + if (eventHeaderDTO.getChannel() == null) { + eventHeaderDTO.setChannel("Junk"); + } + + if (eventHeaderDTO.getSource() == null) { + eventHeaderDTO.setSource("Junk"); + } + + if (eventHeaderDTO.getCorrelationId() == null) { + eventHeaderDTO.setCorrelationId(UUID.randomUUID()); + } + + if (eventHeaderDTO.getMessageId() == null) { + eventHeaderDTO.setMessageId(UUID.randomUUID()); + } + + if (eventHeaderDTO.getDateTime() == null) { + eventHeaderDTO.setDateTime(OffsetDateTime.now()); + } + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/PubsubHelper.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/PubsubHelper.java new file mode 100644 index 0000000..d7c148a --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/PubsubHelper.java @@ -0,0 +1,139 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import static com.google.cloud.spring.pubsub.support.PubSubSubscriptionUtils.toProjectSubscriptionName; +import static com.google.cloud.spring.pubsub.support.PubSubTopicUtils.toProjectTopicName; +import static uk.gov.ons.census.caseapisvc.testutils.TestConstants.OUR_PUBSUB_PROJECT; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.pubsub.v1.Subscriber; +import com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubProperties; +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import uk.gov.ons.census.caseapisvc.utility.ObjectMapperFactory; + +@Component +@ActiveProfiles("test") +@EnableRetry +public class PubsubHelper { + @Qualifier("pubSubTemplateForIntegrationTests") + @Autowired + private PubSubTemplate pubSubTemplate; + + @Autowired private GcpPubSubProperties gcpPubSubProperties; + + @Value("${spring.cloud.gcp.pubsub.project-id}") + private String pubsubProject; + + private static final ObjectMapper objectMapper = ObjectMapperFactory.objectMapper(); + + public QueueSpy pubsubProjectListen(String subscription, Class contentClass) { + String fullyQualifiedSubscription = + toProjectSubscriptionName(subscription, pubsubProject).toString(); + return listen(fullyQualifiedSubscription, contentClass); + } + + public QueueSpy listen(String subscription, Class contentClass) { + BlockingQueue queue = new ArrayBlockingQueue(50); + Subscriber subscriber = + pubSubTemplate.subscribe( + subscription, + message -> { + try { + T messageObject = + objectMapper.readValue( + message.getPubsubMessage().getData().toByteArray(), contentClass); + queue.add(messageObject); + message.ack(); + } catch (IOException e) { + System.out.println("ERROR: Cannot unmarshal bad data on PubSub subscription"); + } finally { + // Always want to ack, to get rid of dodgy messages + message.ack(); + } + }); + + return new QueueSpy(queue, subscriber); + } + + public void sendMessageToPubsubProject(String topicName, Object message) { + String fullyQualifiedTopic = toProjectTopicName(topicName, pubsubProject).toString(); + sendMessage(fullyQualifiedTopic, message); + } + + @Retryable( + retryFor = {java.io.IOException.class}, + maxAttempts = 10, + backoff = @Backoff(delay = 5000), + listeners = {"retryListener"}) + public void sendMessage(String topicName, Object message) { + CompletableFuture future = pubSubTemplate.publish(topicName, message); + + try { + future.get(30, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + public void purgeMessages(String subscription, String topic) { + purgeMessages(subscription, topic, OUR_PUBSUB_PROJECT); + } + + public void purgePubsubProjectMessages(String subscription, String topic) { + purgeMessages(subscription, topic, pubsubProject); + } + + private void purgeMessages(String subscription, String topic, String project) { + RestTemplate restTemplate = new RestTemplate(); + + String subscriptionUrl = + "http://" + + gcpPubSubProperties.getEmulatorHost() + + "/v1/projects/" + + project + + "/subscriptions/" + + subscription; + + try { + // There's no concept of a 'purge' with pubsub. Crudely, we have to delete & recreate + restTemplate.delete(subscriptionUrl); + } catch (HttpClientErrorException exception) { + if (exception.getRawStatusCode() != 404) { + throw exception; + } + } + + try { + restTemplate.put( + subscriptionUrl, new SubscriptionTopic("projects/" + project + "/topics/" + topic)); + } catch (HttpClientErrorException exception) { + if (exception.getRawStatusCode() != 409) { + throw exception; + } + } + } + + @Data + @AllArgsConstructor + private class SubscriptionTopic { + private String topic; + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/QueueSpy.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/QueueSpy.java new file mode 100644 index 0000000..277d2b7 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/QueueSpy.java @@ -0,0 +1,29 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.cloud.pubsub.v1.Subscriber; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public class QueueSpy implements AutoCloseable { + @Getter private BlockingQueue queue; + private Subscriber subscriber; + + @Override + public void close() { + subscriber.stopAsync(); + } + + public T checkExpectedMessageReceived() throws InterruptedException { + return queue.poll(20, TimeUnit.SECONDS); + } + + public void checkMessageIsNotReceived(int timeOut) throws InterruptedException { + T actualMessage = queue.poll(timeOut, TimeUnit.SECONDS); + assertNull(actualMessage, "Message received when not expected"); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConfig.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConfig.java new file mode 100644 index 0000000..a44804e --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConfig.java @@ -0,0 +1,29 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import com.google.cloud.spring.pubsub.core.PubSubTemplate; +import com.google.cloud.spring.pubsub.support.PublisherFactory; +import com.google.cloud.spring.pubsub.support.SubscriberFactory; +import com.google.cloud.spring.pubsub.support.converter.JacksonPubSubMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ActiveProfiles; +import uk.gov.ons.census.caseapisvc.utility.ObjectMapperFactory; + +@Configuration +@ActiveProfiles("test") +public class TestConfig { + @Bean("pubSubTemplateForIntegrationTests") + public PubSubTemplate pubSubTemplate( + PublisherFactory publisherFactory, + SubscriberFactory subscriberFactory, + JacksonPubSubMessageConverter jacksonPubSubMessageConverter) { + PubSubTemplate pubSubTemplate = new PubSubTemplate(publisherFactory, subscriberFactory); + pubSubTemplate.setMessageConverter(jacksonPubSubMessageConverter); + return pubSubTemplate; + } + + @Bean + public JacksonPubSubMessageConverter jacksonPubSubMessageConverterForIntegrationTests() { + return new JacksonPubSubMessageConverter(ObjectMapperFactory.objectMapper()); + } +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConstants.java b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConstants.java new file mode 100644 index 0000000..c2979ac --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/testutils/TestConstants.java @@ -0,0 +1,22 @@ +package uk.gov.ons.census.caseapisvc.testutils; + +import java.util.Map; +import java.util.UUID; + +public class TestConstants { + public static final String OUR_PUBSUB_PROJECT = "our-project"; + public static final String OUTBOUND_UAC_SUBSCRIPTION = "event_uac-update_rh"; + public static final String OUTBOUND_CASE_SUBSCRIPTION = "event_case-update_rh"; + public static final String NEW_CASE_TOPIC = "event_new-case"; + public static final String OUTBOUND_SMS_REQUEST_SUBSCRIPTION = + "rm-internal-sms-request-enriched_notify-service"; + public static final String OUTBOUND_EMAIL_REQUEST_SUBSCRIPTION = + "rm-internal-email-request-enriched_notify-service"; + public static final String PRINT_FULFILMENT_TOPIC = "event_print-fulfilment"; + public static final String SMS_CONFIRMATION_TOPIC = "rm-internal-sms-confirmation"; + public static final String EMAIL_CONFIRMATION_TOPIC = "rm-internal-email-confirmation"; + + public static final UUID TEST_CORRELATION_ID = UUID.randomUUID(); + public static final String TEST_ORIGINATING_USER = "foo@bar.com"; + public static final Map TEST_UAC_METADATA = Map.of("TEST_UAC_METADATA", "TEST"); +} diff --git a/src/test/java/uk/gov/ons/census/caseapisvc/utility/EventHelperTest.java b/src/test/java/uk/gov/ons/census/caseapisvc/utility/EventHelperTest.java new file mode 100644 index 0000000..5ffe020 --- /dev/null +++ b/src/test/java/uk/gov/ons/census/caseapisvc/utility/EventHelperTest.java @@ -0,0 +1,35 @@ +package uk.gov.ons.census.caseapisvc.utility; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.OffsetDateTime; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import uk.gov.ons.census.caseapisvc.model.dto.EventHeaderDTO; + +public class EventHelperTest { + + @Test + public void testCreateEventDTOWithEventType() { + EventHeaderDTO eventHeader = EventHelper.createEventDTO("Test topic"); + + assertThat(eventHeader.getChannel()).isEqualTo("RM"); + assertThat(eventHeader.getSource()).isEqualTo("CASE_API"); + assertThat(eventHeader.getDateTime()).isInstanceOf(OffsetDateTime.class); + assertThat(eventHeader.getMessageId()).isInstanceOf(UUID.class); + assertThat(eventHeader.getCorrelationId()).isInstanceOf(UUID.class); + assertThat(eventHeader.getTopic()).isEqualTo("Test topic"); + } + + @Test + public void testCreateEventDTOWithEventTypeChannelAndSource() { + EventHeaderDTO eventHeader = EventHelper.createEventDTO("Test topic", "CHANNEL", "SOURCE"); + + assertThat(eventHeader.getChannel()).isEqualTo("CHANNEL"); + assertThat(eventHeader.getSource()).isEqualTo("SOURCE"); + assertThat(eventHeader.getDateTime()).isInstanceOf(OffsetDateTime.class); + assertThat(eventHeader.getMessageId()).isInstanceOf(UUID.class); + assertThat(eventHeader.getCorrelationId()).isInstanceOf(UUID.class); + assertThat(eventHeader.getTopic()).isEqualTo("Test topic"); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..00b6cf8 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,13 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:15432/rm?currentSchema=cases + cloud: + gcp: + pubsub: + emulator-host: localhost:18539 + project-id: our-project + + +uacservice: + connection: + port: 18165 diff --git a/src/test/resources/docker-compose.yml b/src/test/resources/docker-compose.yml new file mode 100644 index 0000000..a0b90c9 --- /dev/null +++ b/src/test/resources/docker-compose.yml @@ -0,0 +1,59 @@ +version: '2.1' +services: + postgres-case-it: + container_name: postgres-case-api-it + image: census-rm-dev-common-postgres:latest + command: [ "-c", "shared_buffers=256MB", "-c", "max_connections=500" ] + ports: + - "15432:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d rm -U appuser" ] + interval: 10s + timeout: 5s + retries: 5 + + uac-qid-case-api-it: + container_name: uac-qid-case-api-it + image: census-rm-uac-qid-service:latest + ports: + - "18165:8164" + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-case-api-it:5432/rm?sslmode=disable + - SPRING_DATASOURCE_USERNAME=appuser + - SPRING_DATASOURCE_PASSWORD=postgres + - SPRING_PROFILES_ACTIVE=dev + volumes: + - ./java_healthcheck:/opt/healthcheck/ + healthcheck: + test: [ "CMD", "java", "-jar", "/opt/healthcheck/HealthCheck.jar", "http://localhost:8164/actuator/health" ] + interval: 20s + timeout: 10s + retries: 10 + + pubsub-emulator-case-it: + container_name: pubsub-emulator-case-it + image: gcloud-pubsub-emulator:latest + ports: + - "18538:8538" + + setup-pubsub-emulator-case-it: + container_name: setup-pubsub-emulator-case-api-it + image: gcloud-pubsub-emulator:latest + environment: + - PUBSUB_SETUP_HOST=pubsub-emulator-case-it:8538 + volumes: + - ./setup_pubsub.sh:/setup_pubsub.sh + depends_on: + - pubsub-emulator-case-it + entrypoint: sh -c "/setup_pubsub.sh" + + start_dependencies: + image: dadarek/wait-for-dependencies + depends_on: + uac-qid-case-api-it: + condition: service_healthy + +networks: + default: + external: + name: censusrmdockerdev_default diff --git a/src/test/resources/java_healthcheck/HealthCheck.jar b/src/test/resources/java_healthcheck/HealthCheck.jar new file mode 100644 index 0000000..1778d05 Binary files /dev/null and b/src/test/resources/java_healthcheck/HealthCheck.jar differ diff --git a/src/test/resources/java_healthcheck/HealthCheck.java b/src/test/resources/java_healthcheck/HealthCheck.java new file mode 100644 index 0000000..47480a0 --- /dev/null +++ b/src/test/resources/java_healthcheck/HealthCheck.java @@ -0,0 +1,25 @@ +import java.net.*; + +public class HealthCheck { + // A simple Java class for testing a GET endpoint responds with status code 200 inside Java containers + // Usage: java /HealthCheck.java + + public static void main(String[] args) throws Exception { + if (args.length != 1) { + throw new IllegalArgumentException("Expected exactly one argument, the URL to GET"); + } + + // Prepare the HTTP GET request + URL url = new URL(args[0]); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + + // Make the request + int status = con.getResponseCode(); + + // Exit with a failure code if the GET is not a success + if (status != 200) { + System.exit(1); + } + } +} diff --git a/src/test/resources/java_healthcheck/Makefile b/src/test/resources/java_healthcheck/Makefile new file mode 100644 index 0000000..68029af --- /dev/null +++ b/src/test/resources/java_healthcheck/Makefile @@ -0,0 +1,4 @@ +rebuild-java-healthcheck: + javac HealthCheck.java + jar -cfe HealthCheck.jar HealthCheck HealthCheck.class + rm HealthCheck.class \ No newline at end of file diff --git a/src/test/resources/setup_pubsub.sh b/src/test/resources/setup_pubsub.sh new file mode 100755 index 0000000..dbe27d4 --- /dev/null +++ b/src/test/resources/setup_pubsub.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Wait for pubsub-emulator to come up +bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' '$PUBSUB_SETUP_HOST')" != "200" ]]; do sleep 1; done' + +curl -X PUT http://$PUBSUB_SETUP_HOST/v1/projects/our-project/topics/rm-internal-questionnaire-uac-linked +curl -X PUT http://$PUBSUB_SETUP_HOST/v1/projects/our-project/subscriptions/rm-internal-questionnaire-uac-linked-service -H 'Content-Type: application/json' -d '{"topic": "projects/our-project/topics/rm-internal-questionnaire-uac-linked"}'