diff --git a/.github/workflows/smoke-tests.yaml b/.github/workflows/smoke-tests.yaml deleted file mode 100644 index ef6ae037b1..0000000000 --- a/.github/workflows/smoke-tests.yaml +++ /dev/null @@ -1,86 +0,0 @@ -name: consent-smoke-tests - -on: - push: - branches: - - develop - pull_request: - branches: - - develop - -jobs: - smoke-tests: - runs-on: ubuntu-latest - permissions: - contents: 'read' - id-token: 'write' - steps: - - name: setup - id: setup - run: - echo "bee-name=${REPO_NAME}-${RUN_ID}-dev" >> $GITHUB_OUTPUT - env: - REPO_NAME: ${{ github.event.repository.name }} - RUN_ID: ${{ github.run_id }} - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: 25 - cache: 'maven' - - name: Bee Create - uses: broadinstitute/workflow-dispatch@v4 - with: - workflow: bee-create - repo: broadinstitute/terra-github-workflows - ref: refs/heads/main - token: ${{ secrets.BROADBOT_TOKEN}} - inputs: '{ "bee-name": "${{ steps.setup.outputs.bee-name }}", "bee-template-name": "duos", "version-template": "dev" }' - - name: Run Smoke Tests - run: | - mvn clean test -P integration-tests -DbaseUrl="https://consent.${BEE_NAME}.bee.envs-terra.bio/" - env: - BEE_NAME: ${{ steps.setup.outputs.bee-name }} - - name: Store Test Result Artifact - uses: actions/upload-artifact@v7 - if: always() - with: - name: test-reports - path: 'target/surefire-reports' - - name: Bee Destroy - uses: broadinstitute/workflow-dispatch@v4 - if: always() - with: - workflow: bee-destroy - repo: broadinstitute/terra-github-workflows - ref: refs/heads/main - token: ${{ secrets.BROADBOT_TOKEN}} - inputs: '{ "bee-name": "${{ steps.setup.outputs.bee-name }}" }' - - upload-test-reports: - needs: [smoke-tests] - if: always() - permissions: - contents: 'read' - id-token: 'write' - uses: broadinstitute/dsp-reusable-workflows/.github/workflows/upload_test_results_to_biquery.yaml@main - with: - service-name: 'duos' - test-uuid: ${{ github.run_id }} - environment: 'dev' - artifact: 'test-reports' - big-query-table: 'broad-dsde-qa.automated_testing.test_results' - subuuid: ${{ github.run_id }} - - report-workflow: - if: github.ref == 'refs/heads/develop' - uses: broadinstitute/sherlock/.github/workflows/client-report-workflow.yaml@main - with: - relates-to-chart-releases: 'consent-dev' - notify-slack-channels-upon-workflow-failure: ${{ vars.SLACK_NOTIFICATION_CHANNELS }} - notify-slack-channels-upon-workflow-retry: ${{ vars.SLACK_NOTIFICATION_CHANNELS }} - permissions: - id-token: write diff --git a/DEVNOTES.md b/DEVNOTES.md index d0290deb53..a3013a72a7 100644 --- a/DEVNOTES.md +++ b/DEVNOTES.md @@ -151,4 +151,55 @@ e.g. ```$ export OSS_INDEX_PASSWORD=``` Run the dependency checker: -```$ mvn org.owasp:dependency-check-maven:check``` \ No newline at end of file +```$ mvn org.owasp:dependency-check-maven:check``` + +## Integration Testing + +Integration tests live in `src/test/java/**/integration/` and are run as part +of the standard `mvn test` lifecycle — no special profile, external server, or +manual Postgres setup is required. + +#### How they work + +Each test class extends `ContainerTests`, which uses a JUnit 5 +`DropwizardAppExtension` to boot the full application in-process against the +config at `src/test/resources/consent-ci.yaml`. A WireMock server on port 9999 +stands in for all external services (Sam, ECM, GCS, etc.). + +Database seeding is performed programmatically in `ContainerTests.seedDatabase()` +via typed DAO calls (`@BeforeAll`). The seed data is fully synthetic and +idempotent. To add new baseline rows, extend the relevant `seed*` helper method +inside `ContainerTests`. + +#### Database + +`ContainerTests` starts its own [Testcontainers](https://www.testcontainers.org/) +`PostgreSQLContainer` in a static initializer and passes the container's +coordinates directly to `DropwizardAppExtension` via `ConfigOverride`. The +hardcoded coordinates in `consent-ci.yaml` are never reached at runtime. No +local Postgres is needed in any environment. + +#### How they run in CI + +The GitHub Actions workflow at `.github/workflows/coverage.yaml` runs +`mvn clean test` on every push/PR to `develop`, which exercises unit and +integration tests together via Testcontainers — no additional CI configuration +is needed. + +#### Running integration tests locally + +**Integration tests only:** + +```bash +mvn clean test -Dtest="org.broadinstitute.consent.integration.**" +``` + +**All tests** (unit + integration together, as CI does): + +```bash +mvn clean test +``` + +**From the IDE:** run or debug any test class in the `integration` package +directly — `DAOTestHelper` activates automatically and provides the database. + diff --git a/pom.xml b/pom.xml index 21189ade7b..8d2f99d104 100644 --- a/pom.xml +++ b/pom.xml @@ -36,52 +36,6 @@ consent - - - all-tests - - true - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${surefire.version} - - - - @{argLine} -Xmx1024m -XX:TieredStopAtLevel=1 - -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar - -Xshare:off - - **/*.java - **/integration/**/*.java - - - - - - - integration-tests - - - - org.apache.maven.plugins - maven-surefire-plugin - ${surefire.version} - - **/integration/**/*.java - - false - - - - - - - - diff --git a/src/test/java/org/broadinstitute/consent/integration/ContainerTests.java b/src/test/java/org/broadinstitute/consent/integration/ContainerTests.java new file mode 100644 index 0000000000..b1246a305c --- /dev/null +++ b/src/test/java/org/broadinstitute/consent/integration/ContainerTests.java @@ -0,0 +1,255 @@ +package org.broadinstitute.consent.integration; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.jdbi3.JdbiFactory; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import jakarta.ws.rs.client.Client; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import org.broadinstitute.consent.http.ConsentApplication; +import org.broadinstitute.consent.http.configurations.ConsentConfiguration; +import org.broadinstitute.consent.http.db.DAOTestHelper; +import org.broadinstitute.consent.http.db.DacDAO; +import org.broadinstitute.consent.http.db.InstitutionDAO; +import org.broadinstitute.consent.http.db.UserDAO; +import org.broadinstitute.consent.http.db.UserRoleDAO; +import org.broadinstitute.consent.http.enumeration.UserRoles; +import org.broadinstitute.consent.http.models.Dac; +import org.broadinstitute.consent.http.models.Institution; +import org.broadinstitute.consent.http.models.User; +import org.broadinstitute.consent.http.util.ConsentLogger; +import org.broadinstitute.consent.http.util.gson.GsonUtil; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.gson2.Gson2Config; +import org.jdbi.v3.gson2.Gson2Plugin; +import org.jdbi.v3.guava.GuavaPlugin; +import org.jdbi.v3.sqlobject.SqlObjectPlugin; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +@ExtendWith(DropwizardExtensionsSupport.class) +public abstract class ContainerTests implements ConsentLogger { + + /** + * PostgreSQL container started once per JVM. Static fields are initialized top-to-bottom, so the + * container is running before {@code APPLICATION} is constructed. Testcontainers registers a JVM + * shutdown hook via Ryuk, so no explicit {@code @AfterAll} teardown is required. + */ + @SuppressWarnings("resource") + private static final PostgreSQLContainer POSTGRES = + new PostgreSQLContainer<>(DAOTestHelper.POSTGRES_IMAGE) + .withCommand("postgres -c max_connections=20") + .waitingFor(Wait.forListeningPorts()); + + static { + POSTGRES.start(); + } + + protected static final DropwizardAppExtension APPLICATION = + new DropwizardAppExtension<>( + ConsentApplication.class, + ResourceHelpers.resourceFilePath("consent-ci.yaml"), + ConfigOverride.config("database.driverClass", POSTGRES.getDriverClassName()), + ConfigOverride.config("database.url", POSTGRES.getJdbcUrl()), + ConfigOverride.config("database.user", POSTGRES.getUsername()), + ConfigOverride.config("database.password", POSTGRES.getPassword()), + ConfigOverride.config("database.validationQuery", POSTGRES.getTestQueryString())); + + /** + * WireMock server running on port 9999, which is the fixed base URL used by consent-ci.yaml for + * every external service (Sam, ECM, GCS, etc.). Subclass tests stub specific paths on this server + * before making authenticated API calls. + */ + protected static final WireMockServer WIRE_MOCK = new WireMockServer(options().port(9999)); + + /** + * Guards the one-time database seed so it executes only on the first {@code @BeforeAll} + * invocation across all concrete subclasses in the same JVM, avoiding redundant JDBI setup and + * keeping the seed truly "once per test plan". + */ + private static final AtomicBoolean SEEDED = new AtomicBoolean(false); + + // Note: never close the client returned here — the extension manages its lifetime. + protected static Client getClient() { + return APPLICATION.client(); + } + + /** + * Starts WireMock and seeds the database once per JVM run via typed DAO calls. + * + *

{@link DropwizardExtensionsSupport} implements {@code BeforeAllCallback}, which JUnit 5 + * calls before {@code @BeforeAll} methods, so the application and its database are fully started + * when this method executes. A static {@link AtomicBoolean} guard ensures the expensive JDBI + * setup and seed inserts are performed only on the first invocation, even though + * {@code @BeforeAll} fires once per concrete subclass. + * + *

Every insert operation is idempotent: rows are skipped when they already exist. + */ + @BeforeAll + static void seedDatabase() { + if (!WIRE_MOCK.isRunning()) { + WIRE_MOCK.start(); + } + + if (!SEEDED.compareAndSet(false, true)) { + return; + } + + ConsentConfiguration config = APPLICATION.getConfiguration(); + Environment environment = APPLICATION.getEnvironment(); + + // Build a dedicated JDBI instance for seeding, using the same config/plugins as DAOTestHelper. + Jdbi jdbi = new JdbiFactory().build(environment, config.getDataSourceFactory(), "seed"); + jdbi.installPlugin(new SqlObjectPlugin()); + jdbi.installPlugin(new Gson2Plugin()); + jdbi.installPlugin(new GuavaPlugin()); + jdbi.getConfig().get(Gson2Config.class).setGson(GsonUtil.buildGson()); + + UserDAO userDAO = jdbi.onDemand(UserDAO.class); + UserRoleDAO userRoleDAO = jdbi.onDemand(UserRoleDAO.class); + InstitutionDAO institutionDAO = jdbi.onDemand(InstitutionDAO.class); + DacDAO dacDAO = jdbi.onDemand(DacDAO.class); + + seedUsers(userDAO); + int adminId = userDAO.findUserByEmail("ci-admin@example.com").getUserId(); + seedInstitution(institutionDAO, userDAO, adminId); + seedNonDacRoles(userDAO, userRoleDAO); + int dacId = seedDac(dacDAO, adminId); + seedDacMembers(dacDAO, userDAO, userRoleDAO, dacId, adminId); + } + + @AfterAll + static void stopWireMock() { + if (WIRE_MOCK.isRunning()) { + WIRE_MOCK.stop(); + } + } + + // ------------------------------------------------------------------------- + // Section 1 – Users + // ------------------------------------------------------------------------- + + /** Canonical synthetic users seeded into the CI database before any tests run. */ + public record CiUser(String email, String displayName) {} + + protected static final List CI_USERS = + List.of( + new CiUser("ci-admin@example.com", "CI Admin"), + new CiUser("ci-signing-official@example.com", "CI Signing Official"), + new CiUser("ci-it-director@example.com", "CI IT Director"), + new CiUser("ci-data-submitter@example.com", "CI Data Submitter"), + new CiUser("ci-researcher@example.com", "CI Researcher"), + new CiUser("ci-chair@example.com", "CI DAC Chair"), + new CiUser("ci-member@example.com", "CI DAC Member")); + + private static void seedUsers(UserDAO userDAO) { + Date now = new Date(); + CI_USERS.forEach( + u -> { + if (userDAO.findUserByEmail(u.email()) == null) { + userDAO.insertUser(u.email(), u.displayName(), null, now); + } + }); + } + + // ------------------------------------------------------------------------- + // Section 2 – Institution + // ------------------------------------------------------------------------- + + private static void seedInstitution(InstitutionDAO institutionDAO, UserDAO userDAO, int adminId) { + List existing = institutionDAO.findInstitutionsByName("CI Test Institution"); + int institutionId = + existing.isEmpty() + ? institutionDAO.insertInstitution( + "CI Test Institution", + "CI IT Director", + "ci-it-director@example.com", + null, + null, + null, + null, + null, + null, + adminId, + new Date()) + : existing.getFirst().getId(); + + // Link researcher and signing official to the institution if not already set. + for (String email : List.of("ci-researcher@example.com", "ci-signing-official@example.com")) { + User user = userDAO.findUserByEmail(email); + if (user.getInstitutionId() == null) { + userDAO.updateInstitutionId(user.getUserId(), institutionId); + } + } + } + + // ------------------------------------------------------------------------- + // Section 3 – Non-DAC user roles + // ------------------------------------------------------------------------- + + private static void seedNonDacRoles(UserDAO userDAO, UserRoleDAO userRoleDAO) { + record RoleAssignment(String roleName, String userEmail) {} + List.of( + new RoleAssignment("Admin", "ci-admin@example.com"), + new RoleAssignment("SigningOfficial", "ci-signing-official@example.com"), + new RoleAssignment("ITDirector", "ci-it-director@example.com"), + new RoleAssignment("DataSubmitter", "ci-data-submitter@example.com"), + new RoleAssignment("Researcher", "ci-researcher@example.com")) + .forEach( + ra -> { + int roleId = userRoleDAO.findRoleIdByName(ra.roleName()); + int userId = userDAO.findUserByEmail(ra.userEmail()).getUserId(); + if (userRoleDAO.findRoleByUserIdAndRoleId(userId, roleId) == null) { + userRoleDAO.insertSingleUserRole(roleId, userId); + } + }); + } + + // ------------------------------------------------------------------------- + // Section 4 – DAC (CREATE audit written atomically by createDac) + // ------------------------------------------------------------------------- + + private static int seedDac(DacDAO dacDAO, int adminId) { + return dacDAO.findAll().stream() + .filter(d -> "CI Test DAC".equals(d.getName())) + .findFirst() + .map(Dac::getDacId) + .orElseGet( + () -> dacDAO.createDac("CI Test DAC", "Test DAC for CI integration tests", adminId)); + } + + // ------------------------------------------------------------------------- + // Section 5 – DAC member assignments (ADD audit written atomically by addDacMember) + // ------------------------------------------------------------------------- + + private static void seedDacMembers( + DacDAO dacDAO, UserDAO userDAO, UserRoleDAO userRoleDAO, int dacId, int adminId) { + Set presentMemberIds = + dacDAO.findMembersByDacId(dacId).stream().map(User::getUserId).collect(Collectors.toSet()); + + record DacMember(String roleName, String userEmail) {} + List.of( + new DacMember(UserRoles.CHAIRPERSON.getRoleName(), "ci-chair@example.com"), + new DacMember(UserRoles.MEMBER.getRoleName(), "ci-member@example.com")) + .forEach( + dm -> { + int roleId = userRoleDAO.findRoleIdByName(dm.roleName()); + int userId = userDAO.findUserByEmail(dm.userEmail()).getUserId(); + if (!presentMemberIds.contains(userId)) { + dacDAO.addDacMember(roleId, userId, dacId, adminId); + } + }); + } +} diff --git a/src/test/java/org/broadinstitute/consent/integration/IntegrationTestHelper.java b/src/test/java/org/broadinstitute/consent/integration/IntegrationTestHelper.java deleted file mode 100644 index 83d695070b..0000000000 --- a/src/test/java/org/broadinstitute/consent/integration/IntegrationTestHelper.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.broadinstitute.consent.integration; - -import java.nio.charset.Charset; -import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import org.apache.commons.io.IOUtils; -import org.apache.hc.client5.http.classic.HttpClient; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.broadinstitute.consent.http.util.HttpClientUtil.SimpleResponse; - -public interface IntegrationTestHelper { - - /** - * Integration tests can pass in an alternative url to test against. By default, we'll test - * against develop. - * - * @return Base URL string: `baseUrl` - */ - default String getBaseUrl() { - String baseUrl = System.getenv("baseUrl"); - return Optional.ofNullable(baseUrl) - .filter(Predicate.not(String::isBlank)) - .orElse("https://consent.dsde-dev.broadinstitute.org/"); - } - - int poolSize = 5; - - long delay = 30; - - default SimpleResponse fetchGetResponse(String path) throws Exception { - HttpClient client = HttpClients.createDefault(); - HttpGet request = new HttpGet(getBaseUrl() + path); - final ScheduledExecutorService executor = Executors.newScheduledThreadPool(poolSize); - executor.schedule(request::cancel, delay, TimeUnit.SECONDS); - return client.execute( - request, - httpResponse -> - new SimpleResponse( - httpResponse.getCode(), - IOUtils.toString(httpResponse.getEntity().getContent(), Charset.defaultCharset()))); - } -} diff --git a/src/test/java/org/broadinstitute/consent/integration/README.md b/src/test/java/org/broadinstitute/consent/integration/README.md index 47729fd053..530475ba0f 100644 --- a/src/test/java/org/broadinstitute/consent/integration/README.md +++ b/src/test/java/org/broadinstitute/consent/integration/README.md @@ -1,14 +1,42 @@ -# Smoke Testing +# Integration Testing -Provides a mechanism for running simple smoke tests. The intention here is to keep this -layer as slim as possible to provide a minimum sense of confidence in application stability. +Provides a mechanism for running simple smoke tests against a fully running application stack. +The intention here is to keep this layer as slim as possible to provide a minimum sense of +confidence in application stability. -## Local development/testing process +These tests exercise authenticated HTTP endpoints against a live `DropwizardAppExtension`-managed +application and a real database. They are **not** isolated unit tests. -To run against the default environment (dev), run with no additional arguments: +## How the database is provided + +[`ContainerTests`](ContainerTests.java) starts a Testcontainers `PostgreSQLContainer` in a +static initializer — before `DropwizardAppExtension` is constructed — and passes the +container's coordinates directly as `ConfigOverride` entries: + +```java +ConfigOverride.config("database.url", POSTGRES.getJdbcUrl()), +ConfigOverride.config("database.user", POSTGRES.getUsername()), +ConfigOverride.config("database.password", POSTGRES.getPassword()), +ConfigOverride.config("database.driverClass", POSTGRES.getDriverClassName()), +ConfigOverride.config("database.validationQuery", POSTGRES.getTestQueryString()) +``` + +This means: + +- **No local Postgres installation is needed** — the container is started automatically. +- **No CI database provisioning is needed** — the same container is used in CI. +- The hardcoded coordinates in `consent-ci.yaml` are never reached at runtime; they serve + only as documentation of the expected schema. +- Testcontainers registers a JVM shutdown hook (via Ryuk) that stops the container when the + test JVM exits — no manual teardown is required. + +## Running via Maven ```shell -mvn clean test -P integration-tests +mvn clean test -Dtest="org.broadinstitute.consent.integration.**" ``` -To run against a custom environment, pass a `-DbaseUrl=` with a valid base url. +## Running from the IDE + +No extra configuration is needed. The container starts automatically when the test class is +loaded. diff --git a/src/test/java/org/broadinstitute/consent/integration/status/StatusTests.java b/src/test/java/org/broadinstitute/consent/integration/status/StatusTests.java index 7b9f559553..9bcf6b0995 100644 --- a/src/test/java/org/broadinstitute/consent/integration/status/StatusTests.java +++ b/src/test/java/org/broadinstitute/consent/integration/status/StatusTests.java @@ -2,40 +2,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import com.google.api.client.http.HttpStatusCodes; -import org.broadinstitute.consent.http.util.HttpClientUtil.SimpleResponse; -import org.broadinstitute.consent.integration.IntegrationTestHelper; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import jakarta.ws.rs.core.Response; +import org.broadinstitute.consent.integration.ContainerTests; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -/** - * These tests are not parameterized because that displays poorly in the results xml, i.e. compare: - * Parameterized: + *

  • Sam {@code GET /api/users/v2/self/combinedState}: returns a valid {@code + * CombinedState} JSON so {@link + * org.broadinstitute.consent.http.authentication.DuosUserAuthenticator} can build a {@code + * DuosUser} with a non-null {@code UserStatusInfo}. + *
  • ECM {@code GET /api/oauth/v1/ras} (also matches the double-slash form produced by + * the config concatenation): returns 404 so {@link + * org.broadinstitute.consent.http.service.NihService#syncAccount} treats the user as having + * no NIH account and returns the user record cleanly. + * + * + * Both stubs are idempotent and do not need to be reset between tests in this class. + */ + @BeforeAll + static void stubExternalServices() { + // Sam – combinedState: return an enabled user who has accepted the current ToS. + String combinedStateBody = + """ + { + "samUser": { + "email": "ci-user@example.com", + "enabled": true, + "googleSubjectId": "ci-user-google-subject", + "id": "ci-user-google-subject", + "azureB2CId": null, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + }, + "termsOfServiceDetails": { + "acceptedOn": "2024-01-01T00:00:00.000Z", + "isCurrentVersion": true, + "latestAcceptedVersion": "v1", + "permitsSystemUsage": true + } + } + """; + WIRE_MOCK.stubFor( + get(urlPathEqualTo("/api/users/v2/self/combinedState")) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withStatus(HttpStatusCodes.STATUS_CODE_OK) + .withBody(combinedStateBody))); + + // ECM – RAS provider: 404 → NihService treats this as "no NIH account" and returns the user. + // The config concatenates ecmUrl ("…9999/") + "/api/oauth/v1/ras", which can produce a + // double slash, so the pattern matches both "/api/oauth/v1/ras" and "//api/oauth/v1/ras". + WIRE_MOCK.stubFor( + get(urlPathMatching("/+api/oauth/v1/ras")) + .willReturn(aResponse().withStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND))); + } + + static Stream ciUsers() { + return CI_USERS.stream(); + } + + /** + * Authenticates as each CI user seeded in {@link ContainerTests} and verifies that {@code GET + * /api/user/me} returns that user's profile. + * + *

    Auth is performed by including the OAUTH2_CLAIM_* headers that the app's {@link + * org.broadinstitute.consent.http.filters.RequestHeaderCacheFilter} reads on every inbound + * request and stores in {@link org.broadinstitute.consent.http.filters.ClaimsCache}, keyed by the + * Bearer token value. The auth filter then resolves the token → claims → user. + * + *

    The Sam {@code combinedState} stub email does not need to match the CI user email; + * authentication resolves the DUOS user via {@code OAUTH2_CLAIM_email}, not the Sam response. + */ + @ParameterizedTest + @MethodSource("ciUsers") + void testGetMeForAllCiUsers(CiUser user) { + String bearer = UUID.randomUUID().toString(); + try (Response response = + getClient() + .target(String.format("http://localhost:%d/api/user/me", APPLICATION.getLocalPort())) + .request() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + bearer) + .header("OAUTH2_CLAIM_email", user.email()) + .header("OAUTH2_CLAIM_name", user.displayName()) + .header("OAUTH2_CLAIM_access_token", bearer) + .header("OAUTH2_CLAIM_aud", "test-aud") + .get()) { + assertEquals(200, response.getStatus()); + String body = response.readEntity(String.class); + assertTrue( + body.contains(user.email()), + "Response body should contain %s; got: %s".formatted(user.email(), body)); + } + } +} diff --git a/src/test/resources/consent-ci.yaml b/src/test/resources/consent-ci.yaml new file mode 100644 index 0000000000..6bec436df8 --- /dev/null +++ b/src/test/resources/consent-ci.yaml @@ -0,0 +1,80 @@ +server: + applicationContextPath: / + adminContextPath: /admin + applicationConnectors: + - type: http + port: 8080 + maxRequestHeaderSize: 32KiB + adminConnectors: + - type: http + port: 8081 + requestLog: + type: classic + appenders: + - type: console + +logging: + level: WARN + appenders: + - type: console + threshold: WARN + target: stdout + loggers: + "org.reflections.Reflections": ERROR + "org.apache.pdfbox": ERROR + "org.jdbi.v3": ERROR + +database: + driverClass: org.postgresql.Driver + user: test + password: test + url: jdbc:postgresql://localhost:5432/consent + initialSize: 5 + minSize: 5 + maxSize: 20 + validationQuery: SELECT 1 + +googleStore: + password: /tmp/ci-gcs-account.json + endpoint: http://localhost:9999/ + bucket: ci-bucket + +services: + localURL: http://localhost:8080/ + ontologyURL: http://localhost:9999/ + samUrl: http://localhost:9999/ + ecmUrl: http://localhost:9999/ + activateSupportNotifications: false + timeoutSeconds: 10 + poolSize: 1 + cacheExpireMinutes: 0 + +mailConfiguration: + activateEmailNotifications: false + googleAccount: ci@example.com + sendGridApiKey: ci-key + sendGridStatusUrl: http://localhost:9999/ + +freeMarkerConfiguration: + templateDirectory: /freemarker + defaultEncoding: UTF-8 + +googleAuthentication: + clientId: ci-client-id + +storeOntology: + bucketSubdirectory: ontology + configurationFileName: /configuration + +elasticSearch: + servers: + - localhost + indexName: ontology-ci + datasetIndexName: dataset-ci + +oidcConfiguration: + clientId: ci-client-id + addClientIdToScope: false + extraAuthParams: "" + authorityEndpoint: "http://localhost:9999/" +