Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Integration Tests

# Boots selected sample apps against real Postgres / MongoDB containers (Testcontainers) and
# exercises synapse's controller, data-adapter, exception, and api-docs surfaces end-to-end.
#
# Default run: push or PR to develop -> runs against develop.
# Custom run: workflow_dispatch with a `branch` input -> checks out that branch and tests it.
# This is how a contributor verifies a branch (e.g. a synapse-data-mongodb patch) doesn't break
# the routes/objects the framework ships, without setting up Docker locally.

on:
push:
branches: [develop]
pull_request:
branches: [develop]
workflow_dispatch:
inputs:
branch:
description: 'Branch to integration-test (defaults to develop)'
required: false
default: 'develop'
type: string

# Opt into Node.js 24 for the @v4 javascript actions below (checkout, setup-java, cache,
# upload-artifact). Node 20 will be removed from the runner in September 2026; opting in
# now silences the deprecation warning and avoids a forced switch later.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
integration-tests:
permissions:
contents: read
checks: write # required by mikepenz/action-junit-report to publish the test-results check
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'

- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2

- name: Install reactor (so sample apps resolve the checked-out synapse jars)
run: mvn -B -DskipTests -DskipITs install

- name: Run integration tests
# The previous step already installed every reactor module to ~/.m2, so we don't need
# `-am` here; the listed sample modules pull their synapse deps from the local repo.
# `-fae` (fail-at-end) keeps Maven going across modules so a failure in one sample
# doesn't hide the result of the other.
run: |
mvn -B -fae -P integration-tests verify \
-pl service/service-samples/sample-service-rest-postgres-book,service/service-samples/sample-service-reactive-mongodb-book

- name: Publish test report
# Always runs so the summary table is available on both green and red runs. The action
# itself reports failures back to the job, so the previous Maven step can stay green if
# we ever want to inspect reports without failing the workflow.
uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: '**/target/failsafe-reports/TEST-*.xml'
check_name: Integration Test Results
detailed_summary: true
include_passed: true
require_tests: true
fail_on_failure: false # the Maven verify step already fails the job on test failures

- name: Upload Failsafe reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: failsafe-reports
path: |
**/target/failsafe-reports/
**/target/surefire-reports/
retention-days: 14
32 changes: 32 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,21 @@
<artifactId>testcontainers</artifactId>
<version>1.21.2</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.2</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.21.2</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.21.2</version>
</dependency>
<!-- CVE-2026-24400 (High/8.2) - Fixed in 3.27.7, added 2026-02-10
TODO: Remove this override when spring-boot-dependencies includes assertj-core >= 3.27.7
Check: mvn dependency:tree | grep assertj-core -->
Expand Down Expand Up @@ -1344,5 +1359,22 @@
</plugins>
</build>
</profile>
<!--Profile integration-tests: enables Failsafe so *IT.java suites run during `mvn verify -P integration-tests`. Off by default to keep `mvn verify` fast.-->
<profile>
<id>integration-tests</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<skipITs>false</skipITs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,37 @@
<groupId>io.americanexpress.synapse</groupId>
<artifactId>sample-data-mongodb-reactive</artifactId>
</dependency>

<!-- Integration test dependencies (Testcontainers + Spring Boot Test) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2020 American Express Travel Related Services Company, Inc.
*
* 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
*
* http://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.
*/
package io.americanexpress.service.book.rest.integration;

import io.americanexpress.data.book.repository.BookRepository;
import io.americanexpress.service.book.rest.BookApplication;
import io.americanexpress.service.book.rest.model.CreateBookRequest;
import io.americanexpress.service.book.rest.model.ReadBookRequest;
import io.americanexpress.service.book.rest.model.ReadBookResponse;
import io.americanexpress.service.book.rest.model.UpdateBookRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(classes = BookApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
class BookCrudIT {

@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
BookMongoContainer.registerProperties(registry);
}

@Autowired
private WebTestClient web;

@Autowired
private BookRepository bookRepository;

@BeforeEach
void resetCollection() {
bookRepository.deleteAll().block();
}

@Test
void createReadUpdateDelete_givenValidBook_endToEndAgainstRealMongo() {
CreateBookRequest create = new CreateBookRequest();
create.setTitle("Reactive Synapse");
create.setAuthor("synapse-it");

web.post().uri("/v1/books")
.bodyValue(create)
.exchange()
.expectStatus().isCreated();

// BaseReadFluxReactiveController exposes POST /v1/books/multiple_results — Flux response
ReadBookRequest read = new ReadBookRequest();
web.post().uri("/v1/books/multiple_results")
.bodyValue(read)
.exchange()
.expectStatus().isOk()
.expectBodyList(ReadBookResponse.class)
.hasSize(1)
.value(books -> {
ReadBookResponse first = books.get(0);
org.junit.jupiter.api.Assertions.assertEquals("Reactive Synapse", first.getTitle());
org.junit.jupiter.api.Assertions.assertEquals("synapse-it", first.getAuthor());
});

UpdateBookRequest update = new UpdateBookRequest();
update.setTitle("Reactive Synapse");
update.setAuthor("synapse-it");
update.setNumberOfCopies(7);
web.put().uri("/v1/books")
.bodyValue(update)
.exchange()
.expectStatus().is2xxSuccessful();

// Verify the update landed in the data adapter
org.junit.jupiter.api.Assertions.assertEquals(
7,
bookRepository.findByTitleAndAuthor("Reactive Synapse", "synapse-it")
.block()
.getNumberOfCopies());

web.delete().uri("/v1/books/Reactive Synapse")
.exchange()
.expectStatus().isNoContent();

org.junit.jupiter.api.Assertions.assertEquals(0L, bookRepository.count().block());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2020 American Express Travel Related Services Company, Inc.
*
* 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
*
* http://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.
*/
package io.americanexpress.service.book.rest.integration;

import io.americanexpress.data.book.repository.BookRepository;
import io.americanexpress.service.book.rest.BookApplication;
import io.americanexpress.service.book.rest.model.CreateBookRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(classes = BookApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
class BookExceptionEnvelopeIT {

@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
BookMongoContainer.registerProperties(registry);
}

@Autowired
private WebTestClient web;

@Autowired
private BookRepository bookRepository;

@BeforeEach
void resetCollection() {
bookRepository.deleteAll().block();
}

@Test
void create_givenBlankRequiredFields_returnsBadRequest() {
// CreateBookRequest title and author are @NotBlank — sending blanks must be rejected.
CreateBookRequest invalid = new CreateBookRequest();
invalid.setTitle("");
invalid.setAuthor("");

web.post().uri("/v1/books")
.bodyValue(invalid)
.exchange()
.expectStatus().isBadRequest();
}

@Test
void create_givenMalformedJson_returnsBadRequest() {
web.post().uri("/v1/books")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{not-valid-json")
.exchange()
.expectStatus().is4xxClientError();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2020 American Express Travel Related Services Company, Inc.
*
* 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
*
* http://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.
*/
package io.americanexpress.service.book.rest.integration;

import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

/**
* Singleton MongoDB container shared across every IT in this module so Spring's
* test-context cache can reuse one application context for all suites.
*/
final class BookMongoContainer {

private static final MongoDBContainer INSTANCE =
new MongoDBContainer(DockerImageName.parse("mongo:7"));

static {
INSTANCE.start();
}

private BookMongoContainer() {}

static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", INSTANCE::getReplicaSetUrl);
registry.add("spring.data.mongodb.database", () -> "synapse_it");
}
}
Loading
Loading