diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 3d4ae03..0000000 --- a/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -max_line_length = 120 - -[*.java] -indent_size = 4 -ij_continuation_indent_size = 4 -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = true -ij_java_call_parameters_new_line_after_left_paren = true -ij_java_call_parameters_right_paren_on_new_line = true -ij_java_call_parameters_wrap = on_every_item diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3ce9f9d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" # location of build.gradle + schedule: + interval: "daily" # or "weekly", "monthly" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + groups: + all-dependencies: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" # location of your workflow files + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index a9deb0a..0000000 --- a/.github/renovate.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "local>hmcts/.github:renovate-config", - "local>hmcts/.github//renovate/automerge-all" - ] -} diff --git a/.github/rulesets/master.json b/.github/rulesets/master.json deleted file mode 100644 index 72c9d82..0000000 --- a/.github/rulesets/master.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "id": 4327583, - "name": "master", - "target": "branch", - "source_type": "Repository", - "source": "hmcts/service-cp-crime-scheduleandlist-courtschedule", - "enforcement": "active", - "conditions": { - "ref_name": { - "exclude": [], - "include": [ - "~DEFAULT_BRANCH" - ] - } - }, - "rules": [ - { - "type": "deletion" - }, - { - "type": "non_fast_forward" - }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 0, - "dismiss_stale_reviews_on_push": false, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": false, - "automatic_copilot_code_review_enabled": false, - "allowed_merge_methods": [ - "merge", - "squash", - "rebase" - ] - } - }, - { - "type": "required_status_checks", - "parameters": { - "strict_required_status_checks_policy": false, - "do_not_enforce_on_create": false, - "required_status_checks": [ - { - "context": "lint", - "integration_id": 15368 - } - ] - } - } - ], - "bypass_actors": [] -} diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index e819f21..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 7 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 4 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - dependencies -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: > - This issue is being closed automatically as it was stale diff --git a/.github/workflows/ci-draft.yml b/.github/workflows/ci-draft.yml new file mode 100644 index 0000000..b186b31 --- /dev/null +++ b/.github/workflows/ci-draft.yml @@ -0,0 +1,164 @@ +name: CI Build and Publish Increments Draft + +on: + pull_request: + branches: + - master + - main + push: + branches: + - master + - main + +jobs: + Artefact-Version: + runs-on: ubuntu-latest + outputs: + draft_version: ${{ steps.vars.outputs.draft_version }} + latest_tag: ${{ steps.vars.outputs.latest_tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get short SHA for versioning + id: vars + run: | + if LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null); then + : + else + LATEST_TAG="v0.0.0" + fi + echo "🏷️ Latest Git tag resolved to: $LATEST_TAG" + LATEST_TAG="${LATEST_TAG#v}" + + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + SHORT_SHA=$(git rev-parse --short HEAD) + DRAFT_VERSION="${LATEST_TAG}-${SHORT_SHA}" + + echo "draft_version=$DRAFT_VERSION" + echo "draft_version=$DRAFT_VERSION" >> $GITHUB_OUTPUT + + Build: + needs: [Artefact-Version] + runs-on: ubuntu-latest + outputs: + repo_name: ${{ steps.repo_vars.outputs.repo_name }} + artefact_name: ${{ steps.repo_vars.outputs.artefact_name }} + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: current + + - name: Gradle Build and Publish on Push [Merge] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_DEVOPS_ARTIFACT_USERNAME: ${{ secrets.AZURE_DEVOPS_ARTIFACT_USERNAME }} + AZURE_DEVOPS_ARTIFACT_TOKEN: ${{ secrets.AZURE_DEVOPS_ARTIFACT_TOKEN }} + run: | + VERSION=${{ needs.Artefact-Version.outputs.draft_version }} + + gradle build \ + -DAPI_SPEC_VERSION=$VERSION \ + -DGITHUB_REPOSITORY=${{ github.repository }} \ + -DGITHUB_ACTOR=${{ github.actor }} \ + -DGITHUB_TOKEN=$GITHUB_TOKEN + + if [ "${{ github.event_name }}" == "push" ]; then + echo "Push event trigger - Publishing artefact" + gradle publish \ + -DAPI_SPEC_VERSION=$VERSION \ + -DGITHUB_REPOSITORY=${{ github.repository }} \ + -DGITHUB_ACTOR=${{ github.actor }} \ + -DGITHUB_TOKEN=$GITHUB_TOKEN \ + -DAZURE_DEVOPS_ARTIFACT_USERNAME=$HMCTS_ARTEFACT_ACTOR \ + -DAZURE_DEVOPS_ARTIFACT_TOKEN=$AZURE_DEVOPS_ARTIFACT_TOKEN + else + echo "Skipping publish because this is a pull_request" + fi + + + - name: Extract repo name + if: github.event_name == 'push' + id: repo_vars + run: | + repo_name=${GITHUB_REPOSITORY##*/} + echo "repo_name=${repo_name}" >> $GITHUB_OUTPUT + echo "artefact_name=${repo_name}-${{ needs.Artefact-Version.outputs.draft_version }}" >> $GITHUB_OUTPUT + + - name: Upload JAR Artefact + uses: actions/upload-artifact@v4 + if: github.event_name == 'push' + with: + name: app-jar + path: build/libs/${{ steps.repo_vars.outputs.artefact_name }}.jar + + Build-Docker: + needs: [ Build, Artefact-Version ] + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Download JAR Artefact + uses: actions/download-artifact@v4 + with: + name: app-jar + path: build/libs + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Packages + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Docker Image to GitHub + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ needs.Artefact-Version.outputs.draft_version }} + build-args: | + BASE_IMAGE=openjdk:21-jdk-slim + JAR_FILENAME=${{ needs.Build.outputs.artefact_name }}.jar + + # https://github.com/marketplace/actions/azure-pipelines-action + Deploy: + needs: [ Build, Artefact-Version ] + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Trigger ADO pipeline + uses: Azure/pipelines@v1.2 + with: + azure-devops-project-url: 'https://dev.azure.com/hmcts-cpp/cpp-apps' + azure-pipeline-name: 'cp-gh-artifact-to-acr' + azure-devops-token: ${{ secrets.HMCTS_ADO_PAT }} + azure-pipeline-variables: >- + { + "GROUP_ID" : "uk.gov.hmcts.cp", + "ARTIFACT_ID" : "${{ github.repository }}", + "ARTIFACT_VERSION" : "${{ needs.Artefact-Version.outputs.draft_version}}" + } diff --git a/.github/workflows/ci-released.yml b/.github/workflows/ci-released.yml new file mode 100644 index 0000000..d159600 --- /dev/null +++ b/.github/workflows/ci-released.yml @@ -0,0 +1,130 @@ +name: CI Gradle + +on: + release: + types: [published] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + Artefact-Version: + runs-on: ubuntu-latest + outputs: + RELEASED_VERSION: ${{ steps.vars.outputs.RELEASED_VERSION }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version from tag + id: vars + run: | + TAG_NAME="${GITHUB_REF#refs/tags/}" + VERSION="${TAG_NAME#v}" + echo "RELEASED_VERSION=$VERSION" >> $GITHUB_OUTPUT + + Build: + needs: [Artefact-Version] + runs-on: ubuntu-latest + outputs: + repo_name: ${{ steps.repo_vars.outputs.repo_name }} + artefact_name: ${{ steps.repo_vars.outputs.artefact_name }} + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: current + + - name: Gradle Build and Publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_DEVOPS_ARTIFACT_USERNAME: ${{ secrets.AZURE_DEVOPS_ARTIFACT_USERNAME }} + AZURE_DEVOPS_ARTIFACT_TOKEN: ${{ secrets.AZURE_DEVOPS_ARTIFACT_TOKEN }} + run: | + VERSION=${{ needs.Artefact-Version.outputs.RELEASED_VERSION }} + + gradle publish \ + -DAPI_SPEC_VERSION=$VERSION \ + -DGITHUB_REPOSITORY=${{ github.repository }} \ + -DGITHUB_ACTOR=${{ github.actor }} \ + -DGITHUB_TOKEN=$GITHUB_TOKEN \ + -DAZURE_DEVOPS_ARTIFACT_USERNAME=$HMCTS_ARTEFACT_ACTOR \ + -DAZURE_DEVOPS_ARTIFACT_TOKEN=$AZURE_DEVOPS_ARTIFACT_TOKEN + + - name: Extract repo name + id: repo_vars + run: | + repo_name=${GITHUB_REPOSITORY##*/} + echo "repo_name=${repo_name}" >> $GITHUB_OUTPUT + echo "artefact_name=${repo_name}-${{ needs.Artefact-Version.outputs.RELEASED_VERSION }}" >> $GITHUB_OUTPUT + + - name: Upload JAR Artefact + uses: actions/upload-artifact@v4 + with: + name: app-jar + path: build/libs/${{ steps.repo_vars.outputs.artefact_name }}.jar + + Build-Docker: + needs: [ Build, Artefact-Version ] + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Download JAR Artefact + uses: actions/download-artifact@v4 + with: + name: app-jar + path: build/libs + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Packages + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Docker Image to GitHub + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ needs.Artefact-Version.outputs.RELEASED_VERSION }} + build-args: | + BASE_IMAGE=openjdk:21-jdk-slim + JAR_FILENAME=${{ needs.Build.outputs.artefact_name }}.jar + + # https://github.com/marketplace/actions/azure-pipelines-action + Deploy: + needs: [ Build, Artefact-Version ] + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Trigger ADO pipeline + uses: Azure/pipelines@v1 + with: + azure-devops-project-url: 'https://dev.azure.com/hmcts-cpp/cpp-apps' + azure-pipeline-name: 'hmcts.service-cp-refdata-courthearing-judges' + azure-devops-token: ${{ secrets.HMCTS_ADO_PAT }} + azure-pipeline-variables: | + { + "GROUP_ID" : "uk.gov.hmcts.cp", + "ARTIFACT_ID" : "${{ github.repository }}", + "ARTIFACT_VERSION" : "${{ needs.Artefact-Version.outputs.draft_version}}" + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 97d0252..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Java CI - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - task: [check, test] - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: current - - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v4 - - - name: Run Gradle ${{ matrix.task }} - run: ./gradlew ${{ matrix.task }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 867d05b..e06affb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,22 +1,15 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" +name: CodeQL on: - push: - branches: [ "master" ] pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] + branches: + - master + - main + push: + branches: + - master + - main + schedule: - cron: '36 5 * * 4' @@ -32,52 +25,59 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + language: [ 'java' ] steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality + with: + languages: ${{ matrix.language }} + queries: security-extended - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: current - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Gradle Build and Publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gradle build cyclonedxBom -x test \ + -DGITHUB_REPOSITORY=${{ github.repository }} \ + -DGITHUB_ACTOR=${{ github.actor }} \ + -DGITHUB_TOKEN=$GITHUB_TOKEN - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # If the (auto)build fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - name: Log generated SBOM Hash + run: sha256sum build/resources/main/META-INF/sbom/application.cdx.json || true - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + # This ensures: + # - The SBOM is archived with the CodeQL scan output + # - It's available to download and inspect from the GitHub Actions UI + - name: Upload SBOM + if: always() + uses: actions/upload-artifact@v4 + with: + name: sbom + path: build/resources/main/META-INF/sbom/application.cdx.json diff --git a/.gitignore b/.gitignore index 7683d4e..29d44a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ +gradle +bin/* +!bin/run-in-docker.sh .gradle /build/ +/gradlew +/gradlew.bat !gradle/wrapper/gradle-wrapper.jar *.class bin/main/application.yaml @@ -23,3 +28,6 @@ bin/main/application.yaml applicationinsights-agent-*.jar *.log + +.DS_Store +*/.DS_Store \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 949e24b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "openapi"] - path = openapi - url = ../api-cp-crime-schedulingandlisting-courtschedule.git - branch = master diff --git a/Dockerfile b/Dockerfile index b626bb5..2e0bade 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,26 @@ - # renovate: datasource=github-releases depName=microsoft/ApplicationInsights-Java -ARG APP_INSIGHTS_AGENT_VERSION=3.7.2 - +# ---- Base image (default fallback) ---- ARG BASE_IMAGE -FROM ${BASE_IMAGE:-crmdvrepo01.azurecr.io/registry.hub.docker.com/library/openjdk:21-jdk-slim} +FROM ${BASE_IMAGE:-openjdk:21-jdk-slim} -ENV JAR_FILE_NAME=service-cp-crime-scheduleandlist-courtschedule.jar +# ---- Runtime arguments ---- +ARG JAR_FILENAME=app.jar +ARG JAR_FILE_PATH=build/libs +ENV JAR_FILENAME=$JAR_FILENAME +ENV JAR_FILE_PATH=$JAR_FILE_PATH +ENV JAR_FULL_PATH=$JAR_FILE_PATH/$JAR_FILENAME -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +# ---- Dependencies ---- +RUN apt-get update \ + && apt-get install -y curl \ + && rm -rf /var/lib/apt/lists/* -COPY build/libs/$JAR_FILE_NAME /opt/app/ +# ---- Application files ---- +COPY $JAR_FULL_PATH /opt/app/$JAR_FILENAME COPY lib/applicationinsights.json /opt/app/ +# ---- Permissions ---- +RUN chmod 755 /opt/app/$JAR_FILENAME + +# ---- Runtime ---- EXPOSE 4550 -RUN chmod 755 /opt/app/$JAR_FILE_NAME -CMD sh -c "java -jar /opt/app/$JAR_FILE_NAME" +CMD ["java", "-jar", "/opt/app/$JAR_FILENAME"] \ No newline at end of file diff --git a/README.md b/README.md index 6444356..0780ec9 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,5 @@ -# API CP Spring Boot application template - -## Purpose - -The purpose of this template is to speed up the creation of new Spring applications within HMCTS -and help keep the same standards across multiple teams. If you need to create a new app, you can -simply use this one as a starting point and build on top of it. - -## What's inside - -The template is a working application with a minimal setup. It contains: -* application skeleton -* setup script to prepare project -* common plugins and libraries -* [HMCTS Java plugin](https://github.com/hmcts/gradle-java-plugin) -* docker setup -* automatically publishes API documentation to [hmcts/cnp-api-docs](https://github.com/hmcts/cnp-api-docs) -* code quality tools already set up -* MIT license and contribution information -* Helm chart using chart-java. - -The application exposes health endpoint (http://localhost:4550/health) and metrics endpoint -(http://localhost:4550/metrics). - -## Plugins - -The template contains the following plugins: - -* HMCTS Java plugin - - Applies code analysis tools with HMCTS default settings. See the [project repository](https://github.com/hmcts/gradle-java-plugin) for details. - - Analysis tools include: - - * checkstyle - - https://docs.gradle.org/current/userguide/checkstyle_plugin.html - - Performs code style checks on Java source files using Checkstyle and generates reports from these checks. - The checks are included in gradle's *check* task (you can run them by executing `./gradlew check` command). - - * org.owasp.dependencycheck - - https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html - - Provides monitoring of the project's dependent libraries and creating a report - of known vulnerable components that are included in the build. To run it - execute `gradle dependencyCheck` command. - -* jacoco - - https://docs.gradle.org/current/userguide/jacoco_plugin.html - - Provides code coverage metrics for Java code via integration with JaCoCo. - You can create the report by running the following command: - - ```bash - ./gradlew jacocoTestReport - ``` - - The report will be created in build/reports subdirectory in your project directory. - -* io.spring.dependency-management - - https://github.com/spring-gradle-plugins/dependency-management-plugin - - Provides Maven-like dependency management. Allows you to declare dependency management - using `dependency 'groupId:artifactId:version'` - or `dependency group:'group', name:'name', version:version'`. - -* org.springframework.boot - - http://projects.spring.io/spring-boot/ - - Reduces the amount of work needed to create a Spring application - -* com.github.ben-manes.versions - - https://github.com/ben-manes/gradle-versions-plugin - - Provides a task to determine which dependencies have updates. Usage: - - ```bash - ./gradlew dependencyUpdates -Drevision=release - ``` - -## Setup - -Located in `./bin/init.sh`. Simply run and follow the explanation how to execute it. - -## Building and deploying the application - -### Building the application - -The project uses [Gradle](https://gradle.org) as a build tool. It already contains -`./gradlew` wrapper script, so there's no need to install gradle. - -To build the project execute the following command: - -```bash - ./gradlew clean build -``` - -### Running the application - -Create the image of the application by executing the following command: - -```bash - ./gradlew assemble -``` - -Note: Docker Compose V2 is highly recommended for building and running the application. -In the Compose V2 old `docker-compose` command is replaced with `docker compose`. - -Create docker image and run docker compose: - -```bash - docker compose up --build -``` - -This will start the API container exposing the application's port -(set to `4550` in this template app). - -In order to test if the application is up, you can call its health endpoint: - -```bash - curl http://localhost:4550/health -``` - -You should get a response similar to this: - -``` - {"status":"UP","diskSpace":{"status":"UP","total":249644974080,"free":137188298752,"threshold":10485760}} -``` - -### Alternative script to run application - -To skip all the setting up and building, just execute the following command: - -```bash -./bin/run-in-docker.sh -``` - -For more information: - -```bash -./bin/run-in-docker.sh -h -``` - -Script includes bare minimum environment variables necessary to start api instance. Whenever any variable is changed or any other script regarding docker image/container build, the suggested way to ensure all is cleaned up properly is by this command: - -```bash -docker compose rm -``` - -It clears stopped containers correctly. Might consider removing clutter of images too, especially the ones fiddled with: - -```bash -docker images - -docker image rm -``` - -There is no need to remove postgres and java or similar core images. +# Service: Common Platform (CP) Crime Scheduling and Listing Court Schedule ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details +This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/azure-pipelines.yaml b/azure-pipelines.yaml index bd94268..71d192d 100644 --- a/azure-pipelines.yaml +++ b/azure-pipelines.yaml @@ -5,11 +5,7 @@ trigger: branches: include: - master - - 'team/*' - paths: - include: - - '*' - + - main pr: - '*' diff --git a/build.gradle b/build.gradle index e45f931..d10513d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,56 +1,56 @@ plugins { id 'application' id 'java' - id 'jacoco' id 'io.spring.dependency-management' version '1.1.7' - id 'org.springframework.boot' version '3.4.5' - id 'com.github.ben-manes.versions' version '0.52.0' - id 'org.sonarqube' version '6.2.0.5505' - - id 'org.openapi.generator' version '7.13.0' - id 'com.diffplug.spotless' version '7.0.3' - - /* - Applies analysis tools including checkstyle and OWASP Dependency checker. - See https://github.com/hmcts/gradle-java-plugin - */ - id 'uk.gov.hmcts.java' version '0.12.66' + id 'org.springframework.boot' version '3.5.0' + id 'jacoco' + id 'maven-publish' + id "com.github.ben-manes.versions" version "0.52.0" + id "org.cyclonedx.bom" version "2.3.1" } group = 'uk.gov.hmcts.cp' -version = '0.0.1' +version = System.getProperty('API_SPEC_VERSION') ?: '0.0.999' + +def githubActor = project.findProperty("github.actor") ?: System.getenv("GITHUB_ACTOR") +def githubToken = project.findProperty("github.token") ?: System.getenv("GITHUB_TOKEN") +def githubRepo = System.getenv("GITHUB_REPOSITORY") + +def azureADOArtifactRepository = 'https://pkgs.dev.azure.com/hmcts/Artifacts/_packaging/hmcts-lib/maven/v1' +def azureADOArtifactActor = System.getenv("AZURE_DEVOPS_ARTIFACT_USERNAME") +def azureADOArtifactToken = System.getenv("AZURE_DEVOPS_ARTIFACT_TOKEN") + +//debugging +// if (githubActor != null) { +// println "🔐 Configuring GitHub Packages publishing to: https://maven.pkg.github.com/$githubRepo" +// } +// println "GitHub Packages publishing required environment variables:" +// println " - GITHUB_ACTOR=${githubActor != null ? ' ✔ FOUND' : '❌'}" +// println " - GITHUB_TOKEN=${githubToken != null ? ' ✔ FOUND' : '❌'}" +// println " - GITHUB_REPOSITORY=${githubRepo != null ? ' ✔ FOUND' : '❌'}" + +// println "Azure ADO publishing required environment variables:" +// println " - AZURE_DEVOPS_ARTIFACT_USERNAME=${azureADOArtifactActor != null ? ' ✔ FOUND' : '❌'}" +// println " - AZURE_DEVOPS_ARTIFACT_TOKEN=${azureADOArtifactToken != null ? ' ✔ FOUND' : '❌'}" + java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 - - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } } sourceSets { - - main { - java { - srcDir "$buildDir/generated/src/main/java" - } - } - functionalTest { java { - compileClasspath += main.output - runtimeClasspath += main.output - srcDir file('src/functionalTest/java') + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output } resources.srcDir file('src/functionalTest/resources') } - integrationTest { java { - compileClasspath += main.output - runtimeClasspath += main.output - srcDir file('src/integrationTest/java') + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output } resources.srcDir file('src/integrationTest/resources') } @@ -74,26 +74,29 @@ tasks.withType(JavaExec).configureEach { javaLauncher.set(javaToolchains.launcherFor(java.toolchain)) } -tasks.withType(Test) { +tasks.named('test') { useJUnitPlatform() - + systemProperty 'API_SPEC_VERSION', project.version + failFast = true testLogging { + events "passed", "skipped", "failed" exceptionFormat = 'full' + showStandardStreams = true + } + reports { + junitXml.required.set(true) // For CI tools (e.g. Jenkins, GitHub Actions) + html.required.set(true) // Human-readable browser report } } -test { - failFast = true -} - -task functional(type: Test) { +tasks.register('functional', Test) { description = "Runs functional tests" group = "Verification" testClassesDirs = sourceSets.functionalTest.output.classesDirs classpath = sourceSets.functionalTest.runtimeClasspath } -task integration(type: Test) { +tasks.register('integration', Test) { description = "Runs integration tests" group = "Verification" testClassesDirs = sourceSets.integrationTest.output.classesDirs @@ -101,157 +104,137 @@ task integration(type: Test) { failFast = true } -jacocoTestReport { - executionData(test, integration) +tasks.named('jacocoTestReport') { + dependsOn tasks.named('test') reports { - xml.required = true - csv.required = false - html.required = true + xml.required.set(true) + csv.required.set(false) + html.required.set(true) } } -project.tasks['sonarqube'].dependsOn jacocoTestReport -project.tasks['check'].dependsOn integration - -sonarqube { - properties { - property "sonar.projectName", "Service API CP :: service-cp-crime-scheduleandlist-courtschedule" - property "sonar.projectKey", "uk.gov.hmcts.cp:service-cp-crime-scheduleandlist-courtschedule" - } +tasks.named('check') { + dependsOn tasks.named('integration') + dependsOn tasks.named('functional') } -// before committing a change, make sure task still works -dependencyUpdates { +// check dependencies upon release ONLY +tasks.named("dependencyUpdates").configure { def isNonStable = { String version -> def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { qualifier -> version.toUpperCase().contains(qualifier) } def regex = /^[0-9,.v-]+$/ return !stableKeyword && !(version ==~ regex) } - rejectVersionIf { selection -> // <---- notice how the closure argument is named - return isNonStable(selection.candidate.version) && !isNonStable(selection.currentVersion) + rejectVersionIf { + isNonStable(it.candidate.version) && !isNonStable(it.currentVersion) } } -// https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/configuration.html -dependencyCheck { - suppressionFile = 'config/owasp/suppressions.xml' -} - repositories { mavenLocal() mavenCentral() maven { - url = 'https://jitpack.io' + url = azureADOArtifactRepository } } -ext { - log4JVersion = "2.24.3" - logbackVersion = "1.5.18" - lombokVersion = "1.18.38" - springBootVersion = "3.4.5" -} - -ext['snakeyaml.version'] = '2.0' - -def openApiModule = project(":openapi") -def inputSpecFile = new File(openApiModule.projectDir, "src/main/resources/openapi/courtSchedule.yml") - - -openApiGenerate { - generatorName = "spring" - inputSpec = inputSpecFile.absolutePath - outputDir = "$buildDir/generated" - apiPackage = "uk.gov.hmcts.cp.openapi.api" - modelPackage = "uk.gov.hmcts.cp.openapi.model" - generateModelTests = true - generateApiTests = true - cleanupOutput = true - configOptions = [ - dateLibrary : "java8", - interfaceOnly : "true", - hideGenerationTimestamp: "true", - useJakartaEe : "true", - useBeanValidation : "true", - useTags : "true", - useSpringBoot3 : "true", - implicitHeaders : "false", - performBeanValidation : "true", - openApiNullable : "false" - ] +publishing { + publications { + mavenJava(MavenPublication) { + artifact(tasks.named('bootJar')) + artifact(tasks.named('jar')) + } + } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/$githubRepo") + credentials { + username = githubActor + password = githubToken + } + } + maven { + name = "AzureArtifacts" + url = uri(azureADOArtifactRepository) + credentials { + username = azureADOArtifactActor + password = azureADOArtifactToken + } + } + } } -tasks.named('compileJava') { - dependsOn tasks.named('spotlessApply') +//Creation of Software Bill of Materials +//https://github.com/CycloneDX/cyclonedx-gradle-plugin +cyclonedxBom { + includeConfigs = ["runtimeClasspath"] + skipConfigs = ["compileClasspath", "testImplementation"] + schemaVersion = "1.6" + componentVersion = providers.provider { project.version.toString() } + destination = file("$buildDir/reports") } -spotless { - java { - target 'build/generated/src/main/**/*.java' - removeUnusedImports() - eclipse().configFile('config/formatter/eclipse-formatter.xml') +jar { + enabled = true + archiveClassifier.set('plain') + if (file("CHANGELOG.md").exists()) { + from('CHANGELOG.md') { + into 'META-INF' + } + } else { + println "⚠️ CHANGELOG.md not found, skipping inclusion in JAR" } } +bootJar { + archiveFileName = "${rootProject.name}-${project.version}.jar" - -tasks.named('spotlessJava') { - dependsOn tasks.named('openApiGenerate') + manifest { + attributes('Implementation-Version': project.version.toString()) + } } -tasks.named('spotlessApply') { - dependsOn tasks.named('openApiGenerate') +application { + mainClass = 'uk.gov.hmcts.cp.Application' } -tasks.withType(Checkstyle).configureEach { - def generatedDir = file("${buildDir}/generated/src/main/java").canonicalPath - source = source.filter { file -> - !file.canonicalPath.startsWith(generatedDir) - } +ext { + apiCourtScheduleVersion="0.3.0" + log4JVersion = "2.24.3" + logbackVersion = "1.5.18" + lombokVersion = "1.18.38" } dependencies { + implementation "uk.gov.hmcts.cp:api-cp-crime-schedulingandlisting-courtschedule:$apiCourtScheduleVersion" + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' + implementation 'io.swagger.core.v3:swagger-core:2.2.32' + implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-json' - implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.8.8' - implementation group: 'com.github.hmcts.java-logging', name: 'logging', version: '6.1.9' + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:3.1.11' + implementation 'com.azure.spring:spring-cloud-azure-trace-sleuth:4.20.0' - implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4JVersion + implementation 'net.logstash.logback:logstash-logback-encoder:8.1' implementation group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: log4JVersion implementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion implementation group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion implementation group: 'io.rest-assured', name: 'rest-assured', version: '5.5.5' + implementation 'org.hibernate.validator:hibernate-validator:9.0.0.Final' + implementation 'org.apache.commons:commons-text:1.13.1' - implementation 'org.openapitools:openapi-generator-core:7.13.0' - implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' + compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion + annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion testImplementation(platform('org.junit:junit-bom:5.12.2')) testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion, { + testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.5.0', { exclude group: 'junit', module: 'junit' exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } - - compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion - annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion -} - -application { - mainClass = 'uk.gov.hmcts.cp.Application' -} - -bootJar { - archiveFileName = "service-cp-crime-scheduleandlist-courtschedule.jar" - - manifest { - attributes('Implementation-Version': project.version.toString()) - } -} - -wrapper { - distributionType = Wrapper.DistributionType.ALL } diff --git a/config/formatter/eclipse-formatter.xml b/config/formatter/eclipse-formatter.xml deleted file mode 100644 index 962776d..0000000 --- a/config/formatter/eclipse-formatter.xml +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/owasp/suppressions.xml b/config/owasp/suppressions.xml deleted file mode 100644 index 239ba91..0000000 --- a/config/owasp/suppressions.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - False Positive - - - ^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$ - CVE-2023-35116 - - - diff --git a/docker-compose.yml b/docker-compose.yml index 69ef6e7..e092dd1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '2.1' services: - service-cp-crime-scheduleandlist-courtschedule: + service-cp-refdata-courthearing-judges: build: context: . dockerfile: Dockerfile @@ -10,19 +10,6 @@ services: https_proxy: ${https_proxy} no_proxy: ${no_proxy} BASE_IMAGE: ${BASE_IMAGE} - environment: - # these environment variables are used by java-logging library - - ROOT_APPENDER - - JSON_CONSOLE_PRETTY_PRINT - - ROOT_LOGGING_LEVEL - - REFORM_SERVICE_TYPE - - REFORM_SERVICE_NAME - - REFORM_TEAM - - REFORM_ENVIRONMENT - - LOGBACK_DATE_FORMAT - - LOGBACK_REQUIRE_THREAD - - LOGBACK_REQUIRE_ALERT_LEVEL=false - - LOGBACK_REQUIRE_ERROR_CODE=false ports: - $SERVER_PORT:$SERVER_PORT healthcheck: diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 1b33c55..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 002b867..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 23d15a9..0000000 --- a/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 5eed7ee..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/openapi b/openapi deleted file mode 160000 index 4cb6303..0000000 --- a/openapi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4cb6303af99a137e4038126558141ea498727bf5 diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index da9edde..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include 'openapi' diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtSceduleControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java similarity index 96% rename from src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtSceduleControllerTest.java rename to src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java index 4db861d..9404ffe 100644 --- a/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtSceduleControllerTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerITest.java @@ -16,7 +16,7 @@ @ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc -class CourtSceduleControllerTest { +class CourtScheduleControllerITest { @Autowired private MockMvc mockMvc; diff --git a/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java b/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java index 8b00088..adb8dd7 100644 --- a/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/cp/openapi/OpenAPIPublisherTest.java @@ -31,7 +31,6 @@ class OpenAPIPublisherTest { @DisplayName("Generate swagger documentation") @Test - @SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") void generateDocs() throws Exception { byte[] specs = mvc.perform(get("/v3/api-docs")) .andExpect(status().isOk()) @@ -45,6 +44,6 @@ void generateDocs() throws Exception { try (OutputStream outputStream = Files.newOutputStream(Paths.get(path))) { outputStream.write(specs); } - + assert Files.exists(Paths.get(path)); } } diff --git a/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfiguration.java b/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfiguration.java index ebccb1d..adca540 100644 --- a/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfiguration.java +++ b/src/main/java/uk/gov/hmcts/cp/config/OpenAPIConfiguration.java @@ -1,27 +1,16 @@ package uk.gov.hmcts.cp.config; -import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenAPIConfiguration { + private OpenAPIConfigurationLoader openAPIConfigLoader = new OpenAPIConfigurationLoader(); + @Bean public OpenAPI openAPI() { - return new OpenAPI() - .info( - new Info() - .title("rpe demo") - .description("rpe demo") - .version("v0.0.1") - .license(new License().name("MIT").url("https://opensource.org/licenses/MIT"))) - .externalDocs( - new ExternalDocumentation() - .description("README") - .url("https://github.com/hmcts/service-cp-crime-scheduleandlist-courtschedule")); + return openAPIConfigLoader.openAPI(); } } diff --git a/src/main/java/uk/gov/hmcts/cp/controllers/CourtSceduleController.java b/src/main/java/uk/gov/hmcts/cp/controllers/CourtSceduleController.java deleted file mode 100644 index 89f9c4f..0000000 --- a/src/main/java/uk/gov/hmcts/cp/controllers/CourtSceduleController.java +++ /dev/null @@ -1,23 +0,0 @@ -package uk.gov.hmcts.cp.controllers; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; -import uk.gov.hmcts.cp.openapi.api.CourtScheduleApi; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleschema; -import uk.gov.hmcts.cp.services.CourtScheduleService; - -@RestController -@RequiredArgsConstructor -public class CourtSceduleController implements CourtScheduleApi { - - private final CourtScheduleService courtScheduleService; - - @Override - public ResponseEntity getCourtScheduleByCaseUrn(String caseUrn) { - CourtScheduleschema courtScheduleschema = courtScheduleService.getCourtScheduleschema(caseUrn); - return new ResponseEntity<>(courtScheduleschema, HttpStatus.OK); - } - -} diff --git a/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java b/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java new file mode 100644 index 0000000..76d6f07 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/controllers/CourtScheduleController.java @@ -0,0 +1,42 @@ +package uk.gov.hmcts.cp.controllers; + +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.cp.openapi.api.CourtScheduleApi; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.services.CourtScheduleService; + +@RestController +public class CourtScheduleController implements CourtScheduleApi { + private static final Logger log = LoggerFactory.getLogger(CourtScheduleController.class); + private final CourtScheduleService courtScheduleService; + + public CourtScheduleController(CourtScheduleService courtScheduleService) { + this.courtScheduleService = courtScheduleService; + } + + @Override + public ResponseEntity getCourtScheduleByCaseUrn(String caseUrn) { + String sanitizedCaseUrn; + CourtScheduleResponse courtScheduleResponse; + try { + sanitizedCaseUrn = sanitizeCaseUrn(caseUrn); + courtScheduleResponse = courtScheduleService.getCourtScheduleResponse(sanitizedCaseUrn); + } catch (ResponseStatusException e) { + log.error(e.getMessage()); + return ResponseEntity.status(e.getStatusCode()).build(); + } + log.debug("Found court schedule for caseUrn: {}", sanitizedCaseUrn); + return new ResponseEntity<>(courtScheduleResponse, HttpStatus.OK); + } + + private String sanitizeCaseUrn(String urn) { + if (urn == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "caseUrn is required");; + return StringEscapeUtils.escapeHtml4(urn); + } +} diff --git a/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java b/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java index 22e290b..aba0c8f 100644 --- a/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java +++ b/src/main/java/uk/gov/hmcts/cp/services/CourtScheduleService.java @@ -1,43 +1,53 @@ package uk.gov.hmcts.cp.services; -import static java.util.UUID.randomUUID; - +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleschema; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleschemaCourtScheduleInner; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleschemaCourtScheduleInnerHearingsInner; -import uk.gov.hmcts.cp.openapi.model.CourtScheduleschemaCourtScheduleInnerHearingsInnerCourtSittingsInner; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; import java.time.OffsetDateTime; - import java.util.List; +import java.util.UUID; @Service public class CourtScheduleService { - public CourtScheduleschema getCourtScheduleschema(String caseUrn) { - - CourtScheduleschemaCourtScheduleInnerHearingsInnerCourtSittingsInner courtSittingsItem = - new CourtScheduleschemaCourtScheduleInnerHearingsInnerCourtSittingsInner(); - courtSittingsItem.courtHouse(randomUUID().toString()); - courtSittingsItem.sittingStart(OffsetDateTime.now()); - courtSittingsItem.setSittingEnd(OffsetDateTime.now().plusMinutes(30)); - courtSittingsItem.judiciaryId(randomUUID().toString()); - - CourtScheduleschemaCourtScheduleInnerHearingsInner hearingsItem = - new CourtScheduleschemaCourtScheduleInnerHearingsInner(); - hearingsItem.hearingId(randomUUID().toString()); - hearingsItem.listNote("Requires interpreter"); - hearingsItem.hearingDescription("Sentencing for theft case"); - hearingsItem.hearingType("Trial"); - hearingsItem.courtSittings(List.of(courtSittingsItem)); - - CourtScheduleschemaCourtScheduleInner courtScheduleInner = new CourtScheduleschemaCourtScheduleInner(); - courtScheduleInner.addHearingsItem(hearingsItem); - - CourtScheduleschema courtScheduleschema = new CourtScheduleschema(); - courtScheduleschema.courtSchedule(List.of(courtScheduleInner)); - - return courtScheduleschema; + private static final Logger log = LoggerFactory.getLogger(CourtScheduleService.class); + + public CourtScheduleResponse getCourtScheduleResponse(String caseUrn) throws ResponseStatusException { + if (StringUtils.isEmpty(caseUrn)) { + log.warn("No case urn provided"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "caseUrn is required"); + } + log.warn("NOTE: System configured to return stubbed Court Schedule details. Ignoring provided caseUrn : {}", caseUrn); + CourtScheduleResponse stubbedCourtScheduleResponse = CourtScheduleResponse.builder() + .courtSchedule(List.of( + CourtScheduleResponseCourtScheduleInner.builder() + .hearings(List.of( + CourtScheduleResponseCourtScheduleInnerHearingsInner.builder() + .hearingId(UUID.randomUUID().toString()) + .listNote("Requires interpreter") + .hearingDescription("Sentencing for theft case") + .hearingType("Trial") + .courtSittings(List.of( + CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner.builder() + .courtHouse("Central Criminal Court") + .sittingStart(OffsetDateTime.now()) + .sittingEnd(OffsetDateTime.now().plusMinutes(60)) + .judiciaryId(UUID.randomUUID().toString()) + .build()) + ).build() + ) + ).build() + ) + ).build(); + log.debug("Court Schedule response: {}", stubbedCourtScheduleResponse); + return stubbedCourtScheduleResponse; } } diff --git a/src/main/resources/spring-logback.xml b/src/main/resources/spring-logback.xml new file mode 100644 index 0000000..4166ae3 --- /dev/null +++ b/src/main/resources/spring-logback.xml @@ -0,0 +1,26 @@ + + + + {"application":"${spring.application.name:-application}"} + + traceId + spanId + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java b/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java new file mode 100644 index 0000000..8a450f8 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerTest.java @@ -0,0 +1,76 @@ +package uk.gov.hmcts.cp.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInner; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner; +import uk.gov.hmcts.cp.services.CourtScheduleService; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class CourtScheduleControllerTest { + + private static final Logger log = LoggerFactory.getLogger(CourtScheduleControllerTest.class); + + @BeforeEach + void setUp() { + } + + @Test + void getJudgeById_ShouldReturnJudgesWithOkStatus() { + CourtScheduleController courtScheduleController = new CourtScheduleController(new CourtScheduleService()); + UUID caseUrn = UUID.randomUUID(); + log.info("Calling courtScheduleController.getCourtScheduleByCaseUrn with caseUrn: {}", caseUrn); + ResponseEntity response = courtScheduleController.getCourtScheduleByCaseUrn(caseUrn.toString()); + + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + CourtScheduleResponse responseBody = (CourtScheduleResponse) response.getBody(); + assertNotNull(responseBody); + assertNotNull(responseBody.getCourtSchedule()); + assertEquals(1, responseBody.getCourtSchedule().size()); + + CourtScheduleResponseCourtScheduleInner schedule = responseBody.getCourtSchedule().get(0); + assertNotNull(schedule.getHearings()); + assertEquals(1, schedule.getHearings().size()); + + CourtScheduleResponseCourtScheduleInnerHearingsInner hearing = schedule.getHearings().get(0); + assertNotNull(hearing.getHearingId()); + assertEquals("Requires interpreter", hearing.getListNote()); + assertEquals("Sentencing for theft case", hearing.getHearingDescription()); + assertEquals("Trial", hearing.getHearingType()); + assertNotNull(hearing.getCourtSittings()); + assertEquals(1, hearing.getCourtSittings().size()); + + CourtScheduleResponseCourtScheduleInnerHearingsInnerCourtSittingsInner sitting = + hearing.getCourtSittings().get(0); + assertEquals("Central Criminal Court", sitting.getCourtHouse()); + assertNotNull(sitting.getSittingStart()); + assertTrue(sitting.getSittingEnd().isAfter(sitting.getSittingStart())); + assertNotNull(sitting.getJudiciaryId()); + + } + + @Test + void getJudgeById_ShouldReturnBadRequestStatus() { + CourtScheduleController courtScheduleController = new CourtScheduleController(new CourtScheduleService()); + + log.info("Calling courtScheduleController.getCourtScheduleByCaseUrn with null caseUrn"); + ResponseEntity response = courtScheduleController.getCourtScheduleByCaseUrn(null); + log.debug("Received response: {}", response); + + assertNotNull(response); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/rsecheck/DemoUnitTest.java b/src/test/java/uk/gov/hmcts/cp/rsecheck/DemoUnitTest.java deleted file mode 100644 index 21d4f8f..0000000 --- a/src/test/java/uk/gov/hmcts/cp/rsecheck/DemoUnitTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package uk.gov.hmcts.cp.rsecheck; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -class DemoUnitTest { - - @Test - void exampleOfTest() { - assertTrue(System.currentTimeMillis() > 0, "Example of Unit Test"); - } -} diff --git a/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java b/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java new file mode 100644 index 0000000..a41471e --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/services/CourtScheduleServiceTest.java @@ -0,0 +1,54 @@ +package uk.gov.hmcts.cp.services; + +import org.junit.jupiter.api.Test; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.cp.openapi.model.CourtScheduleResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CourtScheduleServiceTest { + + private final CourtScheduleService courtScheduleService = new CourtScheduleService(); + + @Test + void shouldReturnStubbedCourtScheduleResponse_whenValidCaseUrnProvided() { + // Arrange + String validCaseUrn = "123-ABC-456"; + + // Act + CourtScheduleResponse response = courtScheduleService.getCourtScheduleResponse(validCaseUrn); + + // Assert + assertThat(response).isNotNull(); + assertThat(response.getCourtSchedule()).isNotEmpty(); + assertThat(response.getCourtSchedule().get(0).getHearings()).isNotEmpty(); + assertThat(response.getCourtSchedule().get(0).getHearings().get(0).getCourtSittings()).isNotEmpty(); + assertThat(response.getCourtSchedule().get(0).getHearings().get(0).getHearingDescription()) + .isEqualTo("Sentencing for theft case"); + } + + @Test + void shouldThrowBadRequestException_whenCaseUrnIsNull() { + // Arrange + String nullCaseUrn = null; + + // Act & Assert + assertThatThrownBy(() -> courtScheduleService.getCourtScheduleResponse(nullCaseUrn)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("400 BAD_REQUEST") + .hasMessageContaining("caseUrn is required"); + } + + @Test + void shouldThrowBadRequestException_whenCaseUrnIsEmpty() { + // Arrange + String emptyCaseUrn = ""; + + // Act & Assert + assertThatThrownBy(() -> courtScheduleService.getCourtScheduleResponse(emptyCaseUrn)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("400 BAD_REQUEST") + .hasMessageContaining("caseUrn is required"); + } +} \ No newline at end of file