Part 6: Tests
The test suite for the FF-ICE provider is organized in four layers:
| Class | Tests | What it validates |
|---|---|---|
EventExtractorTest | 6 | JAXB extraction — messageType, gufi, null safety |
FficeEventDeliveryServiceTest | 7 | Delivery engine — filter matching, fan-out, failure handling, metrics |
FficeEventProcessorTest | 7 | Ingress handler — validation, extraction, persistence, duplicate handling |
FficeProviderIT | 20 | Subscription Manager REST API — full lifecycle, access control, topics |
FficeMtlsConnectionIT | 3 | AMQPS / mTLS connectivity — SWIM-TIYP-0037, SWIM-TIYP-0033 |
FficeQueueSecurityIT | 4 | Per-queue access control — SWIM-TIYP-0030 (Mandatory Access Control) |
6.1 Unit tests
Test dependencies
Add the following dependencies to pom.xml before running these tests. They are needed for the mTLS and broker security integration tests in sections 6.3 and 6.4:
<dependency>
<groupId>com.github.swim-developer</groupId>
<artifactId>swim-framework-core</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.84</version>
<scope>test</scope>
</dependency>
EventExtractorTest
The EventExtractor reads from a parsed FficeMessageType JAXB object. Tests verify that the extractor returns the expected identifiers for each FF-ICE message type without starting a Quarkus context.
Create src/test/java/com/github/swim_developer/unit/EventExtractorTest.java:
class EventExtractorTest {
private static FficeUnmarshallerPool pool;
private EventExtractor extractor;
@BeforeAll static void initPool() { pool = new FficeUnmarshallerPool(); }
@BeforeEach void setUp() { extractor = new EventExtractor(); }
private FilterableEvent extractFirst(String xml) throws Exception {
FficeMessageType parsed = (FficeMessageType) pool.unmarshalAndValidate(xml);
List<Optional<FilterableEvent>> results = extractor.extract(parsed);
assertThat(results.getFirst()).isPresent();
return results.getFirst().get();
}
@Test void extractsMessageTypeFromFiledFlightPlan() throws Exception {
assertThat(extractFirst(loadXml("filed-flight-plan.xml")).messageType())
.isEqualTo("FILED_FLIGHT_PLAN");
}
@Test void extractsGufiFromFiledFlightPlan() throws Exception {
assertThat(extractFirst(loadXml("filed-flight-plan.xml")).gufi())
.isEqualTo("f47ac10b-58cc-4372-a567-0e02b2c3d479");
}
@Test void extractsMessageTypeFromFlightDeparture() throws Exception {
assertThat(extractFirst(loadXml("flight-departure.xml")).messageType())
.isEqualTo("FLIGHT_DEPARTURE");
}
@Test void extractsMessageTypeFromFlightArrival() throws Exception {
assertThat(extractFirst(loadXml("flight-arrival.xml")).messageType())
.isEqualTo("FLIGHT_ARRIVAL");
}
@Test void returnsEmptyOptionalForNullMessage() {
assertThat(extractor.extract(null).getFirst()).isEmpty();
}
}
FficeEventDeliveryServiceTest
Tests the delivery engine in isolation. Verifies that messageType filters match, non-matching subscriptions are skipped, multiple subscribers each receive the event, and broker failures are handled gracefully without crashing the delivery loop.
Key assertions:
- Subscription with
messageType=["FILED_FLIGHT_PLAN"]receives an event of typeFILED_FLIGHT_PLAN—matched=1, delivered=1 - Same subscription does NOT receive an event of type
FLIGHT_ARRIVAL—matched=0, no interaction with the AMQP publisher - Subscription with empty filter receives any event
- Two subscriptions each receive the same event —
matched=2, delivered=2 - When the broker throws,
failed=1with no exception propagation - The
ffice_events_delivered_totalcounter increments on success
FficeEventProcessorTest
Tests the ingress handler in isolation. Verifies every branch of IngressMessageHandler.processEvent():
- JAXB validation failure →
ffice_events_failed_total{reason="jaxb_validation_failed"}=1, no database calls - Extraction returns empty →
ffice_events_failed_total{reason="extraction_failed"}=1, no database calls - New event →
persist()is called,txSyncRegistry.registerInterposedSynchronization()dispatches for async delivery - Duplicate event →
update()is called, NOTpersist() - Database failure →
ffice_events_failed_total{reason="persistence_failed"}=1, no dispatch - All 7 FF-ICE message types each increment their own
ffice_events_received_total{type=...}counter
Run the unit tests:
./mvnw test # Linux / macOS
mvnw.cmd test # Windows
All 20 unit tests must pass.
6.2 Integration tests — REST API (FficeProviderIT)
Integration tests verify the provider's Subscription Manager REST API end-to-end. Quarkus DevServices starts a PostgreSQL container automatically. The Kafka and AMQP connectors are disabled in the test profile, so no external broker is needed.
Two infrastructure dependencies are mocked to isolate the REST layer:
JwtRoleValidator— validates JWT roles against the Artemis broker; mocked to return a test username and skip AMQ role checksQueueProvisioningStrategy— creates queues and security roles in Artemis; mocked withdoNothing()
Create src/test/java/com/github/swim_developer/integration/FficeProviderIT.java. The class is annotated with @QuarkusTest @TestMethodOrder @TestSecurity(user = "it-test-user", roles = "user"). A @BeforeEach method deletes all subscriptions, resets mocks, and stubs the expected interactions:
@BeforeEach
void setUp() {
QuarkusTransaction.requiringNew().run(() -> subscriptionStore.deleteAll());
reset(jwtRoleValidator, queueProvisioner);
when(jwtRoleValidator.getUsername()).thenReturn(TEST_USER);
doNothing().when(jwtRoleValidator).validateAmqRole(anyString());
doNothing().when(queueProvisioner).createQueue(anyString());
doNothing().when(queueProvisioner).addSecurityRole(anyString(), anyString(), anyString());
doNothing().when(queueProvisioner).removeQueue(anyString());
doNothing().when(queueProvisioner).removeSecurityRole(anyString());
}
The 20 tests cover:
@Order(1)—POST /swim/v1/subscriptionsreturns 201, statusPAUSED, queue name starts withFFICE-, verifies queue provisioner calls@Order(2)— subscription withmessage_typefilter persists and is returned in the response@Order(3)— invalid topic returns 400@Order(5)— idempotency: same request returns the samesubscription_idand queue@Order(6)—GET /swim/v1/subscriptions/{id}returns details@Order(7)— unknown id returns 404@Order(8)— list returns only current user's subscriptions@Order(9)—PUTwithACTIVEtransitions subscription and persists the new status@Order(10)—PUTwithPAUSEDtransitions back to paused@Order(11)—DELETEsoft-deletes (status becomesDELETED, row still present)@Order(12)— any update on a deleted subscription returns 400@Order(13)—PUT /{id}/renewextends thesubscription_enddate@Order(14)— renew on deleted subscription returns 400@Order(15)— delete non-existent returns 404@Order(16)— reading another user's subscription returns 403@Order(17)— deleting another user's subscription returns 403 and does not change its status@Order(20)—GET /swim/v1/topicsreturns configured topics includingFficeService@Order(21)—GET /swim/v1/topics/{id}returns 200 for existing topic@Order(22)— unknown topic returns 404@Order(30)—GET /swim/v1/subscriptions/pingreturns 200
6.3 Integration tests — mTLS connectivity (FficeMtlsConnectionIT)
This test validates AMQP over mTLS (AMQPS) as mandated by EUROCONTROL SPEC-170 (SWIM-TIYP-0037 — AMQP Transport Security Authentication). It uses ArtemisTlsContainer and TlsTestCertificateGenerator from the swim-framework-core test-jar to spin up a real TLS-enabled Artemis container.
All SWIM AMQP interfaces must use TLS mutual authentication. This test proves the provider's Artemis broker enforces needClientAuth=true — connections without a valid X.509 client certificate are rejected.
Create src/test/java/com/github/swim_developer/integration/FficeMtlsConnectionIT.java:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(TestNameLoggerExtension.class)
@Slf4j
class FficeMtlsConnectionIT {
private static TlsTestCertificateGenerator certs;
private static ArtemisTlsContainer artemis;
private static Vertx vertx;
@BeforeAll
static void setup() throws Exception {
certs = TlsTestCertificateGenerator.generateAll();
artemis = new ArtemisTlsContainer(certs);
artemis.start();
vertx = Vertx.vertx();
}
@AfterAll
static void teardown() {
if (artemis != null) artemis.close();
if (vertx != null) vertx.close();
}
@Test @Order(1)
void fficeProviderConnectsWithValidMtlsCertificate() throws Exception {
// Simulates the provider connecting with its EACP certificate
CompletableFuture<Void> connected = new CompletableFuture<>();
AmqpClient.create(vertx, buildTlsOptions(true)).connect().onComplete(conn -> {
if (conn.failed()) connected.completeExceptionally(conn.cause());
else { connected.complete(null); conn.result().close(); }
});
connected.get(15, TimeUnit.SECONDS); // throws if connection fails
}
@Test @Order(2)
void connectionIsRejectedWithoutClientCertificate() {
// No client cert = rejected. needClientAuth=true enforced.
CompletableFuture<Void> attempt = new CompletableFuture<>();
AmqpClient.create(vertx, buildTlsOptions(false)).connect().onComplete(conn -> {
if (conn.failed()) attempt.completeExceptionally(conn.cause());
else attempt.complete(null);
});
assertThatThrownBy(() -> attempt.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasCauseInstanceOf(Exception.class);
}
@Test @Order(3)
void fficeEventDeliveredOverMtlsChannel() throws Exception {
String fficeQueue = "FFICE-ansp1-sub999";
CompletableFuture<String> received = new CompletableFuture<>();
AmqpClient.create(vertx, buildTlsOptions(true)).connect().onComplete(conn -> {
var connection = conn.result();
connection.createReceiver(fficeQueue).onComplete(recv -> {
recv.result().handler(msg -> {
received.complete(msg.bodyAsString());
connection.close();
});
connection.createSender(fficeQueue).onComplete(send ->
send.result().send(AmqpMessage.create()
.withBody("FFICE-FILED-FLIGHT-PLAN").build()));
});
});
assertThat(received.get(15, TimeUnit.SECONDS))
.isEqualTo("FFICE-FILED-FLIGHT-PLAN");
}
private AmqpClientOptions buildTlsOptions(boolean includeClientCert) {
AmqpClientOptions opts = new AmqpClientOptions()
.setHost(artemis.getHost()).setPort(artemis.getAmqpsPort())
.setSsl(true).setUsername("admin").setPassword("admin")
.setHostnameVerificationAlgorithm("")
.setTrustOptions(new PfxOptions()
.setPath(certs.getTruststorePath().toString())
.setPassword(certs.getKeystorePassword()));
if (includeClientCert) {
opts.setKeyCertOptions(new PfxOptions()
.setPath(certs.getClientKeystorePath().toString())
.setPassword(certs.getKeystorePassword()));
}
return opts;
}
}
6.4 Integration tests — per-queue access control (FficeQueueSecurityIT)
This is the most important security test for a SWIM provider. It proves that even though ANSP2 holds a valid AMQP credential (simulating a valid JWT Bearer token in production), they cannot consume messages from a queue that belongs exclusively to ANSP1.
Mandatory access control must be enforced at every resource. The provider assigns each subscription an exclusive Artemis address (FFICE.<username>.<subscriptionId>) with a per-address security setting. Only the owner's role can consume. This test proves the enforcement is airtight — a valid token for the wrong user is rejected with amqp:unauthorized-access.
The test spins up a custom Artemis container built with an embedded broker.xml and pre-configured user/role properties that mirror exactly what AmqpQueueProvisioner applies at runtime via Jolokia when a subscriber creates a subscription.
Create src/test/java/com/github/swim_developer/integration/FficeQueueSecurityIT.java. Key design points:
buildBrokerXml()sets<security-enabled>true</security-enabled>and<security-invalidation-interval>0</security-invalidation-interval>, plus per-address<security-setting>rules forFFICE-ansp1-sub001andFFICE-ansp2-sub002buildEntrypoint()writesartemis-users.propertiesandartemis-roles.propertieswith roleansp1-swim-ffice-v1-amq-rolefor useransp1andansp2-swim-ffice-v1-amq-rolefor useransp2- Each receive helper closes the AMQP connection after the first message to prevent lingering receivers from consuming messages intended for subsequent tests
- The rejected test uses
.hasCauseInstanceOf(Throwable.class)because the Vert.x AMQP client wraps the AMQP error asNoStackTraceThrowable, which extendsThrowablebut notException
The four tests prove:
| Order | Test | What it proves |
|---|---|---|
@Order(1) |
subscriberReceivesEventsFromOwnDedicatedQueue |
ANSP1 receives a message published by admin to FFICE-ansp1-sub001 |
@Order(2) |
unauthorizedSubscriberIsRejectedWithAmqpError |
ANSP2 tries to receive from FFICE-ansp1-sub001 — Artemis replies with amqp:unauthorized-access |
@Order(3) |
eachSubscriberAccessesOnlyTheirOwnQueue |
ANSP2 receives from their own FFICE-ansp2-sub002 without issue |
@Order(4) |
providerRoutesEventsToMatchingQueuesOnly |
After a new publish to ANSP1's queue, ANSP1 receives; ANSP2 is still rejected (consistent enforcement) |
Run the full test suite
Run all integration tests together:
./mvnw verify -DskipITs=false # Linux / macOS
mvnw.cmd verify -DskipITs=false # Windows
Expected output:
[INFO] Tests run: 20, Failures: 0, Errors: 0, Skipped: 0 (FficeProviderIT)
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 (FficeMtlsConnectionIT)
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 (FficeQueueSecurityIT)
[INFO]
[INFO] BUILD SUCCESS
All 47 tests must pass: 20 unit tests + 27 integration tests (20 REST API + 3 mTLS + 4 queue security).
What you built
In this tutorial you created a complete SWIM provider for FF-ICE messages. The framework handled the majority of the work — you only wrote three service-specific classes:
| File | Lines of code | What it does |
|---|---|---|
JaxbUnmarshallerPool | ~30 | Delegates XML unmarshalling to swim-fixm-ffice-model |
EventExtractor | ~60 | Reads messageId, gufi, messageType from a parsed JAXB object |
IngressMessageHandler | ~80 | Orchestrates validate → extract → persist → dispatch |
Everything else — the Subscription Manager REST API, AMQP outbox delivery loop, heartbeat publisher, subscription expiry scheduler, JWT validation, queue provisioning, OpenTelemetry tracing, Prometheus metrics, health checks, and the per-queue security enforcement in Artemis — is provided by swim-framework-provider.
The test suite proves three compliance guarantees required by EUROCONTROL SPEC-170:
- SWIM-TIYP-0037 — AMQP Transport Security Authentication (mTLS enforced)
- SWIM-TIYP-0033 — TLS Server Authentication (connections without client cert rejected)
- SWIM-TIYP-0030 — Mandatory Access Control (per-queue role enforcement blocks unauthorized consumers)