Testing

Test scaffolding for embedders: fixtures, the interop matrix, mock endpoints, and the escape hatches gated behind the testing feature.

On this page

Overview

ASX has a multi-layer test strategy:

LayerLocationScope
Unit testssrc/** (#[cfg(test)])Individual functions, edge cases
Integration teststests/End-to-end protocol flows, concurrency
Property teststests/profile_property_invariants.rsRandomized profile stack invariants
Interop matrixtests/fixtures/interop/Governed fixture corpus across strict/relaxed modes
WS-Security vectorstests/wssec_c14n_vectors.rs, tests/wssec_strict_matrix.rsC14N golden vectors, strict signature/reference verification
Session isolationtests/session_isolation_concurrency.rsPer-session policy isolation under concurrency
Fuzz / adversarialartifacts/fuzz/Adversarial inputs to profile loader, policy resolver, wire parser
Performance gatextask/Regression detection against baseline ns/op values

Running the Full Test Suite

cargo test --all-features

Runs all 913+ unit and integration tests across all test suites. Zero failures expected.

Run specific feature combinations:

cargo test --features "as2,testing"
cargo test --features "as4,testing,server"
cargo test --features "as2,as4,testing,server"

Integration Test Suites

AS2 flows

cargo test --all-features as2_send_golden
cargo test --all-features as2_receive_mdn

AS4 flows

cargo test --all-features as4_push_flow
cargo test --all-features as4_pull_flow

Covers: SOAP envelope construction, WS-Security signing (RSA + ECDSA) and verification, AES-128-GCM encrypt/decrypt (RSA-OAEP and ECDH-ES + ConcatKDF + AES-128-KW), pull store enqueue/dequeue, Two-Way MEP correlation, Test Service detection.


Testing Helpers (testing feature)

The testing feature enables a set of utilities that make it possible to write AS4 integration tests without real X.509 PKI material (BDEW WIRK certificates, PEPPOL production PKI, etc.).

Security: The testing feature is blocked by compile_error! in release profile builds. It must never appear in production binaries.

InsecureBypassAs4Verifier

Skips all WS-Security checks on inbound AS4 push messages. Parity with InsecureBypassTrustVerifier on the AS2 side.

[dev-dependencies]
asx-rs = { version = "0.14", features = ["as4", "testing"] }
use asx_rs::as4::{
    InsecureBypassAs4Verifier,
    receive_push_with_dedup_async_with_custom_verifier,
    As4ReceivePushRequest,
};
use std::sync::Arc;

let outcome = receive_push_with_dedup_async_with_custom_verifier(
    &session, &bus, request, dedup_backend,
    InsecureBypassAs4Verifier,  // ← bypasses ALL WS-Security verification
).await?;

When active, a tracing::warn! is emitted so test logs are auditable and production log scraping can detect accidental non-test usage.

MockAs4Endpoint

An in-process HTTP AS4 server that accepts any push message (signed or unsigned, encrypted or plain), records received messages in an async channel, and returns a synchronous AS4 receipt. Requires testing + server features.

[dev-dependencies]
asx-rs = { version = "0.14", features = ["as4", "testing", "server"] }
use asx_rs::as4::mock_endpoint::MockAs4Endpoint;
use tokio::time::{timeout, Duration};

// Simple case: bind to a random OS-assigned port — no PKI certificates needed.
let endpoint = MockAs4Endpoint::bind("127.0.0.1:0").await.expect("bind");

// Full sign+encrypt round-trip: configure a decryption key via builder.
// The mock will decrypt ECDH-ES / RSA-OAEP inbound messages automatically.
let endpoint = MockAs4Endpoint::builder()
    .with_decryption_key_pem(my_ec_or_rsa_private_key_pem)
    .bind("127.0.0.1:0")
    .await
    .expect("bind");

// Full NRR round-trip: make the mock answer with a *signed* receipt whose
// MessagePartNRInformation echoes the inbound signature's digests, so
// `verify_sync_response` under `As4ReceiptPolicy::regulated()` has real
// evidence to check.  Without this the mock replies with an unsigned receipt
// carrying an empty <NonRepudiationInformation/>, which `regulated()` rejects.
let endpoint = MockAs4Endpoint::builder()
    .with_receipt_signing_material(receiver_cert_pem, receiver_key_pem)
    .bind("127.0.0.1:0")
    .await
    .expect("bind");

let url = endpoint.local_url(); // "http://127.0.0.1:PORT/as4/inbox"

// Send an AS4 message to `url` using any AS4 client...

// Wait for the first message (returns None if the endpoint is dropped).
let msg = timeout(Duration::from_secs(5), endpoint.next_received())
    .await
    .expect("timed out")
    .expect("endpoint closed");

assert_eq!(msg.action, "urn:bdew:as4:service:UTILMD");
assert_eq!(msg.from_party_ids, &["9900000000001"]);
assert!(!msg.payload.is_empty());

// Alias for ergonomics (matching feedback API):
let msg = endpoint.next_message().await;

// Drain all messages already received without waiting:
let all = endpoint.drain_received().await;

MockReceivedMessage fields:

  • action<eb:Action> value
  • service<eb:Service> value, if present
  • message_id<eb:MessageId>
  • from_party_ids — all <eb:From/eb:PartyId> values (contains the sender GLN)
  • to_party_ids — all <eb:To/eb:PartyId> values (contains the receiver GLN)
  • conversation_id<eb:ConversationId>, if present
  • ref_to_message_id<eb:RefToMessageId> (Two-Way MEP correlation)
  • payload — decrypted, de-SBDH-stripped business payload bytes

Party ID population: for a typical BDEW send where the session session_id is the sender GLN and partner_id is the receiver GLN, from_party_ids[0] == sender GLN and to_party_ids[0] == receiver GLN, as written by SoapEnvelopeBuilder.

EventBus::new_for_testing()

A zero-config BestEffort event bus that never fails on emit when no broadcast subscriber is active. Use this in integration tests that do not assert on protocol events.

use asx_rs::observability::EventBus;

// Requires the `testing` feature. Equivalent to:
// EventBus::builder().capacity(256).emission_mode(EventEmissionMode::BestEffort).build()
let bus = EventBus::new_for_testing();

Production note: new_for_testing() silently discards all protocol events and audit records. For production use, EventBus::new(capacity) (strict transactional) or EventBus::new_regulated(capacity, audit_sink) are the correct APIs.

As4HttpTransport::new_for_localhost_testing()

An HTTP transport that bypasses SSRF validation and HTTPS-only enforcement, enabling integration tests to POST to MockAs4Endpoint at http://127.0.0.1:… using the same As4HttpTransport code path as production.

use asx_rs::transport::egress::As4HttpTransport;

let transport = As4HttpTransport::new_for_localhost_testing()?;
let outcome = transport.send_to_localhost(&endpoint.local_url(), &output).await?;
assert!(outcome.is_success());

To cover receipt verification too, use the verifying counterpart. The sending session must pin the mock's receipt-signing certificate (via cert_handle.fingerprint_sha256, or As4ReceiptPolicy::with_expected_signer_fingerprint):

use asx_rs::as4::As4ReceiptPolicy;

let outcome = transport
    .send_and_verify_to_localhost(
        &endpoint.local_url(), &session, &bus, &output,
        &As4ReceiptPolicy::regulated(),
    )
    .await?;

assert!(outcome.into_receipt()?.is_non_repudiation_evidence());

This keeps test coverage over As4HttpTransport's Content-Type headers, receipt inspection, and connection pooling — none of which are exercised by a raw reqwest::Client.

DurableInMemoryDedupBackend

An in-memory TtlDedupStorage wrapper that advertises is_durable() = true, allowing it to pass the strict durable-backend guard that fires at production receive entry points.

use asx_rs::storage::DurableInMemoryDedupBackend;
use std::sync::Arc;

let dedup: Arc<dyn asx_rs::storage::DedupStorage> = Arc::new(
    DurableInMemoryDedupBackend::new(std::time::Duration::from_secs(3600)),
);

Self-signed keypair generators

Generate minimal self-signed X.509 certificates for test use. Eliminates the need for downstream crates to add openssl or rcgen as dev-dependencies.

use asx_rs::fixtures::{EcCurve, generate_self_signed_ec_keypair, generate_self_signed_rsa_keypair};

// EC keypairs — for ECDSA signing and/or ECDH-ES encryption:
let (cert_pem, key_pem) = generate_self_signed_ec_keypair("test-ap", EcCurve::BrainpoolP256r1);
let (cert_pem, key_pem) = generate_self_signed_ec_keypair("peppol-ap", EcCurve::P256);
let (cert_pem, key_pem) = generate_self_signed_ec_keypair("p384-ap",  EcCurve::P384);

// RSA keypair — for RSA-SHA256 signing and/or RSA-OAEP encryption:
let (cert_pem, key_pem) = generate_self_signed_rsa_keypair("rsa-ap", 2048);

Supported EcCurve variants:

VariantOIDProfiles
P2561.2.840.10045.3.1.7PEPPOL, general AS4
P3841.3.132.0.34Higher-assurance
P5211.3.132.0.35Higher-assurance
BrainpoolP256r11.3.36.3.3.2.8.1.1.7BDEW AS4-Profil / BSI TR-03116-3
BrainpoolP384r11.3.36.3.3.2.8.1.1.11BSI

Generated certificates have:

  • KeyUsage (critical): digitalSignature + keyAgreement (EC) or keyEncipherment (RSA)
  • BasicConstraints (critical): CA:FALSE
  • Validity: 10 years
  • Self-signed with SHA-256

Custom As4Verifier implementations

Under testing, the As4Verifier sealed trait becomes implementable by external crates via the verifier_seal re-export:

use asx_rs::as4::{As4Verifier, verifier_seal, types::As4PushPolicy};
use asx_rs::core::{Result, SessionContext};

struct RecordingVerifier {
    calls: std::sync::atomic::AtomicUsize,
}

impl verifier_seal::Sealed for RecordingVerifier {}

impl As4Verifier for RecordingVerifier {
    fn verify_security(
        &self,
        _session: &SessionContext,
        _policy: &As4PushPolicy,
        _soap_xml: &str,
        _soap_doc: &roxmltree::Document<'_>,
        _message_id: &str,
        _external_reference: Option<(&str, &[u8])>,
    ) -> Result<()> {
        self.calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        Ok(())
    }
}

Interop Fixture Repository

The interop fixture corpus governs AS2 MIME and AS4 SOAP strict/relaxed flows with declared expected outcomes.

Fixture catalog

Location: tests/fixtures/interop/catalog.json

Schema (schema_version: "1.0"):

{
  "schema_version": "1.0",
  "fixtures": [
    {
      "fixture_id": "as2-strict-001",
      "protocol": "As2Mime",
      "mode": "Strict",
      "grouping": {
        "partner_id": "partner-a",
        "profile_name": "strict-edelivery",
        "protocol_stage": "send"
      },
      "payload_path": "partner-a/strict/send/payload.mime",
      "expected_outcome": "SuccessConfirmed",
      "reason_annotations": ["RFC 4130 §6 compliant headers, signed"]
    }
  ]
}

Required coverage

The catalog must contain at least one fixture for each combination:

  • As2Mime/Strict
  • As2Mime/Relaxed
  • As4Soap/Strict
  • As4Soap/Relaxed

Validate the repository

cargo run -p xtask -- fixture-repo-validate tests/fixtures/interop/catalog.json

Validation checks: schema version, non-empty fixture set, unique IDs, non-empty grouping metadata, non-empty reason annotations, payload file existence, protocol-specific file extension (.mime for AS2, .xml for AS4).


Interop Matrix Executor

Runs all interop fixtures across policy/profile combinations and produces a machine-readable MatrixSummary:

cargo run -p xtask --all-features -- interop-matrix \
  tests/fixtures/interop/catalog.json \
  tests/fixtures/interop/quarantine.json \
  3    # iteration count for flake detection
# or:
scripts/run_interop_matrix.sh

MatrixSummary output includes per-fixture pass/fail, observed error code, flakiness status, and quarantine owner.

Quarantine policy

Flaky fixtures are allowed in CI only when listed in tests/fixtures/interop/quarantine.json with an owner assignment. Unquarantined flaky fixtures are blocking. The matrix runner exits non-zero when:

  • Any fixture fails
  • Any fixture is flaky without a quarantine entry

WS-Security Canonicalization Golden Vectors

tests/wssec_c14n_vectors.rs validates the custom Exclusive C14N implementation against deterministic golden vectors:

cargo test --all-features wssec_c14n_vectors
cargo test --all-features wssec_strict_matrix
# Run as explicit gate:
scripts/run_wssec_vector_gate.sh
# or:
cargo run -p xtask --all-features -- wssec-vector-gate

Covered scenarios:

  • Strict canonicalization against golden vector file
  • Signature reference verification for a signed fixture
  • Wrapped reference URI rejection under strict URI normalization rules
  • Whitespace-preserving digest mismatch rejection under strict canonicalization rules
  • Namespace propagation, attribute ordering, text/attribute escaping
  • PI node forwarding, comment stripping, comment preservation
  • InclusiveNamespaces PrefixList with ancestor binding rendering

Vector mismatch output uses canonical_vector_diff(expected, actual) — deterministic line-based diffs with expected/actual markers for reproducible triage.


Session Isolation and Concurrency

tests/session_isolation_concurrency.rs validates session-scoped policy isolation under concurrent execution:

cargo test --all-features session_isolation_concurrency

Covered:

  • Strict and relaxed session pairs executing concurrently without policy leakage
  • Session-scoped exception behavior remains isolated
  • Cross-session contamination attempts fail
  • Per-session event ordering validated for critical audit/signing sequences
  • AS2 concurrent strict-vs-relaxed MDN boundary-quirk flow
  • AS4 concurrent strict-vs-relaxed UserMessage parse flow

Property Tests

tests/profile_property_invariants.rs uses randomized inputs to verify profile stack invariants:

cargo test --all-features profile_property_invariants

Covered invariants:

  • Deterministic resolution stability under randomized layer combinations (same input always produces same output)
  • Monotonic precedence for partner overlays (last applicable partner layer wins)
  • Fail-fast validation for malformed/conflicting policy combinations

Fuzz and Adversarial Testing

The adversarial fuzz gate runs seeded adversarial cases over three targets:

  1. Profile loaderRegionalProfilePack::from_json + regional pack application
  2. Policy resolverProfileStack::validate + resolve determinism
  3. Wire parsingWireEnvelope::from_http_request_with_limits, stream bounded reads, transfer fingerprinting
scripts/run_fuzz_gate.sh 4000 2500 artifacts/fuzz
# or:
cargo run -p xtask --all-features -- fuzz-gate 4000 2500 artifacts/fuzz

Arguments: [iterations] [budget_ms] [output_dir]

Fail conditions:

  • Any panic
  • Determinism violation (different output for same input)
  • Missing remediation hints or empty error messaging
  • Stream/accounting mismatch

Reproducer handling: On failure, the gate minimizes the input payload by deterministic truncation and stores a reproducer in artifacts/fuzz/reproducers/ as JSON with base64 bytes. CI uploads artifacts/fuzz/ as a triage artifact.


Performance Gate

The authoritative baseline is benches/perf-baseline.txt, measured on the CI Linux runner in release. It is not restated here: two copies of the same numbers drift, and the file is the one the gate reads.

Only benchmarks the gate's feature set compiles may appear in it — an entry with no matching measurement is a gate failure, not a skip. The receive-path benchmarks sit behind testing, which is a compile_error! in release, so they run under a debug just perf-gate and are not baselined.

CI enforces a 25 % maximum regression. The values are environment-relative; they are not hardware claims.

Run the performance gate:

just perf-gate

The feature list is explicit rather than --all-features, because the crate refuses to compile a --release build with testing — so cargo run --release -p xtask --all-features does not build:

FEATURES="as2,as4,client,server,trace,compression,async-ocsp,interop-strict"

# Check against the baseline (fails on a >25% regression):
cargo run --release -p xtask --no-default-features --features "$FEATURES" -- \
  perf-gate --iterations 2000 --check-baseline benches/perf-baseline.txt --max-regression 0.25

# Record a new one:
cargo run --release -p xtask --no-default-features --features "$FEATURES" -- \
  perf-gate --iterations 2000 --write-baseline benches/perf-baseline.txt

Transport Server Tests (No Network)

Server handler tests use tower::ServiceExt::oneshot — no listening socket required:

[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
use tower::ServiceExt;

let response = as2_router(Arc::new(handler), "/as2/receive")
    .oneshot(request)
    .await
    .unwrap();
assert_eq!(response.status(), 200);

12 server integration tests ship with the crate and run as part of cargo test --all-features.


Testing Feature Flag

asx-rs = { version = "0.14", features = ["testing"] }

The testing feature exposes asx_rs::fixtures and asx_rs::matrix — test scaffold modules with InteropFixtureMetadata, FixtureCatalog, MatrixSummary, and related helpers. These are not part of the production library surface and are absent from builds without this feature.