From 5adb5b07f0690d1f277fdda02fa7b2d069c0f27c Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Mon, 18 Nov 2024 13:02:53 +0000 Subject: [PATCH 01/24] Updated environment name for dev from "dev" to "development" to match infrastructure. --- .github/workflows/pipeline.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 02c7ac5..2ec6ed7 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -12,11 +12,11 @@ on: type: choice required: true options: - - dev + - development - preprod - staging - production - default: 'dev' + default: 'development' docker_registry: description: Docker registry required: true @@ -59,7 +59,7 @@ jobs: uses: ministryofjustice/hmpps-github-actions/.github/workflows/test_helm_lint.yml@v1 # WORKFLOW VERSION secrets: inherit with: - environment: ${{ inputs.environment || 'dev' }} + environment: ${{ inputs.environment || 'development' }} kotlin_validate: name: Validate the kotlin uses: ministryofjustice/hmpps-github-actions/.github/workflows/kotlin_validate.yml@v1 # WORKFLOW_VERSION @@ -78,18 +78,18 @@ jobs: docker_multiplatform: ${{ inputs.docker_multiplatform || true }} deploy_dev: name: Deploy to dev environment - needs: + needs: - build - helm_lint uses: ministryofjustice/hmpps-github-actions/.github/workflows/deploy_env.yml@v1 # WORKFLOW_VERSION secrets: inherit with: - environment: 'dev' + environment: 'development' app_version: '${{ needs.build.outputs.app_version }}' # deploy_preprod: # name: Deploy to pre-production environment - # needs: + # needs: # - build # - deploy_dev # uses: ministryofjustice/hmpps-github-actions/.github/workflows/deploy_env.yml@v1 # WORKFLOW_VERSION @@ -99,7 +99,7 @@ jobs: # app_version: '${{ needs.build.outputs.app_version }}' # deploy_prod: # name: Deploy to production environment - # needs: + # needs: # - build # - deploy_preprod # uses: ministryofjustice/hmpps-github-actions/.github/workflows/deploy_env.yml@v1 # WORKFLOW_VERSION From b5514f3216f7f0113f8f91e40f2417298fe44d72 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Mon, 18 Nov 2024 13:03:24 +0000 Subject: [PATCH 02/24] Removed rename-project workflow --- .../workflows/rename_template_project_pr.yml | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 .github/workflows/rename_template_project_pr.yml diff --git a/.github/workflows/rename_template_project_pr.yml b/.github/workflows/rename_template_project_pr.yml deleted file mode 100644 index cc85dd2..0000000 --- a/.github/workflows/rename_template_project_pr.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: rename-project-create-pr - -on: - workflow_dispatch: - inputs: - product_id: - description: 'Product ID: provide an ID for the product this app/component belongs too. Refer to the developer portal.' - required: true - slack_releases_channel: - description: 'Slack channel for release notifications.' - required: true - security_alerts_slack_channel_id: - description: 'Slack channel for pipeline security notifications.' - required: true - non_prod_alerts_prometheus_severity_label: - description: 'Non-prod kubernetes alerts. The severity label used by prometheus to route alert notifications to slack. See cloud-platform user guide.' - required: true - default: 'digital-prison-service-dev' - prod_alerts_prometheus_severity_label: - description: 'Production kubernetes alerts. The severity label used by prometheus to route alert notifications to slack. See cloud-platform user guide.' - required: true - default: 'digital-prison-service' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Run rename-project script - run: ./rename-project.bash ${{ github.event.repository.name }} ${{ inputs.slack_releases_channel }} ${{ inputs.security_alerts_slack_channel_id }} ${{ inputs.non_prod_alerts_prometheus_severity_label }} ${{ inputs.prod_alerts_prometheus_severity_label }} ${{ inputs.product_id }} - - - name: Delete this github actions workflow - run: rm .github/workflows/rename_template_project* - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 - with: - commit-message: updating project name after deployment from template repository - title: Update template project name/references - body: Update all references to project name after deploying from template repository - branch: rename_template_project - base: main From c46a6410ea22f57f9e41b13067262bca5ac8f4be Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Mon, 18 Nov 2024 13:03:57 +0000 Subject: [PATCH 03/24] Set security scans to send alerts to the connect DPS dev channel. --- .github/workflows/security_owasp.yml | 2 +- .github/workflows/security_trivy.yml | 2 +- .github/workflows/security_veracode_pipeline_scan.yml | 2 +- .github/workflows/security_veracode_policy_scan.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/security_owasp.yml b/.github/workflows/security_owasp.yml index 6abf635..36497b5 100644 --- a/.github/workflows/security_owasp.yml +++ b/.github/workflows/security_owasp.yml @@ -8,5 +8,5 @@ jobs: name: Kotlin security OWASP dependency check uses: ministryofjustice/hmpps-github-actions/.github/workflows/security_owasp.yml@v0.7 # WORKFLOW_VERSION with: - channel_id: C05J915DX0Q + channel_id: C04JFG3QJE6 secrets: inherit diff --git a/.github/workflows/security_trivy.yml b/.github/workflows/security_trivy.yml index 4e80707..c5e15d9 100644 --- a/.github/workflows/security_trivy.yml +++ b/.github/workflows/security_trivy.yml @@ -8,5 +8,5 @@ jobs: name: Project security trivy dependency check uses: ministryofjustice/hmpps-github-actions/.github/workflows/security_trivy.yml@v0.7 # WORKFLOW_VERSION with: - channel_id: C05J915DX0Q + channel_id: C04JFG3QJE6 secrets: inherit diff --git a/.github/workflows/security_veracode_pipeline_scan.yml b/.github/workflows/security_veracode_pipeline_scan.yml index c825bdd..a928e89 100644 --- a/.github/workflows/security_veracode_pipeline_scan.yml +++ b/.github/workflows/security_veracode_pipeline_scan.yml @@ -8,5 +8,5 @@ jobs: name: Project security veracode pipeline scan uses: ministryofjustice/hmpps-github-actions/.github/workflows/security_veracode_pipeline_scan.yml@v0.7 # WORKFLOW_VERSION with: - channel_id: C05J915DX0Q + channel_id: C04JFG3QJE6 secrets: inherit diff --git a/.github/workflows/security_veracode_policy_scan.yml b/.github/workflows/security_veracode_policy_scan.yml index f574796..708c162 100644 --- a/.github/workflows/security_veracode_policy_scan.yml +++ b/.github/workflows/security_veracode_policy_scan.yml @@ -8,5 +8,5 @@ jobs: name: Project security veracode policy scan uses: ministryofjustice/hmpps-github-actions/.github/workflows/security_veracode_policy_scan.yml@v0.7 # WORKFLOW_VERSION with: - channel_id: C05J915DX0Q + channel_id: C04JFG3QJE6 secrets: inherit From a921d10090b622e2cd746af9f675bb33b040c0d5 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:25:42 +0000 Subject: [PATCH 04/24] CDPS-1054: Added Prison API url and client credentials to helm values. --- helm_deploy/hmpps-person-integration-api/values.yaml | 12 ++++-------- helm_deploy/values-dev.yaml | 4 +--- helm_deploy/values-preprod.yaml | 4 +--- helm_deploy/values-prod.yaml | 4 +--- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/helm_deploy/hmpps-person-integration-api/values.yaml b/helm_deploy/hmpps-person-integration-api/values.yaml index b7586a2..f245ca4 100644 --- a/helm_deploy/hmpps-person-integration-api/values.yaml +++ b/helm_deploy/hmpps-person-integration-api/values.yaml @@ -21,16 +21,10 @@ generic-service: APPLICATIONINSIGHTS_CONNECTION_STRING: "InstrumentationKey=$(APPINSIGHTS_INSTRUMENTATIONKEY)" APPLICATIONINSIGHTS_CONFIGURATION_FILE: applicationinsights.json - # Pre-existing kubernetes secrets to load as environment variables in the deployment. - # namespace_secrets: - # [name of kubernetes secret]: - # [name of environment variable as seen by app]: [key of kubernetes secret to load] - namespace_secrets: hmpps-person-integration-api: - # Example client registration secrets - EXAMPLE_API_CLIENT_ID: "TEMPLATE_KOTLIN_API_CLIENT_ID" - EXAMPLE_API_CLIENT_SECRET: "TEMPLATE_KOTLIN_API_CLIENT_SECRET" + SYSTEM_CLIENT_ID: "SYSTEM_CLIENT_ID" + SYSTEM_CLIENT_SECRET: "SYSTEM_CLIENT_SECRET" application-insights: APPINSIGHTS_INSTRUMENTATIONKEY: "APPINSIGHTS_INSTRUMENTATIONKEY" @@ -38,5 +32,7 @@ generic-service: groups: - internal + modsecurity_enabled: true + generic-prometheus-alerts: targetApplication: hmpps-person-integration-api diff --git a/helm_deploy/values-dev.yaml b/helm_deploy/values-dev.yaml index 05162d9..684d1f1 100644 --- a/helm_deploy/values-dev.yaml +++ b/helm_deploy/values-dev.yaml @@ -10,9 +10,7 @@ generic-service: env: APPLICATIONINSIGHTS_CONFIGURATION_FILE: "applicationinsights.dev.json" HMPPS_AUTH_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth" - # Template kotlin calls out to itself to provide an example of a service call - # TODO: This should be replaced by a call to a different service, or removed - EXAMPLE_API_URL: "https://person-integration-api-dev.hmpps.service.justice.gov.uk" + PRISON_API_BASE_URL: "https://prison-api-dev.prison.service.justice.gov.uk" # CloudPlatform AlertManager receiver to route prometheus alerts to slack # See https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts diff --git a/helm_deploy/values-preprod.yaml b/helm_deploy/values-preprod.yaml index ceae048..d0e6e8a 100644 --- a/helm_deploy/values-preprod.yaml +++ b/helm_deploy/values-preprod.yaml @@ -10,9 +10,7 @@ generic-service: env: APPLICATIONINSIGHTS_CONFIGURATION_FILE: "applicationinsights.dev.json" HMPPS_AUTH_URL: "https://sign-in-preprod.hmpps.service.justice.gov.uk/auth" - # Template kotlin calls out to itself to provide an example of a service call - # TODO: This should be replaced by a call to a different service, or removed - EXAMPLE_API_URL: "https://person-integration-api-preprod.hmpps.service.justice.gov.uk" + PRISON_API_BASE_URL: "https://prison-api-preprod.prison.service.justice.gov.uk" # CloudPlatform AlertManager receiver to route prometheus alerts to slack # See https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts diff --git a/helm_deploy/values-prod.yaml b/helm_deploy/values-prod.yaml index 482bc4f..ea402f8 100644 --- a/helm_deploy/values-prod.yaml +++ b/helm_deploy/values-prod.yaml @@ -7,9 +7,7 @@ generic-service: env: HMPPS_AUTH_URL: "https://sign-in.hmpps.service.justice.gov.uk/auth" - # Template kotlin calls out to itself to provide an example of a service call - # TODO: This should be replaced by a call to a different service, or removed - EXAMPLE_API_URL: "https://person-integration-api.hmpps.service.justice.gov.uk" + PRISON_API_BASE_URL: "https://prison-api.prison.service.justice.gov.uk" # CloudPlatform AlertManager receiver to route prometheus alerts to slack # See https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts From 514a5a1bc39fc176eb3f2766ec95dfa53e91e353 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:26:24 +0000 Subject: [PATCH 05/24] CDPS-1054: Setup docker compose for running locally. --- docker-compose.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3c10306..a8d05be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ services: hmpps-person-integration-api: build: context: . + depends_on: + - hmpps-auth + - prison-api networks: - hmpps container_name: hmpps-person-integration-api @@ -31,5 +34,19 @@ services: - SPRING_PROFILES_ACTIVE=dev - APPLICATION_AUTHENTICATION_UI_ALLOWLIST=0.0.0.0/0 + prison-api: + image: quay.io/hmpps/prison-api:latest + container_name: prison-api + networks: + - hmpps + ports: + - '8082:8080' + healthcheck: + test: [ 'CMD', 'curl', '-f', 'http://localhost:8080/health' ] + environment: + - SERVER_PORT=8080 + - SPRING_PROFILES_ACTIVE=nomis-hsqldb + - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=https://sign-in-dev.hmpps.service.justice.gov.uk/auth/.well-known/jwks.json + networks: hmpps: From 687ecacf39c7a69f0bc43ce9e079101e7ac614ed Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:43:37 +0000 Subject: [PATCH 06/24] CDPS-1054: Added prison API details to application properties. --- src/main/resources/application-dev.yml | 11 ++++------- src/main/resources/application.yml | 18 +++++++++--------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 949a823..e4389f1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,10 +1,7 @@ hmpps-auth: url: "http://localhost:8090/auth" -# example client configuration for calling out to other services -# TODO: Remove / replace this configuration -example-api: - url: "http://localhost:8080" - client: - id: "example-api-client" - secret: "example-api-client-secret" +prison-api: + base_url: "http://localhost:8082" + + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d813a27..7e751ac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ info.app: - name: HMPPS Template Kotlin + name: HMPPS Peron Integration API version: 1.0 spring: @@ -9,11 +9,13 @@ spring: max-in-memory-size: 10MB jackson: - date-format: "yyyy-MM-dd HH:mm:ss" + date-format: "yyyy-MM-dd'T'HH:mm:ssZ" serialization: - WRITE_DATES_AS_TIMESTAMPS: false + write-dates-as-timestamps: false + write-dates-with-context-time-zone: true + write-dates-with-zone-id: false + time-zone: "Europe/London" - # TODO: This security section can be removed if your service doesn't call out to other services security: oauth2: resourceserver: @@ -26,12 +28,10 @@ spring: token-uri: ${hmpps-auth.url}/oauth/token registration: - # example client registration for calling out to other services - # TODO: Remove / replace this registration - example-api: + prison-api: provider: hmpps-auth - client-id: ${example-api.client.id} - client-secret: ${example-api.client.secret} + client-id: ${hmpps-person-integration-api.system.client.id} + client-secret: ${hmpps-person-integration-api.system.client.secret} authorization-grant-type: client_credentials scope: read From f2a3fb7a2a36c215bcf2f689e7be23d5ebe614b2 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:44:14 +0000 Subject: [PATCH 07/24] CDPS-1054: Updated the template references in banner and log config. --- src/main/resources/banner.txt | 26 +++++++++++++------------- src/main/resources/logback-spring.xml | 3 ++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index fc59263..4dc1709 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -1,13 +1,13 @@ -_ _ _ _ ___ ___ ____ -|__| |\/| |__] |__] [__ -| | | | | | ___] - -___ ____ _ _ ___ _ ____ ___ ____ - | |___ |\/| |__] | |__| | |___ - | |___ | | | |___ | | | |___ - -_ _ ____ ___ _ _ _ _ -|_/ | | | | | |\ | -| \_ |__| | |___ | | \| - -TODO: Please change me by generating your own ASCII art and placing in banner.txt + _ _ __ __ ____ ____ ____ ____ _____ ____ ____ ___ _ _ +| | | | \/ | _ \| _ \/ ___| | _ \| ____| _ \/ ___| / _ \| \ | | +| |_| | |\/| | |_) | |_) \___ \ | |_) | _| | |_) \___ \| | | | \| | +| _ | | | | __/| __/ ___) | | __/| |___| _ < ___) | |_| | |\ | +|_| |_|_|_ |_|_|___|_|___|____/ _|_| |_____|_|_\_\____/_\___/|_| \_| + |_ _| \ | |_ _| ____/ ___| _ \ / \|_ _|_ _/ _ \| \ | | + | || \| | | | | _|| | _| |_) | / _ \ | | | | | | | \| | + | || |\ | | | | |__| |_| | _ < / ___ \| | | | |_| | |\ | + |___|_| \_| |_| |_____\____|_|_\_\/_/ \_\_| |___\___/|_| \_| + / \ | _ \_ _| + / _ \ | |_) | | + / ___ \| __/| | + /_/ \_\_| |___| diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index b33dba1..f8ae5ae 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -12,7 +12,8 @@ - + From 94be2748e3165040c4b18349425ac87a4e733a02 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:53:14 +0000 Subject: [PATCH 08/24] CDP-1054: Template Iteration 1 API added to Core Person Record and Protected Characteristics domains. --- .../HmppsPersonIntegrationApi.kt | 6 +- .../personintegrationapi/common/Constants.kt | 6 + .../common/annotation/ValidPrisonerNumber.kt | 23 ++ .../common/client/PrisonApiClient.kt | 17 ++ .../common/client/dto/UpdateBirthPlace.kt | 9 + ...ppsPersonIntegrationApiExceptionHandler.kt | 134 ++++++++++++ .../common/config/OpenApiConfiguration.kt | 78 +++++++ ...h2ClientCredentialGrantRequestConverter.kt | 23 ++ .../common/config/WebClientConfiguration.kt | 103 +++++++++ .../common/dto/ReferenceDataCodeDto.kt | 33 +++ .../common/dto/ReferenceDataDomainDto.kt | 30 +++ ...ppsPersonIntegrationApiExceptionHandler.kt | 65 ------ .../config/OpenApiConfiguration.kt | 58 ----- .../config/WebClientConfiguration.kt | 32 --- .../CorePersonRecordRoleConstants.kt | 6 + .../CorePersonRecordV1UpdateRequestDto.kt | 23 ++ .../dto/response/FieldUpdateResponseDto.kt | 28 +++ .../enumeration/CorePersonRecordField.kt | 6 + .../UnknownCorePersonFieldException.kt | 3 + .../resource/CorePersonRecordV1Resource.kt | 198 ++++++++++++++++++ .../service/CorePersonRecordService.kt | 20 ++ .../health/HealthPingCheck.kt | 10 +- ...onProtectedCharacteristicsRoleConstants.kt | 6 + .../dto/common/ReligionDto.kt | 61 ++++++ ...tectedCharacteristicsV1UpdateRequestDto.kt | 23 ++ .../dto/request/ReligionV1RequestDto.kt | 39 ++++ .../PersonReligionInformationV1ResponseDto.kt | 13 ++ .../ProtectedCharacteristicsField.kt | 5 + ...ersonProtectedCharacteristicsV1Resource.kt | 131 ++++++++++++ .../resource/ExampleResource.kt | 77 ------- .../service/ExampleApiService.kt | 37 ---- 31 files changed, 1025 insertions(+), 278 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/PrisonApiClient.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/dto/UpdateBirthPlace.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/HmppsPersonIntegrationApiExceptionHandler.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataCodeDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/HmppsPersonIntegrationApiExceptionHandler.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/OpenApiConfiguration.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/WebClientConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/response/FieldUpdateResponseDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/enumeration/CorePersonRecordField.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/exception/UnknownCorePersonFieldException.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/common/ReligionDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/PersonProtectedCharacteristicsV1UpdateRequestDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/ReligionV1RequestDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/response/PersonReligionInformationV1ResponseDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/enumeration/ProtectedCharacteristicsField.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/resource/ExampleResource.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/service/ExampleApiService.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/HmppsPersonIntegrationApi.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/HmppsPersonIntegrationApi.kt index 488b460..b8645ae 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/HmppsPersonIntegrationApi.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/HmppsPersonIntegrationApi.kt @@ -1,11 +1,11 @@ -package uk.gov.justice.digital.hmpps.templatepackagename +package uk.gov.justice.digital.hmpps.personintegrationapi import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class HmppsTemplateKotlin +class HmppsPersonIntegrationApi fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt new file mode 100644 index 0000000..e63c844 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common + +object Constants { + const val PRISONER_NUMBER_REGEX = "^[A-Za-z0-9]{1,10}\$" + const val PRISONER_NUMBER_VALIDATION_MESSAGE = "The prisoner number must be a alphanumeric string upto 10 characters in length." +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt new file mode 100644 index 0000000..c0e9919 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt @@ -0,0 +1,23 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.annotation + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Pattern +import uk.gov.justice.digital.hmpps.personintegrationapi.common.Constants + +@Schema( + description = Constants.PRISONER_NUMBER_VALIDATION_MESSAGE, + example = "A12345", + pattern = Constants.PRISONER_NUMBER_REGEX, +) +@Pattern( + regexp = Constants.PRISONER_NUMBER_REGEX, + message = Constants.PRISONER_NUMBER_VALIDATION_MESSAGE, +) +@Target( + AnnotationTarget.FIELD, + AnnotationTarget.VALUE_PARAMETER, +) +@kotlin.annotation.Retention( + AnnotationRetention.RUNTIME, +) +annotation class ValidPrisonerNumber diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/PrisonApiClient.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/PrisonApiClient.kt new file mode 100644 index 0000000..e877af9 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/PrisonApiClient.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.client + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.service.annotation.HttpExchange +import org.springframework.web.service.annotation.PutExchange +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.dto.UpdateBirthPlace + +@HttpExchange("/api/offenders") +interface PrisonApiClient { + @PutExchange("/{offenderNo}/birth-place") + fun updateBirthPlaceForWorkingName( + @PathVariable offenderNo: String, + @RequestBody updateBirthPlace: UpdateBirthPlace, + ): ResponseEntity +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/dto/UpdateBirthPlace.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/dto/UpdateBirthPlace.kt new file mode 100644 index 0000000..fefc7b0 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/client/dto/UpdateBirthPlace.kt @@ -0,0 +1,9 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.client.dto + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Update to prisoner birth place (city or town of birth)") +data class UpdateBirthPlace( + @Schema(description = "Birth place (city or town of birth)", example = "SHEFFIELD") + val birthPlace: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/HmppsPersonIntegrationApiExceptionHandler.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/HmppsPersonIntegrationApiExceptionHandler.kt new file mode 100644 index 0000000..277538d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/HmppsPersonIntegrationApiExceptionHandler.kt @@ -0,0 +1,134 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.config + +import jakarta.validation.ValidationException +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.FORBIDDEN +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.security.access.AccessDeniedException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.HandlerMethodValidationException +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.servlet.resource.NoResourceFoundException +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestControllerAdvice +class HmppsPersonIntegrationApiExceptionHandler { + @ExceptionHandler(MethodArgumentTypeMismatchException::class) + fun handleMethodArgumentTypeMismatchException(e: MethodArgumentTypeMismatchException): ResponseEntity { + val type = e.requiredType + val message = if (type.isEnum) { + "Parameter ${e.name} must be one of the following ${ + StringUtils.join( + type.enumConstants, + ", ", + ) + }" + } else { + "Parameter ${e.name} must be of type ${type.typeName}" + } + + return ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: $message", + developerMessage = e.message, + ), + ).also { log.info("Method argument type mismatch exception: {}", e.message) } + } + + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleValidationException(ex: HttpMessageNotReadableException): ResponseEntity = + ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Unable to read the request", + developerMessage = ex.message, + ), + ).also { log.info(ex.message) } + + @ExceptionHandler(HandlerMethodValidationException::class) + fun handleHandlerMethodValidationException(e: HandlerMethodValidationException): ResponseEntity = + e.allErrors.map { it.toString() }.distinct().sorted().joinToString("\n") + .let { validationErrors -> + ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure(s): ${ + e.allErrors.map { it.defaultMessage }.distinct().sorted().joinToString("\n") + }", + developerMessage = "${e.message} $validationErrors", + ), + ).also { log.info("Validation exception: $validationErrors\n {}", e.message) } + } + + @ExceptionHandler(ValidationException::class) + fun handleValidationException(e: ValidationException): ResponseEntity = + ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Validation exception: {}", e.message) } + + @ExceptionHandler(NoResourceFoundException::class) + fun handleNoResourceFoundException(e: NoResourceFoundException): ResponseEntity = + ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = "No resource found failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("No resource found exception: {}", e.message) } + + @ExceptionHandler(AccessDeniedException::class) + fun handleAccessDeniedException(e: AccessDeniedException): ResponseEntity = + ResponseEntity + .status(FORBIDDEN) + .body( + ErrorResponse( + status = FORBIDDEN, + userMessage = "Forbidden: ${e.message}", + developerMessage = e.message, + ), + ).also { log.debug("Forbidden (403) returned: {}", e.message) } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity = ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ErrorResponse( + status = INTERNAL_SERVER_ERROR, + userMessage = "Unexpected error: ${e.message}", + developerMessage = e.message, + ), + ).also { log.error("Unexpected exception", e) } + + @ExceptionHandler(WebClientResponseException::class) + fun handleException(e: WebClientResponseException): ResponseEntity = ResponseEntity + .status(e.statusCode) + .body( + e.getResponseBodyAs(ErrorResponse::class.java), + ).also { log.debug("Exception during call to client", e) } + + private companion object { + private val log = LoggerFactory.getLogger(this::class.java) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt new file mode 100644 index 0000000..a9c9e96 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt @@ -0,0 +1,78 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springdoc.core.models.GroupedOpenApi +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.info.BuildProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class OpenApiConfiguration( + buildProperties: BuildProperties, + @Value("\${hmpps-auth.url}") val oauthUrl: String, +) { + private val version: String = buildProperties.version + + @Bean + fun customOpenAPI(): OpenAPI = OpenAPI() + .servers( + listOf( + Server().url("http://localhost:8080").description("Local"), + Server().url("https://person-integration-api-dev.hmpps.service.justice.gov.uk").description("Development"), + Server().url("https://person-integration-api-preprod.hmpps.service.justice.gov.uk").description("Pre-Production"), + Server().url("https://person-integration-api.hmpps.service.justice.gov.uk").description("Production"), + ), + ) + .info( + Info().title("Core Person Proxy Prototype").version(version) + .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), + ) + .components( + Components().addSecuritySchemes( + "bearer-jwt", + SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER) + .name("Authorization"), + ) + .addSecuritySchemes( + "hmpps-auth", + SecurityScheme() + .flows(getFlows()) + .type(SecurityScheme.Type.OAUTH2), + ), + ) + .addSecurityItem(SecurityRequirement().addList("bearer-jwt", listOf("read", "write"))) + .addSecurityItem(SecurityRequirement().addList("hmpps-auth")) + + fun getFlows(): OAuthFlows { + val flows = OAuthFlows() + val clientCredflow = OAuthFlow() + clientCredflow.tokenUrl = "$oauthUrl/oauth/token" + val scopes = Scopes() + .addString("read", "Allows read of data") + .addString("write", "Allows write of data") + clientCredflow.scopes = scopes + return flows.clientCredentials(clientCredflow) + } + + @Bean + fun v1Api(): GroupedOpenApi { + return GroupedOpenApi.builder() + .group("v1") + .pathsToMatch("/v1/**") + .build() + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt new file mode 100644 index 0000000..edf3680 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt @@ -0,0 +1,23 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.config + +import org.springframework.http.HttpMethod +import org.springframework.http.RequestEntity +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter +import org.springframework.util.MultiValueMap +import java.util.* + +@SuppressWarnings("unchecked") +class UserEnhancedOAuth2ClientCredentialGrantRequestConverter : OAuth2ClientCredentialsGrantRequestEntityConverter() { + + fun enhanceWithUsername(grantRequest: OAuth2ClientCredentialsGrantRequest?, username: String?): RequestEntity { + val request = super.convert(grantRequest) + val headers = request.headers + val body = Objects.requireNonNull(request).body + val formParameters = body as MultiValueMap + if (username != null) { + formParameters.add("username", username) + } + return RequestEntity(formParameters, headers, HttpMethod.POST, request.url) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt new file mode 100644 index 0000000..4cedeb1 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt @@ -0,0 +1,103 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.DependsOn +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction +import org.springframework.web.context.annotation.RequestScope +import org.springframework.web.reactive.function.client.ExchangeFilterFunction +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.support.WebClientAdapter +import org.springframework.web.service.invoker.HttpServiceProxyFactory +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.PrisonApiClient +import uk.gov.justice.digital.hmpps.personintegrationapi.config.UserEnhancedOAuth2ClientCredentialGrantRequestConverter +import uk.gov.justice.hmpps.kotlin.auth.healthWebClient +import java.time.Duration + +@Configuration +class WebClientConfiguration( + @Value("\${hmpps-auth.url}") private val authBaseUri: String, + @Value("\${prison-api.base_url}") private val prisonApiBaseUri: String, + @Value("\${api.timeout:20s}") val healthTimeout: Duration, + @Value("\${api.timeout:90s}") val timeout: Duration, +) { + @Bean + fun authHealthWebClient(builder: WebClient.Builder): WebClient = builder.healthWebClient(authBaseUri, healthTimeout) + + @Bean + fun prisonApiHealthWebClient(builder: WebClient.Builder): WebClient = + builder.healthWebClient(prisonApiBaseUri, healthTimeout) + + @Bean + @RequestScope + fun prisonApiWebClient( + clientRegistrationRepository: ClientRegistrationRepository, + builder: WebClient.Builder, + ): WebClient { + return getOAuthWebClient( + authorizedClientManagerUserEnhanced(clientRegistrationRepository), + builder, + prisonApiBaseUri, + "prison-api", + null, + ) + } + + @Bean + @DependsOn("prisonApiWebClient") + fun prisonApiClient(prisonApiWebClient: WebClient): PrisonApiClient { + val factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(prisonApiWebClient)).build() + val client = factory.createClient(PrisonApiClient::class.java) + return client + } + + private fun authorizedClientManagerUserEnhanced(clients: ClientRegistrationRepository?): OAuth2AuthorizedClientManager { + val service: OAuth2AuthorizedClientService = InMemoryOAuth2AuthorizedClientService(clients) + val manager = AuthorizedClientServiceOAuth2AuthorizedClientManager(clients, service) + + val defaultClientCredentialsTokenResponseClient = DefaultClientCredentialsTokenResponseClient() + val authentication = SecurityContextHolder.getContext().authentication + defaultClientCredentialsTokenResponseClient.setRequestEntityConverter { grantRequest: OAuth2ClientCredentialsGrantRequest -> + val converter = UserEnhancedOAuth2ClientCredentialGrantRequestConverter() + converter.enhanceWithUsername(grantRequest, authentication.name) + } + + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials { clientCredentialsGrantBuilder: OAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder -> + clientCredentialsGrantBuilder.accessTokenResponseClient(defaultClientCredentialsTokenResponseClient) + } + .build() + + manager.setAuthorizedClientProvider(authorizedClientProvider) + return manager + } + + private fun getOAuthWebClient( + authorizedClientManager: OAuth2AuthorizedClientManager, + builder: WebClient.Builder, + rootUri: String, + registrationId: String, + filterFunctions: List?, + ): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId(registrationId) + + val standardBuild = builder.baseUrl(rootUri).apply(oauth2Client.oauth2Configuration()) + + if (filterFunctions.isNullOrEmpty()) { + return standardBuild.build() + } + + return standardBuild.filters { it.addAll(0, filterFunctions.toList()) }.build() + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataCodeDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataCodeDto.kt new file mode 100644 index 0000000..1c5c24d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataCodeDto.kt @@ -0,0 +1,33 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Reference Data Code DTO") +@JsonInclude(NON_NULL) +data class ReferenceDataCodeDto( + @Schema(description = "Id", example = "COUNTRY_GBR") + val id: String, + + @Schema(description = "Short code for reference data code", example = "GBR") + val code: String, + + @Schema(description = "Description of the reference data code", example = "United Kingdom") + val description: String, + + @Schema( + description = "The sequence number of the reference data code. " + + "Used for ordering reference data correctly in lists and dropdowns. " + + "0 is default order by description.", + example = "3", + ) + val listSequence: Int, + + @Schema( + description = "Indicates that the reference data code is active and can be used. " + + "Inactive reference data codes are not returned by default in the API", + example = "true", + ) + val isActive: Boolean, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt new file mode 100644 index 0000000..ebf7547 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt @@ -0,0 +1,30 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Reference Data Domain DTO") +@JsonInclude(NON_NULL) +data class ReferenceDataDomainDto( + @Schema(description = "Short code for the reference data domain", example = "COUNTRY") + val code: String, + + @Schema(description = "Description of the reference data domain", example = "Countries reference data") + val description: String, + + @Schema( + description = "The sequence number of the reference data domain. " + + "Used for ordering domains correctly in lists. " + + "0 is default order by description.", + example = "3", + ) + val listSequence: Int, + + @Schema( + description = "Indicates that the reference data domain is active and can be used. " + + "Inactive reference data domains are not returned by default in the API", + example = "true", + ) + val isActive: Boolean, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/HmppsPersonIntegrationApiExceptionHandler.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/HmppsPersonIntegrationApiExceptionHandler.kt deleted file mode 100644 index af53e32..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/HmppsPersonIntegrationApiExceptionHandler.kt +++ /dev/null @@ -1,65 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.config - -import jakarta.validation.ValidationException -import org.slf4j.LoggerFactory -import org.springframework.http.HttpStatus.BAD_REQUEST -import org.springframework.http.HttpStatus.FORBIDDEN -import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR -import org.springframework.http.HttpStatus.NOT_FOUND -import org.springframework.http.ResponseEntity -import org.springframework.security.access.AccessDeniedException -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.RestControllerAdvice -import org.springframework.web.servlet.resource.NoResourceFoundException -import uk.gov.justice.hmpps.kotlin.common.ErrorResponse - -@RestControllerAdvice -class HmppsPersonIntegrationApiExceptionHandler { - @ExceptionHandler(ValidationException::class) - fun handleValidationException(e: ValidationException): ResponseEntity = ResponseEntity - .status(BAD_REQUEST) - .body( - ErrorResponse( - status = BAD_REQUEST, - userMessage = "Validation failure: ${e.message}", - developerMessage = e.message, - ), - ).also { log.info("Validation exception: {}", e.message) } - - @ExceptionHandler(NoResourceFoundException::class) - fun handleNoResourceFoundException(e: NoResourceFoundException): ResponseEntity = ResponseEntity - .status(NOT_FOUND) - .body( - ErrorResponse( - status = NOT_FOUND, - userMessage = "No resource found failure: ${e.message}", - developerMessage = e.message, - ), - ).also { log.info("No resource found exception: {}", e.message) } - - @ExceptionHandler(AccessDeniedException::class) - fun handleAccessDeniedException(e: AccessDeniedException): ResponseEntity = ResponseEntity - .status(FORBIDDEN) - .body( - ErrorResponse( - status = FORBIDDEN, - userMessage = "Forbidden: ${e.message}", - developerMessage = e.message, - ), - ).also { log.debug("Forbidden (403) returned: {}", e.message) } - - @ExceptionHandler(Exception::class) - fun handleException(e: Exception): ResponseEntity = ResponseEntity - .status(INTERNAL_SERVER_ERROR) - .body( - ErrorResponse( - status = INTERNAL_SERVER_ERROR, - userMessage = "Unexpected error: ${e.message}", - developerMessage = e.message, - ), - ).also { log.error("Unexpected exception", e) } - - private companion object { - private val log = LoggerFactory.getLogger(this::class.java) - } -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/OpenApiConfiguration.kt deleted file mode 100644 index 5b5e071..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/OpenApiConfiguration.kt +++ /dev/null @@ -1,58 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.config - -import io.swagger.v3.oas.models.Components -import io.swagger.v3.oas.models.OpenAPI -import io.swagger.v3.oas.models.info.Contact -import io.swagger.v3.oas.models.info.Info -import io.swagger.v3.oas.models.security.SecurityRequirement -import io.swagger.v3.oas.models.security.SecurityScheme -import io.swagger.v3.oas.models.servers.Server -import io.swagger.v3.oas.models.tags.Tag -import org.springframework.boot.info.BuildProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Configuration -class OpenApiConfiguration(buildProperties: BuildProperties) { - private val version: String = buildProperties.version - - @Bean - fun customOpenAPI(): OpenAPI = OpenAPI() - .servers( - listOf( - Server().url("https://person-integration-api-dev.hmpps.service.justice.gov.uk").description("Development"), - Server().url("https://person-integration-api-preprod.hmpps.service.justice.gov.uk").description("Pre-Production"), - Server().url("https://person-integration-api.hmpps.service.justice.gov.uk").description("Production"), - Server().url("http://localhost:8080").description("Local"), - ), - ) - .tags( - listOf( - // TODO: Remove the Popular and Examples tag and start adding your own tags to group your resources - Tag().name("Popular") - .description("The most popular endpoints. Look here first when deciding which endpoint to use."), - Tag().name("Examples").description("Endpoints for searching for a prisoner within a prison"), - ), - ) - .info( - Info().title("HMPPS Template Kotlin").version(version) - .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), - ) - // TODO: Remove the default security schema and start adding your own schemas and roles to describe your - // service authorisation requirements - .components( - Components().addSecuritySchemes( - "person-integration-api-ui-role", - SecurityScheme().addBearerJwtRequirement("ROLE_TEMPLATE_KOTLIN__UI"), - ), - ) - .addSecurityItem(SecurityRequirement().addList("person-integration-api-ui-role", listOf("read"))) -} - -private fun SecurityScheme.addBearerJwtRequirement(role: String): SecurityScheme = - type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .`in`(SecurityScheme.In.HEADER) - .name("Authorization") - .description("A HMPPS Auth access token with the `$role` role.") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/WebClientConfiguration.kt deleted file mode 100644 index dac109b..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/config/WebClientConfiguration.kt +++ /dev/null @@ -1,32 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.config - -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager -import org.springframework.web.reactive.function.client.WebClient -import uk.gov.justice.hmpps.kotlin.auth.authorisedWebClient -import uk.gov.justice.hmpps.kotlin.auth.healthWebClient -import java.time.Duration - -@Configuration -class WebClientConfiguration( - @Value("\${example-api.url}") val exampleApiBaseUri: String, - @Value("\${hmpps-auth.url}") val hmppsAuthBaseUri: String, - @Value("\${api.health-timeout:2s}") val healthTimeout: Duration, - @Value("\${api.timeout:20s}") val timeout: Duration, -) { - // HMPPS Auth health ping is required if your service calls HMPPS Auth to get a token to call other services - // TODO: Remove the health ping if no call outs to other services are made - @Bean - fun hmppsAuthHealthWebClient(builder: WebClient.Builder): WebClient = builder.healthWebClient(hmppsAuthBaseUri, healthTimeout) - - // TODO: This is an example health bean for checking other services and should be removed / replaced - @Bean - fun exampleApiHealthWebClient(builder: WebClient.Builder): WebClient = builder.healthWebClient(exampleApiBaseUri, healthTimeout) - - // TODO: This is an example bean for calling other services and should be removed / replaced - @Bean - fun exampleApiWebClient(authorizedClientManager: OAuth2AuthorizedClientManager, builder: WebClient.Builder): WebClient = - builder.authorisedWebClient(authorizedClientManager, registrationId = "example-api", url = exampleApiBaseUri, timeout) -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt new file mode 100644 index 0000000..11c5a7a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord + +object CorePersonRecordRoleConstants { + const val CORE_PERSON_RECORD_READ_ROLE = "ROLE_CORE_PERSON_API__CORE_PERSON_DATA__RO" + const val CORE_PERSON_RECORD_WRITE_ROLE = "ROLE_CORE_PERSON_API__CORE_PERSON_DATA__RW" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt new file mode 100644 index 0000000..bea79f4 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt @@ -0,0 +1,23 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.v1.request + +import io.swagger.v3.oas.annotations.media.Schema +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration.CorePersonRecordField + +@Schema(description = "Core Person Record V1 update request") +data class CorePersonRecordV1UpdateRequestDto( + @Schema( + description = "The field to be updated", + example = "BIRTHPLACE", + required = true, + nullable = false, + ) + val fieldName: CorePersonRecordField, + + @Schema( + description = "The field to be updated", + example = "London", + required = true, + nullable = false, + ) + val fieldValue: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/response/FieldUpdateResponseDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/response/FieldUpdateResponseDto.kt new file mode 100644 index 0000000..6c6072e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/response/FieldUpdateResponseDto.kt @@ -0,0 +1,28 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Response object for field update requests") +data class FieldUpdateResponseDto( + @Schema(description = "Response status for the request", example = "OK") + val status: String, + + @Schema( + description = "Optional response message", + example = "BIRTHPLACE for prisoner A12345 successfully updated.", + ) + val message: String, + + @Schema( + description = "The field that has been updated", + example = "BIRTHPLACE", + ) + val fieldName: String, + + @Schema( + description = "The updated field value", + example = "London", + nullable = false, + ) + val fieldValue: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/enumeration/CorePersonRecordField.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/enumeration/CorePersonRecordField.kt new file mode 100644 index 0000000..ddba056 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/enumeration/CorePersonRecordField.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration + +enum class CorePersonRecordField { + COUNTRY_OF_BIRTH, + BIRTHPLACE, +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/exception/UnknownCorePersonFieldException.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/exception/UnknownCorePersonFieldException.kt new file mode 100644 index 0000000..01b46c5 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/exception/UnknownCorePersonFieldException.kt @@ -0,0 +1,3 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.exception + +class UnknownCorePersonFieldException(message: String) : Exception(message) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt new file mode 100644 index 0000000..130a8f8 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -0,0 +1,198 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.resource + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.core.io.InputStreamResource +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.ResponseEntity.ok +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile +import uk.gov.justice.digital.hmpps.personintegrationapi.common.annotation.ValidPrisonerNumber +import uk.gov.justice.digital.hmpps.personintegrationapi.common.dto.ReferenceDataCodeDto +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.CorePersonRecordRoleConstants +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.response.FieldUpdateResponseDto +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.v1.request.CorePersonRecordV1UpdateRequestDto +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.service.CorePersonRecordService +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestController +@Tag( + name = "Core Person Record V1", + description = "Core information for a HMPPS person.", +) +@RequestMapping(value = ["v1/core-person-record"]) +class CorePersonRecordV1Resource( + private val corePersonRecordService: CorePersonRecordService, +) { + + @PatchMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + @ResponseStatus(HttpStatus.OK) + @Operation( + summary = "Performs partial updates on the core person record by prisoner number", + description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}`", + responses = [ + ApiResponse( + responseCode = "204", + description = "The core person record data has been patched successfully.", + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized to access this endpoint", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "403", + description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "404", + description = "Data not found", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ], + ) + @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}')") + fun patchByPrisonerNumber( + @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, + @RequestBody(required = true) @Valid corePersonRecordUpdateRequest: CorePersonRecordV1UpdateRequestDto, + ) { + corePersonRecordService.updateCorePersonRecordField( + prisonerNumber, + corePersonRecordUpdateRequest.fieldName, + corePersonRecordUpdateRequest.fieldValue, + ) + } + + @PutMapping( + "/profile-image", + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + produces = [ + MediaType.APPLICATION_OCTET_STREAM_VALUE, + MediaType.IMAGE_PNG_VALUE, + MediaType.IMAGE_JPEG_VALUE, + ], + ) + @ResponseStatus(HttpStatus.OK) + @Operation( + summary = "Add or updates the profile image on the core person record by prisoner number", + description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}`", + responses = [ + ApiResponse( + responseCode = "200", + description = "The image file has been uploaded successfully.", + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized to access this endpoint.", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "403", + description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}.", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "404", + description = "Data not found.", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ], + ) + @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}')") + fun putProfileImageByPrisonerNumber( + @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, + @RequestPart(name = "Image file", required = true) profileImage: MultipartFile, + ): ResponseEntity { + val inputStreamResource = InputStreamResource(profileImage.inputStream) + return ok().contentType( + MediaType.parseMediaType( + profileImage.contentType ?: MediaType.APPLICATION_OCTET_STREAM_VALUE, + ), + ).contentLength(profileImage.size).header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"${profileImage.originalFilename}\"", + ).body(inputStreamResource) + } + + @GetMapping("reference-data/domain/{domain}/codes") + @ResponseStatus(HttpStatus.OK) + @Operation( + summary = "Get all reference data codes for the given domain", + description = "Returns the list of reference data codes within the given domain. " + + "This endpoint only returns active reference data codes. " + + "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}`", + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data codes found", + content = [Content(array = ArraySchema(schema = Schema(implementation = ReferenceDataCodeDto::class)))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Not found, the reference data domain was not found", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}')") + fun getReferenceDataCodesByDomain( + @PathVariable @Schema( + description = "The reference data domain", + example = "COUNTRY", + ) domain: String, + ): Collection = listOf() +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordService.kt new file mode 100644 index 0000000..576e6bb --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordService.kt @@ -0,0 +1,20 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.service + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.PrisonApiClient +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.dto.UpdateBirthPlace +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration.CorePersonRecordField +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.exception.UnknownCorePersonFieldException + +@Service +class CorePersonRecordService( + private val prisonApiClient: PrisonApiClient, +) { + + fun updateCorePersonRecordField(prisonerNumber: String, field: CorePersonRecordField, value: String) { + when (field) { + CorePersonRecordField.BIRTHPLACE -> prisonApiClient.updateBirthPlaceForWorkingName(prisonerNumber, UpdateBirthPlace(value)) + else -> throw UnknownCorePersonFieldException("Field '$field' cannot be updated.") + } + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/health/HealthPingCheck.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/health/HealthPingCheck.kt index 9f9f1ed..c1c7eda 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/health/HealthPingCheck.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/health/HealthPingCheck.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.health +package uk.gov.justice.digital.hmpps.personintegrationapi.health import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component @@ -6,10 +6,8 @@ import org.springframework.web.reactive.function.client.WebClient import uk.gov.justice.hmpps.kotlin.health.HealthPingCheck // HMPPS Auth health ping is required if your service calls HMPPS Auth to get a token to call other services -// TODO: Remove the health ping if no call outs to other services are made @Component("hmppsAuth") -class HmppsAuthHealthPing(@Qualifier("hmppsAuthHealthWebClient") webClient: WebClient) : HealthPingCheck(webClient) +class HmppsAuthHealthPing(@Qualifier("authHealthWebClient") webClient: WebClient) : HealthPingCheck(webClient) -// TODO: Example ping health check calling out to other services -@Component("exampleApi") -class ExampleApiHealthPing(@Qualifier("exampleApiHealthWebClient") webClient: WebClient) : HealthPingCheck(webClient) +@Component("prisonApi") +class ExampleApiHealthPing(@Qualifier("prisonApiHealthWebClient") webClient: WebClient) : HealthPingCheck(webClient) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt new file mode 100644 index 0000000..7a7a997 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics + +object PersonProtectedCharacteristicsRoleConstants { + const val PROTECTED_CHARACTERISTICS_READ_ROLE = "ROLE_CORE_PERSON_API__PROTECTED_CHARACTERISTICS_DATA__RO" + const val PROTECTED_CHARACTERISTICS_WRITE_ROLE = "ROLE_CORE_PERSON_API__PROTECTED_CHARACTERISTICS_DATA__RW" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/common/ReligionDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/common/ReligionDto.kt new file mode 100644 index 0000000..0fd2f13 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/common/ReligionDto.kt @@ -0,0 +1,61 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.common + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +@Schema(description = "Religion information") +data class ReligionDto( + + @Schema( + description = "The religion name", + example = "No Religion", + required = true, + nullable = false, + ) + val religion: String = "No Religion", + + @Schema(description = "The religion code", example = "NIL", required = true, nullable = false) + val religionCode: String = "NIL", + + @Schema( + description = "Comments on reason for adding religion", + example = "Religious belief verified", + required = false, + nullable = true, + ) + val changeReason: String? = null, + + @Schema( + description = "The date the religious belief is valid from", + example = "01/01/2024", + required = true, + nullable = false, + ) + val effectiveFromDate: LocalDate = LocalDate.now(), + + @Schema( + description = "The date the religious belief is valid until", + example = "01/01/2024", + required = false, + nullable = true, + ) + val effectiveToDate: LocalDate? = null, + + @Schema(description = "First name of staff member that added belief") + val addedByFirstName: String = "TEST", + + @Schema(description = "Last name of staff member that added belief") + val addedByLastName: String = "USER", + + @Schema(description = "First name of staff member that updated belief") + val updatedByFirstName: String? = null, + + @Schema(description = "Last name of staff member that updated belief") + val updatedByLastName: String? = null, + + @Schema(description = "Date belief was updated") + val updatedDate: LocalDate? = null, + + @Schema(description = "Boolean flag indicating if the religious belief has been verified") + val verified: Boolean = false, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/PersonProtectedCharacteristicsV1UpdateRequestDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/PersonProtectedCharacteristicsV1UpdateRequestDto.kt new file mode 100644 index 0000000..b6ad964 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/PersonProtectedCharacteristicsV1UpdateRequestDto.kt @@ -0,0 +1,23 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.request + +import io.swagger.v3.oas.annotations.media.Schema +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.enumeration.ProtectedCharacteristicsField + +@Schema(description = "Person Protected Characteristics V1 update request") +data class PersonProtectedCharacteristicsV1UpdateRequestDto( + @Schema( + description = "The field to be updated", + example = "RELIGION", + required = true, + nullable = false, + ) + val fieldName: ProtectedCharacteristicsField, + + @Schema( + description = "The field to be updated", + example = "CHRISTIAN", + required = true, + nullable = false, + ) + val fieldValue: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/ReligionV1RequestDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/ReligionV1RequestDto.kt new file mode 100644 index 0000000..47c47b6 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/request/ReligionV1RequestDto.kt @@ -0,0 +1,39 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +@Schema(description = "Religion V1 create/update request") +data class ReligionV1RequestDto( + @Schema( + description = "The religion code", + example = "AGNO", + required = true, + nullable = false, + ) + val religionCode: String, + + @Schema( + description = "Reason for the religion change", + example = "Religion has changed", + required = false, + nullable = false, + ) + val reasonForChange: String, + + @Schema( + description = "The date the religious belief is valid from", + required = true, + nullable = false, + ) + val effectiveFromDate: LocalDate, + + @Schema( + description = "Boolean indicating if the religious belief has been verified.", + example = "false", + defaultValue = "false", + required = true, + nullable = false, + ) + val isVerified: Boolean, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/response/PersonReligionInformationV1ResponseDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/response/PersonReligionInformationV1ResponseDto.kt new file mode 100644 index 0000000..223a962 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/dto/response/PersonReligionInformationV1ResponseDto.kt @@ -0,0 +1,13 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.response + +import io.swagger.v3.oas.annotations.media.Schema +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.common.ReligionDto + +@Schema(description = "Persons religion information") +data class PersonReligionInformationV1ResponseDto( + @Schema(description = "The persons current religion") + val currentReligion: ReligionDto, + + @Schema(description = "Collection of historical religion information.") + val religionHistory: Set = emptySet(), +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/enumeration/ProtectedCharacteristicsField.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/enumeration/ProtectedCharacteristicsField.kt new file mode 100644 index 0000000..7d29132 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/enumeration/ProtectedCharacteristicsField.kt @@ -0,0 +1,5 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.enumeration + +enum class ProtectedCharacteristicsField { + RELIGION, +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt new file mode 100644 index 0000000..4110dda --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt @@ -0,0 +1,131 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics.resource + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.ResponseEntity.ok +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.personintegrationapi.common.annotation.ValidPrisonerNumber +import uk.gov.justice.digital.hmpps.personintegrationapi.common.dto.ReferenceDataCodeDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.common.ReligionDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.request.ReligionV1RequestDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.response.PersonReligionInformationV1ResponseDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics.PersonProtectedCharacteristicsRoleConstants +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestController +@Tag( + name = "Person Protected characteristics V1", + description = "Protected characteristics information for a HMPPS person.", +) +@RequestMapping( + value = ["v1/person-protected-characteristics"], + produces = [MediaType.APPLICATION_JSON_VALUE], +) +class PersonProtectedCharacteristicsV1Resource { + + @PutMapping("/religion") + @ResponseStatus(HttpStatus.OK) + @Operation( + description = "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}`", + security = [SecurityRequirement(name = PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE)], + responses = [ + ApiResponse( + responseCode = "200", + description = "Religion data successfully added/updated.", + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized to access this endpoint", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "403", + description = "Missing required role. Requires ${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "404", + description = "Data not found", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = ErrorResponse::class), + ), + ], + ), + ], + ) + @PreAuthorize("hasRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}')") + fun putReligionByPrisonerNumber( + @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, + @RequestBody(required = true) @Valid religionV1RequestDto: ReligionV1RequestDto, + ): ResponseEntity { + return ok().body( + PersonReligionInformationV1ResponseDto( + currentReligion = ReligionDto(), + religionHistory = emptySet(), + ), + ) + } + + @GetMapping("reference-data/domain/{domain}/codes") + @ResponseStatus(HttpStatus.OK) + @Operation( + summary = "Get all reference data codes for the given domain", + description = "Returns the list of reference data codes within the given domain. " + + "This endpoint only returns active reference data codes. " + + "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}`", + security = [SecurityRequirement(name = PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE)], + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data codes found", + content = [Content(array = ArraySchema(schema = Schema(implementation = ReferenceDataCodeDto::class)))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Not found, the reference data domain was not found", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + @PreAuthorize("hasRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}')") + fun getReferenceDataCodesByDomain( + @PathVariable @Schema( + description = "The reference data domain", + example = "COUNTRY", + ) domain: String, + ): Collection = listOf() +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/resource/ExampleResource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/resource/ExampleResource.kt deleted file mode 100644 index 2053dc4..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/resource/ExampleResource.kt +++ /dev/null @@ -1,77 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.resource - -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.security.SecurityRequirement -import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.security.access.prepost.PreAuthorize -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 uk.gov.justice.digital.hmpps.templatepackagename.service.ExampleApiService -import uk.gov.justice.hmpps.kotlin.common.ErrorResponse -import java.time.LocalDateTime - -// This controller is expected to be called from the UI - so the hmpps-template-typescript project. -// TODO: This is an example and should renamed / replaced -@RestController -// Role here is specific to the UI. -@PreAuthorize("hasRole('ROLE_TEMPLATE_KOTLIN__UI')") -@RequestMapping(value = ["/example"], produces = ["application/json"]) -class ExampleResource(private val exampleApiService: ExampleApiService) { - - @GetMapping("/time") - @Tag(name = "Examples") - @Operation( - summary = "Retrieve today's date and time", - description = "This is an example endpoint that calls a service to return the current date and time. Requires role ROLE_TEMPLATE_KOTLIN__UI", - security = [SecurityRequirement(name = "person-integration-api-ui-role")], - responses = [ - ApiResponse(responseCode = "200", description = "today's date and time"), - ApiResponse( - responseCode = "401", - description = "Unauthorized to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "403", - description = "Forbidden to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ], - ) - fun getTime(): LocalDateTime = exampleApiService.getTime() - - @GetMapping("/message/{parameter}") - @Tag(name = "Popular") - @Operation( - summary = "Example message endpoint to call another API", - description = """This is an example endpoint that calls back to the kotlin template. - It will return a 404 response as the /example-external-api endpoint hasn't been implemented, so we use wiremock - in integration tests to simulate other responses. - Requires role ROLE_TEMPLATE_KOTLIN__UI""", - security = [SecurityRequirement(name = "person-integration-api-ui-role")], - responses = [ - ApiResponse(responseCode = "200", description = "a message with a parameter"), - ApiResponse( - responseCode = "401", - description = "Unauthorized to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "403", - description = "Forbidden to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "404", - description = "Not found", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ], - ) - fun getMessage(@PathVariable parameter: String) = exampleApiService.exampleGetExternalApiCall(parameter) -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/service/ExampleApiService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/service/ExampleApiService.kt deleted file mode 100644 index d848987..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/service/ExampleApiService.kt +++ /dev/null @@ -1,37 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.service - -import org.springframework.stereotype.Service -import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.WebClientResponseException -import reactor.core.publisher.Mono -import java.time.LocalDateTime - -// This is an example of how to write a service calling out to another service. In this case we have wired up the -// kotlin template with itself so that the template doesn't depend on any other services. -// TODO: This is an example and should be renamed / replaced -@Service -class ExampleApiService( - private val exampleApiWebClient: WebClient, -) { - fun getTime(): LocalDateTime = LocalDateTime.now() - - fun exampleGetExternalApiCall(parameter: String): ExampleMessageDto? = - exampleApiWebClient.get() - // Note that we don't use string interpolation ("/${parameter}"). - // This is important - using string interpolation causes each uri to be added as a separate path in app - // insights and you'll run out of memory in your app. - // Also note that this is just an example and the /example-external-api endpoint doesn't exist in this kotlin - // template project so will return a not found response each time. - .uri("/example-external-api/{parameter}", parameter) - .retrieve() - .bodyToMono(ExampleMessageDto::class.java) - // if the endpoint returns a not found response (404) then treat as empty rather than throwing a server error - // other options would be to re-throw the not found and use the controller advice to return a 404 - .onErrorResume(WebClientResponseException.NotFound::class.java) { Mono.empty() } - .block() -} - -// TODO: This is an example message and should be renamed / replaced -data class ExampleMessageDto( - val message: String, -) From e60934ec4bdbab9096e8c82910ee16dbcf194a90 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 08:53:47 +0000 Subject: [PATCH 09/24] CDPS-1054: Template tests added for iteration 1 API. --- .../CorePersonRecordV1ResourceIntTest.kt | 104 ++++++++++++++ .../service/CorePersonRecordServiceTest.kt | 10 ++ .../integration/ExampleResourceIntTest.kt | 129 ------------------ .../integration/IntegrationTestBase.kt | 14 +- .../integration/NotFoundTest.kt | 2 +- .../integration/OpenApiDocsTest.kt | 50 +++---- .../integration/ResourceSecurityTest.kt | 4 +- .../integration/health/HealthCheckTest.kt | 6 +- .../integration/health/InfoTest.kt | 4 +- .../wiremock/ExampleApiMockServer.kt | 58 -------- .../wiremock/HmppsAuthMockServer.kt | 3 +- .../wiremock/PrisonApiMockServer.kt | 81 +++++++++++ ...otectedCharacteristicsV1ResourceIntTest.kt | 16 +++ src/test/resources/application-test.yml | 9 +- 14 files changed, 256 insertions(+), 234 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt delete mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ExampleResourceIntTest.kt delete mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/ExampleApiMockServer.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/PrisonApiMockServer.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt new file mode 100644 index 0000000..0500604 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt @@ -0,0 +1,104 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.resource + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.CorePersonRecordRoleConstants +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration.CorePersonRecordField +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER_NOT_FOUND +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISON_API_NOT_FOUND_RESPONSE + +class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { + + @DisplayName("PATCH v1/core-person-record") + @Nested + inner class PatchReligionByPrisonerNumberTest { + + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(VALID_PATCH_REQUEST_BODY) + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .bodyValue(VALID_PATCH_REQUEST_BODY) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can patch core person record by prisoner number`() { + webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .bodyValue(VALID_PATCH_REQUEST_BODY) + .exchange() + .expectStatus().isOk + .expectBody().json( + // language=json + """ + { + "status": "${HttpStatus.OK.reasonPhrase}", + "message": "${CorePersonRecordField.BIRTHPLACE.name} for prisoner $PRISONER_NUMBER successfully updated.", + "fieldName": "${CorePersonRecordField.BIRTHPLACE.name}", + "fieldValue": "London" + } + """.trimIndent(), + ) + } + } + + @Nested + inner class NotFound { + + @Test + fun `handles a 404 not found response from downstream api`() { + webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER_NOT_FOUND") + .contentType(MediaType.APPLICATION_JSON) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .bodyValue(VALID_PATCH_REQUEST_BODY) + .exchange() + .expectStatus().isNotFound + .expectBody().json(PRISON_API_NOT_FOUND_RESPONSE.trimIndent()) + } + } + } + + @DisplayName("PUT v1/core-person-record/profile-image") + @Nested + inner class PutProfileImageByPrisonerNumberTest + + @DisplayName("GET v1/core-person-record/reference-data/domain/{domain}/codes") + @Nested + inner class GetReferenceDataCodesByDomain + + private companion object { + + val VALID_PATCH_REQUEST_BODY = + // language=json + """ + { + "fieldName": "BIRTHPLACE", + "fieldValue": "London" + } + """.trimIndent() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt new file mode 100644 index 0000000..43713bb --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt @@ -0,0 +1,10 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.service + +import org.junit.jupiter.api.Test + +class CorePersonRecordServiceTest { + + @Test + fun updateCorePersonRecordField() { + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ExampleResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ExampleResourceIntTest.kt deleted file mode 100644 index 39af6b2..0000000 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ExampleResourceIntTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration - -import com.github.tomakehurst.wiremock.client.WireMock -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.ExampleApiExtension.Companion.exampleApi -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth -import java.time.LocalDate - -class ExampleResourceIntTest : IntegrationTestBase() { - - @Nested - @DisplayName("GET /example/time") - inner class TimeEndpoint { - - @Test - fun `should return unauthorized if no token`() { - webTestClient.get() - .uri("/example/time") - .exchange() - .expectStatus() - .isUnauthorized - } - - @Test - fun `should return forbidden if no role`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation()) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return forbidden if wrong role`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation(roles = listOf("ROLE_WRONG"))) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return OK`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation(roles = listOf("ROLE_TEMPLATE_KOTLIN__UI"))) - .exchange() - .expectStatus() - .isOk - .expectBody() - .jsonPath("$").value { - assertThat(it).startsWith("${LocalDate.now()}") - } - } - } - - @Nested - @DisplayName("GET /example/message/{parameter}") - inner class UserDetailsEndpoint { - - @Test - fun `should return unauthorized if no token`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .exchange() - .expectStatus() - .isUnauthorized - } - - @Test - fun `should return forbidden if no role`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(roles = listOf())) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return forbidden if wrong role`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(roles = listOf("ROLE_WRONG"))) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return OK`() { - hmppsAuth.stubGrantToken() - exampleApi.stubExampleExternalApiUserMessage() - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(username = "AUTH_OK", roles = listOf("ROLE_TEMPLATE_KOTLIN__UI"))) - .exchange() - .expectStatus() - .isOk - .expectBody() - .jsonPath("$.message").isEqualTo("A stubbed message") - - exampleApi.verify(WireMock.getRequestedFor(WireMock.urlEqualTo("/example-external-api/bob"))) - hmppsAuth.verify(1, WireMock.postRequestedFor(WireMock.urlEqualTo("/auth/oauth/token"))) - } - - @Test - fun `should return empty response if user not found`() { - hmppsAuth.stubGrantToken() - exampleApi.stubExampleExternalApiNotFound() - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(username = "AUTH_NOTFOUND", roles = listOf("ROLE_TEMPLATE_KOTLIN__UI"))) - .exchange() - .expectStatus() - .isOk - .expectBody() - .jsonPath("$.message").doesNotExist() - - exampleApi.verify(WireMock.getRequestedFor(WireMock.urlEqualTo("/example-external-api/bob"))) - hmppsAuth.verify(1, WireMock.postRequestedFor(WireMock.urlEqualTo("/auth/oauth/token"))) - } - } -} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/IntegrationTestBase.kt index 21cc096..2d76cb8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/IntegrationTestBase.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration +package uk.gov.justice.digital.hmpps.personintegrationapi.integration import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -7,13 +7,13 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDO import org.springframework.http.HttpHeaders import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.ExampleApiExtension -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.ExampleApiExtension.Companion.exampleApi -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.HmppsAuthApiExtension -import uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.HmppsAuthApiExtension +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PrisonApiExtension +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PrisonApiExtension.Companion.prisonApi import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper -@ExtendWith(HmppsAuthApiExtension::class, ExampleApiExtension::class) +@ExtendWith(HmppsAuthApiExtension::class, PrisonApiExtension::class) @SpringBootTest(webEnvironment = RANDOM_PORT) @ActiveProfiles("test") abstract class IntegrationTestBase { @@ -32,6 +32,6 @@ abstract class IntegrationTestBase { protected fun stubPingWithResponse(status: Int) { hmppsAuth.stubHealthPing(status) - exampleApi.stubHealthPing(status) + prisonApi.stubHealthPing(status) } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/NotFoundTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/NotFoundTest.kt index 8899f0a..31dd1e8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/NotFoundTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/NotFoundTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration +package uk.gov.justice.digital.hmpps.personintegrationapi.integration import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/OpenApiDocsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/OpenApiDocsTest.kt index fea041f..b368ffd 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/OpenApiDocsTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/OpenApiDocsTest.kt @@ -1,11 +1,9 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration +package uk.gov.justice.digital.hmpps.personintegrationapi.integration import io.swagger.v3.parser.OpenAPIV3Parser import net.minidev.json.JSONArray import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.http.MediaType import java.time.LocalDate @@ -31,7 +29,8 @@ class OpenApiDocsTest : IntegrationTestBase() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().is3xxRedirection - .expectHeader().value("Location") { it.contains("/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config") } + .expectHeader() + .value("Location") { it.contains("/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config") } } @Test @@ -52,7 +51,8 @@ class OpenApiDocsTest : IntegrationTestBase() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk - .expectBody().jsonPath("info.version").isEqualTo(DateTimeFormatter.ISO_DATE.format(LocalDate.now())) + .expectBody().jsonPath("info.version") + .isEqualTo(DateTimeFormatter.ISO_DATE.format(LocalDate.now())) } @Test @@ -63,33 +63,33 @@ class OpenApiDocsTest : IntegrationTestBase() { } @Test - fun `the open api json path security requirements are valid`() { - val result = OpenAPIV3Parser().readLocation("http://localhost:$port/v3/api-docs", null, null) - - // The security requirements of each path don't appear to be validated like they are at https://editor.swagger.io/ - // We therefore need to grab all the valid security requirements and check that each path only contains those items - val securityRequirements = result.openAPI.security.flatMap { it.keys } - result.openAPI.paths.forEach { pathItem -> - assertThat(pathItem.value.get.security.flatMap { it.keys }).isSubsetOf(securityRequirements) - } + fun `the security scheme is setup for bearer tokens`() { + val bearerJwts = JSONArray() + bearerJwts.addAll(listOf("read", "write")) + webTestClient.get() + .uri("/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("$.components.securitySchemes.bearer-jwt.type").isEqualTo("http") + .jsonPath("$.components.securitySchemes.bearer-jwt.scheme").isEqualTo("bearer") + .jsonPath("$.components.securitySchemes.bearer-jwt.bearerFormat").isEqualTo("JWT") + .jsonPath("$.security[0].bearer-jwt") + .isEqualTo(bearerJwts) } - @ParameterizedTest - @CsvSource(value = ["person-integration-api-ui-role, ROLE_TEMPLATE_KOTLIN__UI"]) - fun `the security scheme is setup for bearer tokens`(key: String, role: String) { + @Test + fun `the security scheme is setup for client credentials grant with hmpps-auth`() { webTestClient.get() .uri("/v3/api-docs") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk .expectBody() - .jsonPath("$.components.securitySchemes.$key.type").isEqualTo("http") - .jsonPath("$.components.securitySchemes.$key.scheme").isEqualTo("bearer") - .jsonPath("$.components.securitySchemes.$key.description").value { - assertThat(it).contains(role) - } - .jsonPath("$.components.securitySchemes.$key.bearerFormat").isEqualTo("JWT") - .jsonPath("$.security[0].$key").isEqualTo(JSONArray().apply { this.add("read") }) + .jsonPath("$.components.securitySchemes.hmpps-auth.type") + .isEqualTo("oauth2") + .jsonPath("$.components.securitySchemes.hmpps-auth.flows.clientCredentials").exists() } @Test @@ -100,6 +100,6 @@ class OpenApiDocsTest : IntegrationTestBase() { .exchange() .expectStatus().isOk .expectBody() - .jsonPath("$.paths[*][*][?(!@.security)]").doesNotExist() + .jsonPath("$.paths[*][*][?(!@.security)]").exists() } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ResourceSecurityTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ResourceSecurityTest.kt index ed76c3e..ae3af50 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ResourceSecurityTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/ResourceSecurityTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration +package uk.gov.justice.digital.hmpps.personintegrationapi.integration import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -15,6 +15,8 @@ class ResourceSecurityTest : IntegrationTestBase() { private val unprotectedDefaultMethods = setOf( "GET /v3/api-docs.yaml", + "GET /v3/api-docs.yaml/{group}", + "GET /v3/api-docs/{group}", "GET /swagger-ui.html", "GET /v3/api-docs", "GET /v3/api-docs/swagger-config", diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/HealthCheckTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/HealthCheckTest.kt index 1a9a954..01005f5 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/HealthCheckTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/HealthCheckTest.kt @@ -1,7 +1,7 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration.health +package uk.gov.justice.digital.hmpps.personintegrationapi.integration.health import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.templatepackagename.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase class HealthCheckTest : IntegrationTestBase() { @@ -30,7 +30,7 @@ class HealthCheckTest : IntegrationTestBase() { .expectBody() .jsonPath("status").isEqualTo("DOWN") .jsonPath("components.hmppsAuth.status").isEqualTo("DOWN") - .jsonPath("components.exampleApi.status").isEqualTo("DOWN") + .jsonPath("components.prisonApi.status").isEqualTo("DOWN") } @Test diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/InfoTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/InfoTest.kt index 45c4614..6494de7 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/InfoTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/health/InfoTest.kt @@ -1,8 +1,8 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration.health +package uk.gov.justice.digital.hmpps.personintegrationapi.integration.health import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.templatepackagename.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase import java.time.LocalDateTime import java.time.format.DateTimeFormatter diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/ExampleApiMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/ExampleApiMockServer.kt deleted file mode 100644 index 13c52ee..0000000 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/ExampleApiMockServer.kt +++ /dev/null @@ -1,58 +0,0 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock - -import com.github.tomakehurst.wiremock.WireMockServer -import com.github.tomakehurst.wiremock.client.WireMock.aResponse -import com.github.tomakehurst.wiremock.client.WireMock.get -import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching -import org.junit.jupiter.api.extension.AfterAllCallback -import org.junit.jupiter.api.extension.BeforeAllCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -// TODO: Remove / replace this mock server as it currently calls the Example API (itself) -class ExampleApiMockServer : WireMockServer(8091) { - fun stubHealthPing(status: Int) { - stubFor( - get("/health/ping").willReturn( - aResponse() - .withHeader("Content-Type", "application/json") - .withBody("""{"status":"${if (status == 200) "UP" else "DOWN"}"}""") - .withStatus(status), - ), - ) - } - - fun stubExampleExternalApiUserMessage() { - stubFor( - get(urlPathMatching("/example-external-api/[a-zA-Z]*")) - .willReturn( - aResponse() - .withHeader("Content-Type", "application/json") - .withBody("""{ "message": "A stubbed message" }"""), - ), - ) - } - - fun stubExampleExternalApiNotFound() { - stubFor( - get(urlPathMatching("/example-external-api/[a-zA-Z]*")) - .willReturn( - aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("""{ "userMessage": "A stubbed message" }"""), - ), - ) - } -} - -class ExampleApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCallback { - companion object { - @JvmField - val exampleApi = ExampleApiMockServer() - } - - override fun beforeAll(context: ExtensionContext): Unit = exampleApi.start() - override fun beforeEach(context: ExtensionContext): Unit = exampleApi.resetAll() - override fun afterAll(context: ExtensionContext): Unit = exampleApi.stop() -} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/HmppsAuthMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/HmppsAuthMockServer.kt index d75326b..47f57ab 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/HmppsAuthMockServer.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/HmppsAuthMockServer.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.templatepackagename.integration.wiremock +package uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock.aResponse @@ -26,6 +26,7 @@ class HmppsAuthApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCal override fun beforeEach(context: ExtensionContext) { hmppsAuth.resetRequests() + hmppsAuth.stubGrantToken() } override fun afterAll(context: ExtensionContext) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/PrisonApiMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/PrisonApiMockServer.kt new file mode 100644 index 0000000..ad360c5 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/integration/wiremock/PrisonApiMockServer.kt @@ -0,0 +1,81 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.put +import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.http.HttpStatus + +internal const val PRISONER_NUMBER = "A1234AA" +internal const val PRISONER_NUMBER_THROW_EXCEPTION = "THROW" +internal const val PRISONER_NUMBER_NOT_FOUND = "NOTFOUND" +internal const val PRISON_API_NOT_FOUND_RESPONSE = """ + { + "status": 404, + "errorCode": "12345", + "userMessage": "Prisoner not found", + "developerMessage": "Prisoner not found" + } + """ + +class PrisonApiMockServer : WireMockServer(8082) { + fun stubHealthPing(status: Int) { + stubFor( + get("/health/ping").willReturn( + aResponse().withHeader("Content-Type", "application/json") + .withBody("""{"status":"${if (status == 200) "UP" else "DOWN"}"}""").withStatus(status), + ), + ) + } + + fun stubUpdateBirthPlaceForWorkingName(prisonerNumber: String = PRISONER_NUMBER) { + stubFor( + put(urlPathMatching("/api/offenders/$prisonerNumber/birth-place")).willReturn( + aResponse().withHeader("Content-Type", "application/json") + .withStatus(HttpStatus.NO_CONTENT.value()), + ), + ) + } + + fun stubUpdateBirthPlaceForWorkingNameException(prisonerNumber: String = PRISONER_NUMBER_THROW_EXCEPTION) { + stubFor( + put(urlPathMatching("/api/offenders/$prisonerNumber/birth-place")).willReturn( + aResponse().withHeader("Content-Type", "application/json") + .withStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()), + ), + ) + } + + fun stubUpdateBirthPlaceForWorkingNameNotFound(prisonerNumber: String = PRISONER_NUMBER_NOT_FOUND) { + stubFor( + put(urlPathMatching("/api/offenders/$prisonerNumber/birth-place")).willReturn( + aResponse().withHeader("Content-Type", "application/json") + .withStatus(HttpStatus.NOT_FOUND.value()).withBody( + PRISON_API_NOT_FOUND_RESPONSE.trimIndent(), + ), + ), + ) + } +} + +class PrisonApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCallback { + companion object { + @JvmField + val prisonApi = PrisonApiMockServer() + } + + override fun beforeAll(context: ExtensionContext): Unit = prisonApi.start() + override fun beforeEach(context: ExtensionContext) { + prisonApi.resetAll() + prisonApi.stubUpdateBirthPlaceForWorkingName() + prisonApi.stubUpdateBirthPlaceForWorkingNameException() + prisonApi.stubUpdateBirthPlaceForWorkingNameNotFound() + } + + override fun afterAll(context: ExtensionContext): Unit = prisonApi.stop() +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt new file mode 100644 index 0000000..50a0e46 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt @@ -0,0 +1,16 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics.resource + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase + +class PersonProtectedCharacteristicsV1ResourceIntTest : IntegrationTestBase() { + + @DisplayName("PUT v1/person-protected-characteristics/religion") + @Nested + inner class PutReligionByPrisonerNumberTest + + @DisplayName("GET v1/person-protected-characteristics/reference-data/domain/{domain}/codes") + @Nested + inner class GetReferenceDataCodesByDomain +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index ea3dd6f..1e431e0 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -8,10 +8,5 @@ management.endpoint: hmpps-auth: url: "http://localhost:8090/auth" -# example client configuration for calling out to other services -# TODO: Remove / replace this configuration -example-api: - url: "http://localhost:8091" - client: - id: "example-api-client" - secret: "example-api-client-secret" +prison-api: + base_url: "http://localhost:8082" From c568719821c3ff0a2956558f699b1576fc86928b Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 09:07:36 +0000 Subject: [PATCH 10/24] CDPS-1054: Corrected typo in app name. --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7e751ac..ea015c4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ info.app: - name: HMPPS Peron Integration API + name: HMPPS Person Integration API version: 1.0 spring: From 0dab87170f683c3fd10aec30514ec9854d35adf4 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 09:08:29 +0000 Subject: [PATCH 11/24] CDPS-1054: Set SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI to the local hmpps auth container url --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a8d05be..87f25eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: environment: - SERVER_PORT=8080 - SPRING_PROFILES_ACTIVE=nomis-hsqldb - - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=https://sign-in-dev.hmpps.service.justice.gov.uk/auth/.well-known/jwks.json + - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://localhost:8090/auth/.well-known/jwks.json networks: hmpps: From e811c5ede10320fd8a3d24d7ca199957f78f47c3 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 09:10:07 +0000 Subject: [PATCH 12/24] CDPS-1054: Lint issues fixed. --- .../corepersonrecord/resource/CorePersonRecordV1Resource.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt index 130a8f8..8e16253 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -28,7 +28,6 @@ import org.springframework.web.multipart.MultipartFile import uk.gov.justice.digital.hmpps.personintegrationapi.common.annotation.ValidPrisonerNumber import uk.gov.justice.digital.hmpps.personintegrationapi.common.dto.ReferenceDataCodeDto import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.CorePersonRecordRoleConstants -import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.response.FieldUpdateResponseDto import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.dto.v1.request.CorePersonRecordV1UpdateRequestDto import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.service.CorePersonRecordService import uk.gov.justice.hmpps.kotlin.common.ErrorResponse From 33d77c3925508265290075ed8771bc429569b6da Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 09:12:52 +0000 Subject: [PATCH 13/24] CDPS-1054: Switch helm lint environment name from development to dev. --- .github/workflows/pipeline.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 2ec6ed7..dea89eb 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -12,11 +12,11 @@ on: type: choice required: true options: - - development + - dev - preprod - staging - production - default: 'development' + default: 'dev' docker_registry: description: Docker registry required: true @@ -59,7 +59,7 @@ jobs: uses: ministryofjustice/hmpps-github-actions/.github/workflows/test_helm_lint.yml@v1 # WORKFLOW VERSION secrets: inherit with: - environment: ${{ inputs.environment || 'development' }} + environment: ${{ inputs.environment || 'dev' }} kotlin_validate: name: Validate the kotlin uses: ministryofjustice/hmpps-github-actions/.github/workflows/kotlin_validate.yml@v1 # WORKFLOW_VERSION From c8b2da253a655345f7c4b4a7977c5b32916ea78c Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 09:54:20 +0000 Subject: [PATCH 14/24] CDPS-1054: Updated resource and service tests for core person record. --- .../resource/CorePersonRecordV1Resource.kt | 4 +- .../CorePersonRecordV1ResourceIntTest.kt | 20 ++----- .../service/CorePersonRecordServiceTest.kt | 53 ++++++++++++++++++- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt index 8e16253..1c0c701 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -88,12 +88,14 @@ class CorePersonRecordV1Resource( fun patchByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, @RequestBody(required = true) @Valid corePersonRecordUpdateRequest: CorePersonRecordV1UpdateRequestDto, - ) { + ): ResponseEntity { corePersonRecordService.updateCorePersonRecordField( prisonerNumber, corePersonRecordUpdateRequest.fieldName, corePersonRecordUpdateRequest.fieldValue, ) + + return ResponseEntity.noContent().build() } @PutMapping( diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt index 0500604..ba27649 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt @@ -3,10 +3,8 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.resou import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.springframework.http.HttpStatus import org.springframework.http.MediaType import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.CorePersonRecordRoleConstants -import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration.CorePersonRecordField import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER_NOT_FOUND @@ -16,7 +14,7 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { @DisplayName("PATCH v1/core-person-record") @Nested - inner class PatchReligionByPrisonerNumberTest { + inner class PatchCorePersonRecordByPrisonerNumberTest { @Nested inner class Security { @@ -51,18 +49,7 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) .bodyValue(VALID_PATCH_REQUEST_BODY) .exchange() - .expectStatus().isOk - .expectBody().json( - // language=json - """ - { - "status": "${HttpStatus.OK.reasonPhrase}", - "message": "${CorePersonRecordField.BIRTHPLACE.name} for prisoner $PRISONER_NUMBER successfully updated.", - "fieldName": "${CorePersonRecordField.BIRTHPLACE.name}", - "fieldValue": "London" - } - """.trimIndent(), - ) + .expectStatus().isNoContent } } @@ -71,7 +58,8 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { @Test fun `handles a 404 not found response from downstream api`() { - webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER_NOT_FOUND") + webTestClient.patch() + .uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER_NOT_FOUND") .contentType(MediaType.APPLICATION_JSON) .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) .bodyValue(VALID_PATCH_REQUEST_BODY) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt index 43713bb..8196ad3 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/service/CorePersonRecordServiceTest.kt @@ -1,10 +1,59 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.service +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.reset +import org.mockito.kotlin.whenever +import org.springframework.http.ResponseEntity +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.PrisonApiClient +import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.dto.UpdateBirthPlace +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.enumeration.CorePersonRecordField +import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.exception.UnknownCorePersonFieldException +@ExtendWith(MockitoExtension::class) class CorePersonRecordServiceTest { - @Test - fun updateCorePersonRecordField() { + @Mock + lateinit var prisonApiClient: PrisonApiClient + + @InjectMocks + lateinit var underTest: CorePersonRecordService + + @AfterEach + fun afterEach() { + reset(prisonApiClient) + } + + @BeforeEach + fun beforeEach() { + whenever(prisonApiClient.updateBirthPlaceForWorkingName(PRISONER_NUMBER, TEST_BIRTHPLACE_BODY)).thenReturn(ResponseEntity.noContent().build()) + } + + @Nested + inner class UpdateCorePersonRecordField { + @Test + fun `can update the birthplace field`() { + underTest.updateCorePersonRecordField(PRISONER_NUMBER, CorePersonRecordField.BIRTHPLACE, TEST_BIRTHPLACE_VALUE) + } + + @Test + fun `throws an exception if the field type is not supported`() { + assertThrows { + underTest.updateCorePersonRecordField(PRISONER_NUMBER, CorePersonRecordField.COUNTRY_OF_BIRTH, "") + } + } + } + + private companion object { + const val PRISONER_NUMBER = "A1234AA" + const val TEST_BIRTHPLACE_VALUE = "London" + val TEST_BIRTHPLACE_BODY = UpdateBirthPlace("London") } } From 6fa3703a7bed8a4e5cebcdd4f441552a7f6d2c88 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 13:46:25 +0000 Subject: [PATCH 15/24] CDPS-1054: Added basic tests for prototype functionality and applied auto-formating. --- .../personintegrationapi/common/Constants.kt | 3 +- .../common/annotation/ValidPrisonerNumber.kt | 2 +- .../common/config/OpenApiConfiguration.kt | 9 +- ...h2ClientCredentialGrantRequestConverter.kt | 8 +- .../common/config/WebClientConfiguration.kt | 21 ++-- .../common/dto/ReferenceDataDomainDto.kt | 5 +- .../resource/CorePersonRecordV1Resource.kt | 2 +- ...ientCredentialGrantRequestConverterTest.kt | 45 +++++++ .../CorePersonRecordV1ResourceIntTest.kt | 108 ++++++++++++++++- ...otectedCharacteristicsV1ResourceIntTest.kt | 111 +++++++++++++++++- 10 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverterTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt index e63c844..aa58e0f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/Constants.kt @@ -2,5 +2,6 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.common object Constants { const val PRISONER_NUMBER_REGEX = "^[A-Za-z0-9]{1,10}\$" - const val PRISONER_NUMBER_VALIDATION_MESSAGE = "The prisoner number must be a alphanumeric string upto 10 characters in length." + const val PRISONER_NUMBER_VALIDATION_MESSAGE = + "The prisoner number must be a alphanumeric string upto 10 characters in length." } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt index c0e9919..6dce165 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/annotation/ValidPrisonerNumber.kt @@ -17,7 +17,7 @@ import uk.gov.justice.digital.hmpps.personintegrationapi.common.Constants AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, ) -@kotlin.annotation.Retention( +@Retention( AnnotationRetention.RUNTIME, ) annotation class ValidPrisonerNumber diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt index a9c9e96..7813dc5 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt @@ -28,9 +28,12 @@ class OpenApiConfiguration( .servers( listOf( Server().url("http://localhost:8080").description("Local"), - Server().url("https://person-integration-api-dev.hmpps.service.justice.gov.uk").description("Development"), - Server().url("https://person-integration-api-preprod.hmpps.service.justice.gov.uk").description("Pre-Production"), - Server().url("https://person-integration-api.hmpps.service.justice.gov.uk").description("Production"), + Server().url("https://person-integration-api-dev.hmpps.service.justice.gov.uk") + .description("Development"), + Server().url("https://person-integration-api-preprod.hmpps.service.justice.gov.uk") + .description("Pre-Production"), + Server().url("https://person-integration-api.hmpps.service.justice.gov.uk") + .description("Production"), ), ) .info( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt index edf3680..67b583f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt @@ -8,9 +8,13 @@ import org.springframework.util.MultiValueMap import java.util.* @SuppressWarnings("unchecked") -class UserEnhancedOAuth2ClientCredentialGrantRequestConverter : OAuth2ClientCredentialsGrantRequestEntityConverter() { +class UserEnhancedOAuth2ClientCredentialGrantRequestConverter : + OAuth2ClientCredentialsGrantRequestEntityConverter() { - fun enhanceWithUsername(grantRequest: OAuth2ClientCredentialsGrantRequest?, username: String?): RequestEntity { + fun enhanceWithUsername( + grantRequest: OAuth2ClientCredentialsGrantRequest?, + username: String?, + ): RequestEntity { val request = super.convert(grantRequest) val headers = request.headers val body = Objects.requireNonNull(request).body diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt index 4cedeb1..b413d4c 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt @@ -15,7 +15,6 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentia import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction import org.springframework.web.context.annotation.RequestScope -import org.springframework.web.reactive.function.client.ExchangeFilterFunction import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.support.WebClientAdapter import org.springframework.web.service.invoker.HttpServiceProxyFactory @@ -32,7 +31,8 @@ class WebClientConfiguration( @Value("\${api.timeout:90s}") val timeout: Duration, ) { @Bean - fun authHealthWebClient(builder: WebClient.Builder): WebClient = builder.healthWebClient(authBaseUri, healthTimeout) + fun authHealthWebClient(builder: WebClient.Builder): WebClient = + builder.healthWebClient(authBaseUri, healthTimeout) @Bean fun prisonApiHealthWebClient(builder: WebClient.Builder): WebClient = @@ -49,14 +49,14 @@ class WebClientConfiguration( builder, prisonApiBaseUri, "prison-api", - null, ) } @Bean @DependsOn("prisonApiWebClient") fun prisonApiClient(prisonApiWebClient: WebClient): PrisonApiClient { - val factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(prisonApiWebClient)).build() + val factory = + HttpServiceProxyFactory.builderFor(WebClientAdapter.create(prisonApiWebClient)).build() val client = factory.createClient(PrisonApiClient::class.java) return client } @@ -74,7 +74,9 @@ class WebClientConfiguration( val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials { clientCredentialsGrantBuilder: OAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder -> - clientCredentialsGrantBuilder.accessTokenResponseClient(defaultClientCredentialsTokenResponseClient) + clientCredentialsGrantBuilder.accessTokenResponseClient( + defaultClientCredentialsTokenResponseClient, + ) } .build() @@ -87,17 +89,10 @@ class WebClientConfiguration( builder: WebClient.Builder, rootUri: String, registrationId: String, - filterFunctions: List?, ): WebClient { val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) oauth2Client.setDefaultClientRegistrationId(registrationId) - val standardBuild = builder.baseUrl(rootUri).apply(oauth2Client.oauth2Configuration()) - - if (filterFunctions.isNullOrEmpty()) { - return standardBuild.build() - } - - return standardBuild.filters { it.addAll(0, filterFunctions.toList()) }.build() + return builder.baseUrl(rootUri).apply(oauth2Client.oauth2Configuration()).build() } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt index ebf7547..98cdb7b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/dto/ReferenceDataDomainDto.kt @@ -10,7 +10,10 @@ data class ReferenceDataDomainDto( @Schema(description = "Short code for the reference data domain", example = "COUNTRY") val code: String, - @Schema(description = "Description of the reference data domain", example = "Countries reference data") + @Schema( + description = "Description of the reference data domain", + example = "Countries reference data", + ) val description: String, @Schema( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt index 1c0c701..1f3f6bc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -151,7 +151,7 @@ class CorePersonRecordV1Resource( @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}')") fun putProfileImageByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, - @RequestPart(name = "Image file", required = true) profileImage: MultipartFile, + @RequestPart(name = "imageFile", required = true) profileImage: MultipartFile, ): ResponseEntity { val inputStreamResource = InputStreamResource(profileImage.inputStream) return ok().contentType( diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverterTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverterTest.kt new file mode 100644 index 0000000..05240df --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverterTest.kt @@ -0,0 +1,45 @@ +package uk.gov.justice.digital.hmpps.personintegrationapi.common.config + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest +import org.springframework.security.oauth2.client.registration.ClientRegistration +import org.springframework.security.oauth2.core.AuthorizationGrantType +import org.springframework.security.oauth2.core.ClientAuthenticationMethod +import org.springframework.util.LinkedMultiValueMap +import uk.gov.justice.digital.hmpps.personintegrationapi.config.UserEnhancedOAuth2ClientCredentialGrantRequestConverter + +class UserEnhancedOAuth2ClientCredentialGrantRequestConverterTest { + + private lateinit var oAuth2ClientCredentialsGrantRequest: OAuth2ClientCredentialsGrantRequest + + private val underTest: UserEnhancedOAuth2ClientCredentialGrantRequestConverter = + UserEnhancedOAuth2ClientCredentialGrantRequestConverter() + + @BeforeEach + fun setUp() { + oAuth2ClientCredentialsGrantRequest = OAuth2ClientCredentialsGrantRequest(CLIENT_REGISTRATION) + } + + @Test + fun `client credentials grant request has the username added`() { + val response = underTest.enhanceWithUsername(oAuth2ClientCredentialsGrantRequest, TEST_USERNAME) + assertThat((response.body as LinkedMultiValueMap<*, *>)["username"]).isEqualTo(listOf(TEST_USERNAME)) + } + + companion object { + const val TEST_USERNAME = "TEST_USERNAME" + val CLIENT_REGISTRATION: ClientRegistration = + ClientRegistration + .withRegistrationId("test_id") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri("oauth/token") + .scope("read") + .userNameAttributeName("id") + .clientName("Client Name") + .clientId("client-id") + .clientSecret("client-secret").build() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt index ba27649..d0bb90e 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt @@ -1,9 +1,17 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.resource +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.InputStreamResource import org.springframework.http.MediaType +import org.springframework.http.client.MultipartBodyBuilder +import org.springframework.mock.web.MockMultipartFile +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.reactive.function.BodyInserters +import uk.gov.justice.digital.hmpps.personintegrationapi.common.dto.ReferenceDataCodeDto import uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord.CorePersonRecordRoleConstants import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER @@ -72,11 +80,92 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { @DisplayName("PUT v1/core-person-record/profile-image") @Nested - inner class PutProfileImageByPrisonerNumberTest + inner class PutProfileImageByPrisonerNumberTest { + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.put() + .uri("/v1/core-person-record/profile-image?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(MULTIPART_BUILDER.build())) + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.put() + .uri("/v1/core-person-record/profile-image?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.MULTIPART_FORM_DATA) + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .body(BodyInserters.fromMultipartData(MULTIPART_BUILDER.build())) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can update core person record profile image by prisoner number`() { + val response = webTestClient.put() + .uri("/v1/core-person-record/profile-image?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.MULTIPART_FORM_DATA) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .body(BodyInserters.fromMultipartData(MULTIPART_BUILDER.build())) + .exchange() + .expectStatus().isOk + .expectBody(InputStreamResource::class.java) + .returnResult().responseBody + + assertThat(response?.filename).isEqualTo(MULTIPART_FILE.originalFilename) + assertThat(response?.contentAsByteArray).isEqualTo(MULTIPART_FILE.bytes) + } + } + } @DisplayName("GET v1/core-person-record/reference-data/domain/{domain}/codes") @Nested - inner class GetReferenceDataCodesByDomain + inner class GetReferenceDataCodesByDomain { + + @Nested + inner class Security { + @Test + fun `access forbidden when no authority`() { + webTestClient.get().uri("/v1/core-person-record/reference-data/domain/$TEST_DOMAIN/codes") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get().uri("/v1/core-person-record/reference-data/domain/$TEST_DOMAIN/codes") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can update core person record profile image by prisoner number`() { + val response = + webTestClient.get().uri("/v1/core-person-record/reference-data/domain/$TEST_DOMAIN/codes") + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE))) + .exchange() + .expectStatus().isOk + .expectBodyList(ReferenceDataCodeDto::class.java) + .returnResult().responseBody + + assertThat(response).isEqualTo(emptyList()) + } + } + } private companion object { @@ -88,5 +177,20 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { "fieldValue": "London" } """.trimIndent() + + val MULTIPART_FILE: MultipartFile = MockMultipartFile( + "file", + "filename.jpg", + MediaType.IMAGE_JPEG_VALUE, + "I AM A JPEG, HONEST...".toByteArray(), + ) + + const val TEST_DOMAIN = "COUNTRY" + + val MULTIPART_BUILDER = + MultipartBodyBuilder().apply { + part("imageFile", ByteArrayResource(MULTIPART_FILE.bytes)) + .header("Content-Disposition", "form-data; name=imageFile; filename=filename.jpg") + } } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt index 50a0e46..533d962 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt @@ -1,16 +1,123 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics.resource +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType +import uk.gov.justice.digital.hmpps.personintegrationapi.common.dto.ReferenceDataCodeDto import uk.gov.justice.digital.hmpps.personintegrationapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.personintegrationapi.integration.wiremock.PRISONER_NUMBER +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.common.ReligionDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.request.ReligionV1RequestDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectedcharacteristics.dto.v1.response.PersonReligionInformationV1ResponseDto +import uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharacteristics.PersonProtectedCharacteristicsRoleConstants +import java.time.LocalDate class PersonProtectedCharacteristicsV1ResourceIntTest : IntegrationTestBase() { @DisplayName("PUT v1/person-protected-characteristics/religion") @Nested - inner class PutReligionByPrisonerNumberTest + inner class PutReligionByPrisonerNumberTest { + @Nested + inner class Security { + + @Test + fun `access forbidden when no authority`() { + webTestClient.put() + .uri("v1/person-protected-characteristics/religion?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(TEST_RELIGION_REQUEST_DTO) + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.put() + .uri("v1/person-protected-characteristics/religion?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(TEST_RELIGION_REQUEST_DTO) + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))).exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can update a persons religion by prisoner number`() { + val response = webTestClient.put() + .uri("v1/person-protected-characteristics/religion?prisonerNumber=$PRISONER_NUMBER") + .contentType(MediaType.APPLICATION_JSON) + .headers(setAuthorisation(roles = listOf(PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE))) + .bodyValue(TEST_RELIGION_REQUEST_DTO) + .exchange() + .expectStatus().isOk + .expectBody(PersonReligionInformationV1ResponseDto::class.java) + .returnResult().responseBody + + assertThat(response).isEqualTo(EXPECTED_RELIGION_UPDATE_RESPONSE) + } + } + } @DisplayName("GET v1/person-protected-characteristics/reference-data/domain/{domain}/codes") @Nested - inner class GetReferenceDataCodesByDomain + inner class GetReferenceDataCodesByDomain { + @Nested + inner class Security { + @Test + fun `access forbidden when no authority`() { + webTestClient.get() + .uri("/v1/person-protected-characteristics/reference-data/domain/$TEST_DOMAIN/codes") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `access forbidden with wrong role`() { + webTestClient.get() + .uri("/v1/person-protected-characteristics/reference-data/domain/$TEST_DOMAIN/codes") + .headers(setAuthorisation(roles = listOf("ROLE_IS_WRONG"))) + .exchange() + .expectStatus().isForbidden + } + } + + @Nested + inner class HappyPath { + + @Test + fun `can get a list of protected characteristics reference data code for a given domain`() { + val response = + webTestClient.get().uri("/v1/person-protected-characteristics/reference-data/domain/$TEST_DOMAIN/codes") + .headers(setAuthorisation(roles = listOf(PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE))) + .exchange() + .expectStatus().isOk + .expectBodyList(ReferenceDataCodeDto::class.java) + .returnResult().responseBody + + assertThat(response).isEqualTo(emptyList()) + } + } + } + + companion object { + val TEST_RELIGION_REQUEST_DTO = + ReligionV1RequestDto( + "AGNO", + "Test Change", + LocalDate.of(2024, 1, 1), + false, + ) + + val EXPECTED_RELIGION_UPDATE_RESPONSE = PersonReligionInformationV1ResponseDto( + currentReligion = ReligionDto(), + religionHistory = emptySet(), + ) + + const val TEST_DOMAIN = "RELIGION" + } } From f860fc38c1f354aafaa2d8b76cac6b7d0e227b37 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 15:46:27 +0000 Subject: [PATCH 16/24] CDPS-1054: Updated roles to be read or read/write and allowed access to reference data to either role. --- .../CorePersonRecordRoleConstants.kt | 2 +- .../resource/CorePersonRecordV1Resource.kt | 16 ++++++++-------- ...ersonProtectedCharacteristicsRoleConstants.kt | 2 +- .../PersonProtectedCharacteristicsV1Resource.kt | 13 +++++-------- .../CorePersonRecordV1ResourceIntTest.kt | 6 +++--- ...nProtectedCharacteristicsV1ResourceIntTest.kt | 2 +- 6 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt index 11c5a7a..e789a58 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/CorePersonRecordRoleConstants.kt @@ -2,5 +2,5 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.corepersonrecord object CorePersonRecordRoleConstants { const val CORE_PERSON_RECORD_READ_ROLE = "ROLE_CORE_PERSON_API__CORE_PERSON_DATA__RO" - const val CORE_PERSON_RECORD_WRITE_ROLE = "ROLE_CORE_PERSON_API__CORE_PERSON_DATA__RW" + const val CORE_PERSON_RECORD_READ_WRITE_ROLE = "ROLE_CORE_PERSON_API__CORE_PERSON_DATA__RW" } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt index 1f3f6bc..05fa2a0 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -46,7 +46,7 @@ class CorePersonRecordV1Resource( @ResponseStatus(HttpStatus.OK) @Operation( summary = "Performs partial updates on the core person record by prisoner number", - description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}`", + description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}`", responses = [ ApiResponse( responseCode = "204", @@ -64,7 +64,7 @@ class CorePersonRecordV1Resource( ), ApiResponse( responseCode = "403", - description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}", + description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -84,7 +84,7 @@ class CorePersonRecordV1Resource( ), ], ) - @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}')") + @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}')") fun patchByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, @RequestBody(required = true) @Valid corePersonRecordUpdateRequest: CorePersonRecordV1UpdateRequestDto, @@ -110,7 +110,7 @@ class CorePersonRecordV1Resource( @ResponseStatus(HttpStatus.OK) @Operation( summary = "Add or updates the profile image on the core person record by prisoner number", - description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}`", + description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}`", responses = [ ApiResponse( responseCode = "200", @@ -128,7 +128,7 @@ class CorePersonRecordV1Resource( ), ApiResponse( responseCode = "403", - description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}.", + description = "Missing required role. Requires ${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}.", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -148,7 +148,7 @@ class CorePersonRecordV1Resource( ), ], ) - @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE}')") + @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}')") fun putProfileImageByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, @RequestPart(name = "imageFile", required = true) profileImage: MultipartFile, @@ -170,7 +170,7 @@ class CorePersonRecordV1Resource( summary = "Get all reference data codes for the given domain", description = "Returns the list of reference data codes within the given domain. " + "This endpoint only returns active reference data codes. " + - "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}`", + "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}` or `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}`", responses = [ ApiResponse( responseCode = "200", @@ -189,7 +189,7 @@ class CorePersonRecordV1Resource( ), ], ) - @PreAuthorize("hasRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}')") + @PreAuthorize("hasAnyRole('${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_ROLE}', '${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}')") fun getReferenceDataCodesByDomain( @PathVariable @Schema( description = "The reference data domain", diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt index 7a7a997..f637e3f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/PersonProtectedCharacteristicsRoleConstants.kt @@ -2,5 +2,5 @@ package uk.gov.justice.digital.hmpps.personintegrationapi.personprotectercharact object PersonProtectedCharacteristicsRoleConstants { const val PROTECTED_CHARACTERISTICS_READ_ROLE = "ROLE_CORE_PERSON_API__PROTECTED_CHARACTERISTICS_DATA__RO" - const val PROTECTED_CHARACTERISTICS_WRITE_ROLE = "ROLE_CORE_PERSON_API__PROTECTED_CHARACTERISTICS_DATA__RW" + const val PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE = "ROLE_CORE_PERSON_API__PROTECTED_CHARACTERISTICS_DATA__RW" } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt index 4110dda..1a9bbc3 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1Resource.kt @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.http.HttpStatus @@ -43,8 +42,7 @@ class PersonProtectedCharacteristicsV1Resource { @PutMapping("/religion") @ResponseStatus(HttpStatus.OK) @Operation( - description = "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}`", - security = [SecurityRequirement(name = PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE)], + description = "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE}`", responses = [ ApiResponse( responseCode = "200", @@ -62,7 +60,7 @@ class PersonProtectedCharacteristicsV1Resource { ), ApiResponse( responseCode = "403", - description = "Missing required role. Requires ${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}", + description = "Missing required role. Requires ${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE}", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -82,7 +80,7 @@ class PersonProtectedCharacteristicsV1Resource { ), ], ) - @PreAuthorize("hasRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE}')") + @PreAuthorize("hasRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE}')") fun putReligionByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, @RequestBody(required = true) @Valid religionV1RequestDto: ReligionV1RequestDto, @@ -101,8 +99,7 @@ class PersonProtectedCharacteristicsV1Resource { summary = "Get all reference data codes for the given domain", description = "Returns the list of reference data codes within the given domain. " + "This endpoint only returns active reference data codes. " + - "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}`", - security = [SecurityRequirement(name = PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE)], + "Requires role `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}` or `${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE}`", responses = [ ApiResponse( responseCode = "200", @@ -121,7 +118,7 @@ class PersonProtectedCharacteristicsV1Resource { ), ], ) - @PreAuthorize("hasRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}')") + @PreAuthorize("hasAnyRole('${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_ROLE}', '${PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE}')") fun getReferenceDataCodesByDomain( @PathVariable @Schema( description = "The reference data domain", diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt index d0bb90e..7f6ffe1 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt @@ -54,7 +54,7 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { fun `can patch core person record by prisoner number`() { webTestClient.patch().uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER") .contentType(MediaType.APPLICATION_JSON) - .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE))) .bodyValue(VALID_PATCH_REQUEST_BODY) .exchange() .expectStatus().isNoContent @@ -69,7 +69,7 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { webTestClient.patch() .uri("/v1/core-person-record?prisonerNumber=$PRISONER_NUMBER_NOT_FOUND") .contentType(MediaType.APPLICATION_JSON) - .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE))) .bodyValue(VALID_PATCH_REQUEST_BODY) .exchange() .expectStatus().isNotFound @@ -114,7 +114,7 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { val response = webTestClient.put() .uri("/v1/core-person-record/profile-image?prisonerNumber=$PRISONER_NUMBER") .contentType(MediaType.MULTIPART_FORM_DATA) - .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_WRITE_ROLE))) + .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE))) .body(BodyInserters.fromMultipartData(MULTIPART_BUILDER.build())) .exchange() .expectStatus().isOk diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt index 533d962..d2ea8cc 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/personprotectercharacteristics/resource/PersonProtectedCharacteristicsV1ResourceIntTest.kt @@ -51,7 +51,7 @@ class PersonProtectedCharacteristicsV1ResourceIntTest : IntegrationTestBase() { val response = webTestClient.put() .uri("v1/person-protected-characteristics/religion?prisonerNumber=$PRISONER_NUMBER") .contentType(MediaType.APPLICATION_JSON) - .headers(setAuthorisation(roles = listOf(PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_WRITE_ROLE))) + .headers(setAuthorisation(roles = listOf(PersonProtectedCharacteristicsRoleConstants.PROTECTED_CHARACTERISTICS_READ_WRITE_ROLE))) .bodyValue(TEST_RELIGION_REQUEST_DTO) .exchange() .expectStatus().isOk From a5f07b18ce65f36339748999b9e4a9a03983fa75 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Tue, 19 Nov 2024 15:59:53 +0000 Subject: [PATCH 17/24] CDPS-1054: Updated app name on open api spec. --- .../personintegrationapi/common/config/OpenApiConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt index 7813dc5..527aedb 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/OpenApiConfiguration.kt @@ -37,7 +37,7 @@ class OpenApiConfiguration( ), ) .info( - Info().title("Core Person Proxy Prototype").version(version) + Info().title("HMPPS Person Integration API").version(version) .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), ) .components( From 40fb198d70dd9380ff8ad97af49bf89e90085ad3 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:44:38 +0000 Subject: [PATCH 18/24] CDPS-1054: Updated the docker-compose file to use container names instead of localhost. --- docker-compose.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 87f25eb..e4597eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3" services: hmpps-person-integration-api: build: @@ -16,8 +15,7 @@ services: environment: - SERVER_PORT=8080 - HMPPS_AUTH_URL=http://hmpps-auth:8080/auth - # TODO: Remove this URL and replace with outgoing service URLs - - EXAMPLE_URL=http://hmpps-person-integration-api:8080 + - PRISON_API_BASE_URL=http://prison-api:8082 - SPRING_PROFILES_ACTIVE=dev hmpps-auth: @@ -46,7 +44,7 @@ services: environment: - SERVER_PORT=8080 - SPRING_PROFILES_ACTIVE=nomis-hsqldb - - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://localhost:8090/auth/.well-known/jwks.json + - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://hmpps-auth:8080/auth/.well-known/jwks.json networks: hmpps: From b4dc9cf711ae9ccf85a60b0c9e63cd31961f32f0 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:44:56 +0000 Subject: [PATCH 19/24] CDPS-1054: Remove wildcard import. --- .../UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt index 67b583f..375dce6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/UserEnhancedOAuth2ClientCredentialGrantRequestConverter.kt @@ -5,7 +5,7 @@ import org.springframework.http.RequestEntity import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter import org.springframework.util.MultiValueMap -import java.util.* +import java.util.Objects @SuppressWarnings("unchecked") class UserEnhancedOAuth2ClientCredentialGrantRequestConverter : From b8d1199b729a47097e8387dbc5a554719f3d9b67 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:46:12 +0000 Subject: [PATCH 20/24] CDPS-1054: Added service specific timeouts to web clients. --- .../common/config/WebClientConfiguration.kt | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt index b413d4c..ae00fda 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/common/config/WebClientConfiguration.kt @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.DependsOn +import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService @@ -18,6 +19,7 @@ import org.springframework.web.context.annotation.RequestScope import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.support.WebClientAdapter import org.springframework.web.service.invoker.HttpServiceProxyFactory +import reactor.netty.http.client.HttpClient import uk.gov.justice.digital.hmpps.personintegrationapi.common.client.PrisonApiClient import uk.gov.justice.digital.hmpps.personintegrationapi.config.UserEnhancedOAuth2ClientCredentialGrantRequestConverter import uk.gov.justice.hmpps.kotlin.auth.healthWebClient @@ -26,17 +28,19 @@ import java.time.Duration @Configuration class WebClientConfiguration( @Value("\${hmpps-auth.url}") private val authBaseUri: String, + @Value("\${hmpps-auth.health.timeout:20s}") private val authHealthTimeout: Duration, + @Value("\${prison-api.base_url}") private val prisonApiBaseUri: String, - @Value("\${api.timeout:20s}") val healthTimeout: Duration, - @Value("\${api.timeout:90s}") val timeout: Duration, + @Value("\${prison-api.health_timeout:20s}") private val prisonApiHealthTimeout: Duration, + @Value("\${prison-api.timeout:30s}") private val prisonApiTimeout: Duration, ) { @Bean fun authHealthWebClient(builder: WebClient.Builder): WebClient = - builder.healthWebClient(authBaseUri, healthTimeout) + builder.healthWebClient(authBaseUri, authHealthTimeout) @Bean fun prisonApiHealthWebClient(builder: WebClient.Builder): WebClient = - builder.healthWebClient(prisonApiBaseUri, healthTimeout) + builder.healthWebClient(prisonApiBaseUri, prisonApiHealthTimeout) @Bean @RequestScope @@ -49,6 +53,7 @@ class WebClientConfiguration( builder, prisonApiBaseUri, "prison-api", + prisonApiTimeout, ) } @@ -58,6 +63,7 @@ class WebClientConfiguration( val factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(prisonApiWebClient)).build() val client = factory.createClient(PrisonApiClient::class.java) + return client } @@ -89,10 +95,15 @@ class WebClientConfiguration( builder: WebClient.Builder, rootUri: String, registrationId: String, + timout: Duration, ): WebClient { val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) oauth2Client.setDefaultClientRegistrationId(registrationId) - return builder.baseUrl(rootUri).apply(oauth2Client.oauth2Configuration()).build() + return builder + .baseUrl(rootUri) + .clientConnector(ReactorClientHttpConnector(HttpClient.create().responseTimeout(timout))) + .apply(oauth2Client.oauth2Configuration()) + .build() } } From cad9f297c41109313c3dfcd1cca540346c77962a Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:50:11 +0000 Subject: [PATCH 21/24] CDPS-1054: Switched PUT and PATCH endpoints to return No Content on success. --- .../resource/CorePersonRecordV1Resource.kt | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt index 05fa2a0..5ad490a 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1Resource.kt @@ -7,12 +7,10 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid -import org.springframework.core.io.InputStreamResource -import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.http.ResponseEntity.ok +import org.springframework.http.ResponseEntity.noContent import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping @@ -43,7 +41,7 @@ class CorePersonRecordV1Resource( ) { @PatchMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.NO_CONTENT) @Operation( summary = "Performs partial updates on the core person record by prisoner number", description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}`", @@ -94,8 +92,7 @@ class CorePersonRecordV1Resource( corePersonRecordUpdateRequest.fieldName, corePersonRecordUpdateRequest.fieldValue, ) - - return ResponseEntity.noContent().build() + return noContent().build() } @PutMapping( @@ -107,13 +104,13 @@ class CorePersonRecordV1Resource( MediaType.IMAGE_JPEG_VALUE, ], ) - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.NO_CONTENT) @Operation( summary = "Add or updates the profile image on the core person record by prisoner number", description = "Requires role `${CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE}`", responses = [ ApiResponse( - responseCode = "200", + responseCode = "204", description = "The image file has been uploaded successfully.", ), ApiResponse( @@ -152,16 +149,8 @@ class CorePersonRecordV1Resource( fun putProfileImageByPrisonerNumber( @RequestParam(required = true) @Valid @ValidPrisonerNumber prisonerNumber: String, @RequestPart(name = "imageFile", required = true) profileImage: MultipartFile, - ): ResponseEntity { - val inputStreamResource = InputStreamResource(profileImage.inputStream) - return ok().contentType( - MediaType.parseMediaType( - profileImage.contentType ?: MediaType.APPLICATION_OCTET_STREAM_VALUE, - ), - ).contentLength(profileImage.size).header( - HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"${profileImage.originalFilename}\"", - ).body(inputStreamResource) + ): ResponseEntity { + return noContent().build() } @GetMapping("reference-data/domain/{domain}/codes") From 291ad8e5dae7b98a6401dda6b1d156acc139f958 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:50:42 +0000 Subject: [PATCH 22/24] CDPS-1054: Fixed description for field value property. --- .../dto/request/CorePersonRecordV1UpdateRequestDto.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt index bea79f4..dc2bac6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/dto/request/CorePersonRecordV1UpdateRequestDto.kt @@ -14,7 +14,7 @@ data class CorePersonRecordV1UpdateRequestDto( val fieldName: CorePersonRecordField, @Schema( - description = "The field to be updated", + description = "The new value for the field", example = "London", required = true, nullable = false, From c0aeeaed39ea95c3bb5c25fe4a425e2c8e9e9098 Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:54:00 +0000 Subject: [PATCH 23/24] CDPS-1054: Updated image update test to expect a no content response. --- .../resource/CorePersonRecordV1ResourceIntTest.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt index 7f6ffe1..9a0c71b 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/personintegrationapi/corepersonrecord/resource/CorePersonRecordV1ResourceIntTest.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.core.io.ByteArrayResource -import org.springframework.core.io.InputStreamResource import org.springframework.http.MediaType import org.springframework.http.client.MultipartBodyBuilder import org.springframework.mock.web.MockMultipartFile @@ -111,18 +110,13 @@ class CorePersonRecordV1ResourceIntTest : IntegrationTestBase() { @Test fun `can update core person record profile image by prisoner number`() { - val response = webTestClient.put() + webTestClient.put() .uri("/v1/core-person-record/profile-image?prisonerNumber=$PRISONER_NUMBER") .contentType(MediaType.MULTIPART_FORM_DATA) .headers(setAuthorisation(roles = listOf(CorePersonRecordRoleConstants.CORE_PERSON_RECORD_READ_WRITE_ROLE))) .body(BodyInserters.fromMultipartData(MULTIPART_BUILDER.build())) .exchange() - .expectStatus().isOk - .expectBody(InputStreamResource::class.java) - .returnResult().responseBody - - assertThat(response?.filename).isEqualTo(MULTIPART_FILE.originalFilename) - assertThat(response?.contentAsByteArray).isEqualTo(MULTIPART_FILE.bytes) + .expectStatus().isNoContent } } } From 5395c1632145f247dc647c06fe50d6db22f9af4c Mon Sep 17 00:00:00 2001 From: Michael Clancy Date: Wed, 20 Nov 2024 14:55:40 +0000 Subject: [PATCH 24/24] CDPS-1054: Corrected prison api port number in docker compose. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e4597eb..2038eb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: environment: - SERVER_PORT=8080 - HMPPS_AUTH_URL=http://hmpps-auth:8080/auth - - PRISON_API_BASE_URL=http://prison-api:8082 + - PRISON_API_BASE_URL=http://prison-api:8080 - SPRING_PROFILES_ACTIVE=dev hmpps-auth: