Step 7 of 7

Part 6: Tests

The test suite for the FF-ICE provider is organized in four layers:

ClassTestsWhat it validates
EventExtractorTest6JAXB extraction — messageType, gufi, null safety
FficeEventDeliveryServiceTest7Delivery engine — filter matching, fan-out, failure handling, metrics
FficeEventProcessorTest7Ingress handler — validation, extraction, persistence, duplicate handling
FficeProviderIT20Subscription Manager REST API — full lifecycle, access control, topics
FficeMtlsConnectionIT3AMQPS / mTLS connectivity — SWIM-TIYP-0037, SWIM-TIYP-0033
FficeQueueSecurityIT4Per-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 type FILED_FLIGHT_PLANmatched=1, delivered=1
  • Same subscription does NOT receive an event of type FLIGHT_ARRIVALmatched=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=1 with no exception propagation
  • The ffice_events_delivered_total counter 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, NOT persist()
  • 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 checks
  • QueueProvisioningStrategy — creates queues and security roles in Artemis; mocked with doNothing()

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/subscriptions returns 201, status PAUSED, queue name starts with FFICE-, verifies queue provisioner calls
  • @Order(2) — subscription with message_type filter persists and is returned in the response
  • @Order(3) — invalid topic returns 400
  • @Order(5) — idempotency: same request returns the same subscription_id and 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)PUT with ACTIVE transitions subscription and persists the new status
  • @Order(10)PUT with PAUSED transitions back to paused
  • @Order(11)DELETE soft-deletes (status becomes DELETED, row still present)
  • @Order(12) — any update on a deleted subscription returns 400
  • @Order(13)PUT /{id}/renew extends the subscription_end date
  • @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/topics returns configured topics including FficeService
  • @Order(21)GET /swim/v1/topics/{id} returns 200 for existing topic
  • @Order(22) — unknown topic returns 404
  • @Order(30)GET /swim/v1/subscriptions/ping returns 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.

Yellow Profile compliance: SWIM-TIYP-0037

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.

Yellow Profile compliance: SWIM-TIYP-0030

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 for FFICE-ansp1-sub001 and FFICE-ansp2-sub002
  • buildEntrypoint() writes artemis-users.properties and artemis-roles.properties with role ansp1-swim-ffice-v1-amq-role for user ansp1 and ansp2-swim-ffice-v1-amq-role for user ansp2
  • 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 as NoStackTraceThrowable, which extends Throwable but not Exception

The four tests prove:

OrderTestWhat 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:

FileLines of codeWhat it does
JaxbUnmarshallerPool~30Delegates XML unmarshalling to swim-fixm-ffice-model
EventExtractor~60Reads messageId, gufi, messageType from a parsed JAXB object
IngressMessageHandler~80Orchestrates 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)