diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b7f2b1dd..fadcf37c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -96,6 +96,7 @@ jobs: - name: DAST - Build and run containerised app run: | + cd apiTest docker compose -f docker-compose.yml up -d echo "Waiting for health endpoint..." diff --git a/apiTest/apiTest_README.md b/apiTest/apiTest_README.md new file mode 100644 index 00000000..3bc1d9e3 --- /dev/null +++ b/apiTest/apiTest_README.md @@ -0,0 +1,191 @@ +# API Test Module + +This module contains api tests that run against a Docker containerized version of the application. +The tests make HTTP calls to verify the API endpoints are working correctly. + +## Overview + +The `apiTest` module is a standalone Gradle project that: +- Runs api tests against the application running in Docker containers +- Automatically manages Docker containers (starts before tests, stops after) +- Generates HTML and XML test reports + +## Prerequisites + +Before running the tests, ensure you have the following installed and configured: + +### Required Software + +1. **Java 21** (or higher) + - Verify installation: `java -version` + - Should show version 21 or higher + +2. **Docker Desktop** (or Docker Engine) + - Verify installation: `docker --version` + - Docker must be running (check with `docker ps`) + - Docker Compose V2 must be available: `docker compose version` + +### System Requirements + +- At least 4GB of free RAM (Docker containers need memory) +- Ports available: `8082` (application), `5432` (PostgreSQL), `9999` (WireMock) +- Sufficient disk space for Docker images + +## Running Tests + +### From the Root Directory + +```bash +# Navigate to the apiTest directory +cd apiTest + +# Run all tests +../gradlew test + +# Run tests with more verbose output +../gradlew test --info + +# Run tests with debug output +../gradlew test --debug +``` + +### From the apiTest Directory + +```bash +# If you're already in the apiTest directory +../gradlew test +``` + +### What Happens When You Run Tests + +1. **Builds the root project's bootJar** - The application JAR is built first +2. **Builds Docker images** - Creates the application Docker image +3. **Starts Docker containers**: + - PostgreSQL database (port 5432) + - Application server (port 8082) + - WireMock server (port 9999) +4. **Runs tests** - Executes all test classes +5. **Stops and removes containers** - Cleanup after tests complete + +### Running Specific Tests + +```bash +# Run a specific test class +../gradlew test --tests "RootApiTest" + +# Run a specific test method +../gradlew test --tests "RootApiTest.root_endpoint_should_be_ok" +``` + +## Test Reports + +After running tests, you can view the results in several formats: + +### HTML Test Report (Recommended) + +**Location:** `apiTest/build/reports/tests/test/index.html` + +The HTML report includes: +- Test summary (total, passed, failed, skipped) +- Individual test results with execution times +- Stack traces for failed tests +- Package and class-level summaries + +### JUnit XML Reports + +**Location:** `apiTest/build/test-results/test/` + +These XML reports are useful for CI/CD integration. + +## Troubleshooting + +### Issue: "Could not start Gradle Test Executor 1: Failed to load JUnit Platform" + +**Solution:** This should be resolved with the current configuration. If you see this error: +1. Clean the build: `../gradlew clean` +2. Rebuild: `../gradlew build` + +### Issue: "no main manifest attribute, in /app/apiTest-0.0.999.jar" + +**Solution:** This means the Docker build context is wrong. Ensure: +1. The `docker-compose.yml` has `context: ..` (builds from root directory) +2. The root project's `bootJar` is built before Docker build +3. Run: `../gradlew buildRootBootJar` manually if needed + +### Issue: Container exits with code 1 + +**Check application logs:** +```bash +# View app container logs +docker logs apitest-app-1 + +# Or using docker-compose +docker-compose -f docker-compose.yml logs app + +# View all container logs +docker-compose -f docker-compose.yml logs +``` + +**Common causes:** +- Application configuration errors +- Database connection issues +- Missing environment variables +- Port conflicts + +### Issue: Port already in use + +**Solution:** Stop any services using the required ports: +```bash +# Check what's using port 8082 +lsof -i :8082 + +# Check what's using port 5432 +lsof -i :5432 + +# Stop conflicting services or change ports in docker-compose.yml +``` + +### Issue: Cannot connect to database + +**Solution:** +- Ensure the database container is healthy: `docker ps` should show "Healthy" +- Check database logs: `docker-compose -f docker-compose.yml logs db` +- Verify connection string in `docker-compose.yml` matches database configuration + +## Manual Container Management + +If you need to manually manage containers: + +```bash +# Start containers without running tests +docker-compose -f docker-compose.yml up -d + +# Stop containers +docker-compose -f docker-compose.yml down + +# View container logs +docker-compose -f docker-compose.yml logs -f app + +# Check container status +docker-compose -f docker-compose.yml ps + +# Rebuild containers +docker-compose -f docker-compose.yml build --no-cache +``` + +## Test Configuration + +### Environment Variables + +Tests use the following default configuration: +- Application base URL: `http://localhost:8082` (can be overridden with `app.baseUrl` system property) +- Database: PostgreSQL on port 5432 +- WireMock: Port 9999 + +### Customizing Test Execution + +You can override the application URL: +```bash +gradle test -Dapp.baseUrl=http://localhost:8080 +``` + diff --git a/apiTest/build.gradle b/apiTest/build.gradle new file mode 100644 index 00000000..4d47c69b --- /dev/null +++ b/apiTest/build.gradle @@ -0,0 +1,99 @@ +// TODO need to delete duplicate code +plugins { + id 'java' + id 'com.avast.gradle.docker-compose' version '0.17.20' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'uk.gov.hmcts.cp' +version = System.getProperty('ARTEFACT_VERSION') ?: '0.0.999' + +// Disable main source set since this module only has tests +sourceSets { + main { + java.srcDirs = [] + resources.srcDirs = [] + } +} + +// Disable unnecessary tasks for test-only module +tasks.named('jar') { + enabled = false +} + +tasks.named('compileJava') { + enabled = false +} + +tasks.named('processResources') { + enabled = false +} + +dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:4.0.1" + } +} + +tasks.named('test') { + description = "Runs api tests against docker-compose stack" + group = "Verification" + useJUnitPlatform() + dependsOn tasks.composeUp + finalizedBy tasks.composeDown + + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = 'full' + showStandardStreams = true + } + + reports { + junitXml.required.set(true) + html.required.set(true) + } +} + +tasks.named('build') { + dependsOn tasks.named('test') +} + +dockerCompose { + useComposeFiles = ['docker-compose.yml'] + startedServices = ['db', 'app'] + + buildBeforeUp = true + waitForTcpPorts = true + upAdditionalArgs = ['--wait', '--wait-timeout', '120'] + + captureContainersOutput = true + removeOrphans = true + stopContainers = true + removeContainers = true + + useDockerComposeV2 = true + dockerExecutable = 'docker' +} + +// Build the root project's bootJar before building Docker image +tasks.register('buildRootBootJar', Exec) { + description = "Builds the root project's bootJar" + workingDir = projectDir.parent + executable = "${projectDir.parent}/gradlew" + args = ['bootJar'] +} + +tasks.named('composeBuild') { + dependsOn tasks.named('buildRootBootJar') +} + +dependencies { + testImplementation "org.springframework:spring-web" + testImplementation "org.springframework.boot:spring-boot-starter-test" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" +} + +repositories { + mavenLocal() + mavenCentral() +} \ No newline at end of file diff --git a/docker-compose.yml b/apiTest/docker-compose.yml similarity index 97% rename from docker-compose.yml rename to apiTest/docker-compose.yml index 099b87f8..5bb3522f 100644 --- a/docker-compose.yml +++ b/apiTest/docker-compose.yml @@ -10,6 +10,7 @@ services: app: build: + context: .. dockerfile: Dockerfile environment: SERVER_PORT: 8082 diff --git a/src/apiTest/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java b/apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java similarity index 94% rename from src/apiTest/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java rename to apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java index 44f6d797..e3725918 100644 --- a/src/apiTest/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java +++ b/apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java @@ -13,6 +13,7 @@ class ActuatorApiTest { private final String baseUrl = System.getProperty("app.baseUrl", "http://localhost:8082"); + // TODO remove this client and use FeignClient/RestClient private final RestTemplate http = new RestTemplate(); @Test diff --git a/src/apiTest/java/uk/gov/hmcts/cp/subscription/http/RootApiTest.java b/apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/RootApiTest.java similarity index 100% rename from src/apiTest/java/uk/gov/hmcts/cp/subscription/http/RootApiTest.java rename to apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/RootApiTest.java diff --git a/src/apiTest/java/uk/gov/hmcts/cp/subscription/http/SubscriptionApiTest.java b/apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/SubscriptionApiTest.java similarity index 100% rename from src/apiTest/java/uk/gov/hmcts/cp/subscription/http/SubscriptionApiTest.java rename to apiTest/src/test/java/uk/gov/hmcts/cp/subscription/http/SubscriptionApiTest.java diff --git a/apiTest/src/test/resources/__files/material-content.pdf b/apiTest/src/test/resources/__files/material-content.pdf new file mode 100644 index 00000000..94e3be39 Binary files /dev/null and b/apiTest/src/test/resources/__files/material-content.pdf differ diff --git a/apiTest/src/test/resources/__files/material-response.json b/apiTest/src/test/resources/__files/material-response.json new file mode 100644 index 00000000..30f20a05 --- /dev/null +++ b/apiTest/src/test/resources/__files/material-response.json @@ -0,0 +1,7 @@ +{ + "materialId": "6c198796-08bb-4803-b456-fa0c29ca6021", + "alfrescoAssetId": "82257b1b-571d-432e-8871-b0c5b4bd18b1", + "fileName": "PrisonCourtRegister_20251219083322.pdf", + "mimeType": "application/pdf", + "materialAddedDate": "2025-12-19T08:33:29.866Z" +} \ No newline at end of file diff --git a/apiTest/src/test/resources/mappings/material-content-mapping.json b/apiTest/src/test/resources/mappings/material-content-mapping.json new file mode 100644 index 00000000..89d431bb --- /dev/null +++ b/apiTest/src/test/resources/mappings/material-content-mapping.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "GET", + "url": "/material-query-api/query/api/rest/material/material/7c198796-08bb-4803-b456-fa0c29ca6021/content" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/pdf", + "Content-Disposition": "inline; filename=\"material-content.pdf\"" + }, + "bodyFileName": "material-content.pdf" + } +} diff --git a/apiTest/src/test/resources/mappings/material-metadata-mapping.json b/apiTest/src/test/resources/mappings/material-metadata-mapping.json new file mode 100644 index 00000000..083403ea --- /dev/null +++ b/apiTest/src/test/resources/mappings/material-metadata-mapping.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/metadata" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "material-response.json" + } +} diff --git a/build.gradle b/build.gradle index f2baf3d7..0a2f3e2b 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,6 @@ plugins { id 'com.github.ben-manes.versions' version '0.53.0' id 'org.cyclonedx.bom' version '3.1.0' id 'com.gorylenko.gradle-git-properties' version '2.5.4' - id 'com.avast.gradle.docker-compose' version '0.17.20' } group = 'uk.gov.hmcts.cp' @@ -27,7 +26,6 @@ apply { from("$rootDir/gradle/github/test.gradle") from("$rootDir/gradle/github/jar.gradle") - from("$rootDir/gradle/tasks/apitest.gradle") from("$rootDir/gradle/dependencies/spring-cloud.gradle") } diff --git a/gradle/github/jar.gradle b/gradle/github/jar.gradle index ab1810db..1ab3c102 100644 --- a/gradle/github/jar.gradle +++ b/gradle/github/jar.gradle @@ -24,10 +24,6 @@ bootJar { } } -tasks.named('composeBuild') { - dependsOn tasks.named('bootJar') -} - tasks.withType(AbstractArchiveTask).configureEach { preserveFileTimestamps = false reproducibleFileOrder = true diff --git a/gradle/tasks/apitest.gradle b/gradle/tasks/apitest.gradle deleted file mode 100644 index ba17df22..00000000 --- a/gradle/tasks/apitest.gradle +++ /dev/null @@ -1,54 +0,0 @@ -tasks.register('apiTest', Test) { - description = "Runs api tests against docker-compose stack" - group = "Verification" - - mustRunAfter tasks.named('test') - - testClassesDirs = sourceSets.apiTest.output.classesDirs - classpath = sourceSets.apiTest.runtimeClasspath - useJUnitPlatform() - - dependsOn tasks.composeUp - finalizedBy tasks.composeDown -} - -tasks.named('check') { - dependsOn tasks.named('apiTest') -} - -tasks.named('build') { - dependsOn tasks.named('test') - dependsOn tasks.named('apiTest') -} - -sourceSets { - apiTest { - java { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - - } - } -} - -configurations { - apiTestImplementation.extendsFrom testImplementation - apiTestRuntimeOnly.extendsFrom testRuntimeOnly -} - -dockerCompose { - useComposeFiles = ['docker-compose.yml'] - startedServices = ['db', 'app'] - - buildBeforeUp = true - waitForTcpPorts = true - upAdditionalArgs = ['--wait', '--wait-timeout', '120'] - - captureContainersOutput = true - removeOrphans = true - stopContainers = true - removeContainers = true - - useDockerComposeV2 = true - dockerExecutable = 'docker' -} \ No newline at end of file