# MQTT test fixture Coverage map + gap inventory for the MQTT driver's harness. **TL;DR:** the unit suite (266 tests) carries the mapping, indexing and failure-classification logic; the live suite (7 tests) proves the parts only a real broker can prove — TLS, real authentication, CA pinning in both directions, retained-message seeding, and that a rejected CONNACK is actually surfaced. Sparkplug B has **no fixture at all** — it is unimplemented (P2). ## What the fixture is `eclipse-mosquitto:2.0.22` plus a second container of the same image running `publisher/publish.sh` (the image ships `mosquitto_pub`, so no bespoke image is needed). Compose lives at `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/`; the deployed stack is `/opt/otopcua-mqtt` on the shared Docker host. **Two authenticated listeners, no anonymous fallback** (`allow_anonymous false`) — deliberate: an anonymous broker would let a broken auth config pass every test. | Listener | Endpoint | Env var | |---|---|---| | TLS + auth | `10.100.0.35:8883` | `MQTT_FIXTURE_ENDPOINT` | | plaintext + auth | `10.100.0.35:1883` | `MQTT_FIXTURE_PLAIN_ENDPOINT` | `MqttFixture` gates the suite on `MQTT_FIXTURE_ENDPOINT` / `MQTT_FIXTURE_USERNAME` / `MQTT_FIXTURE_PASSWORD` (+ `MQTT_FIXTURE_CA_CERT`, `MQTT_FIXTURE_TOPIC_PREFIX`) and **skips cleanly** when they are absent, so the suite is safe to run offline. **Unlike every other fixture it needs generated material first** — `./gen-fixture-material.sh` writes the gitignored `secrets/` (password file, CA, server cert/key). The broker will not start without it. Bring-up and the CA-fetch recipe are in [`infra/README.md`](../../infra/README.md) §3. ### Published topics | Topic | Shape | Retain | Why it exists | |---|---|---|---| | `otopcua/fixture/retained/seed` | `{"value":42.5,…}` | ✅ **published exactly once** | The only way to prove retained *seeding*. On a republished topic a retained seed is indistinguishable from a fast live publish | | `otopcua/fixture/line1/temperature` | `{"value":…,"unit":"C","seq":n}` | ✅ | JSON + JSONPath extraction | | `otopcua/fixture/line1/counter` | `{"value":n}` | ✅ | Monotonic, for ordering | | `otopcua/fixture/line1/state` | `RUNNING` (bare text) | ✅ | Non-JSON `Raw`/`Scalar` payloads | | `otopcua/fixture/noise/chatter` | JSON | ❌ | **Never authored as a tag** — a broker carrying unauthored traffic is the normal case and the driver must stay silent about it | ## What it actually covers The 7 live tests: TLS+auth connect; connect pinned to the fixture CA; connect pinned to a *foreign* CA is **rejected** (the falsifiability control — a pinning test that only ever passes proves nothing); wrong password is rejected **and surfaced** (the MQTTnet-5 silent-`Healthy` defect); plain subscribe→value under a RawPath; retained message seeds a value on subscribe; and an unauthored topic stays silent while an authored tag keeps flowing. Live-gated end-to-end (2026-07-24, P1 milestone): deployed on the docker-dev rig against this fixture, both tags resolved and served changing values through OPC UA, and the AdminUI `#`-observation picker rendered the real topic tree. ## What it does NOT cover ### 1. Sparkplug B — entirely No edge-node simulator exists. Birth/alias/seq-gap/rebirth/death are P2 (Tasks 15–26) and the project-owned C# simulator is part of that work. ### 2. Broker-side failure injection No test kills the broker mid-session, partitions the network, or forces a SUBACK failure against a real broker. Reconnect, backoff and per-topic degradation are **unit-proven only**. ### 3. Wildcard tags against a live broker The by-filter indexing fix that keeps wildcard tags alive across a reconnect is covered by unit tests; no live test authors a `+`/`#` tag and bounces the connection. ### 4. QoS 2 delivery semantics The fixture publishes at QoS 0/1. QoS 2's exactly-once handshake is never exercised end-to-end. ### 5. Scale One publisher, five topics, a ~2 s cadence. Nothing probes the 50 000-node browse cap, the 1 MiB payload cap, or thousands of authored tags. ### 6. Redundant-pair client-id collision Found by hand during the P1 gate (a fixed `clientId` makes pair nodes evict each other), **not** by a test. No automated coverage asserts that two concurrent drivers coexist. ## When to trust the MQTT fixture, when to reach for a real broker | Question | Fixture answers it? | |---|---| | Does TLS + auth work, and does bad auth fail loudly? | ✅ yes | | Is a retained message seeded on subscribe? | ✅ yes | | Does JSONPath extraction + coercion behave? | ✅ yes (unit + live) | | Does the driver ignore unauthored traffic? | ✅ yes | | Does it survive a broker restart / flapping link? | ❌ unit-only | | Does it hold up at plant tag counts? | ❌ no | | Anything Sparkplug B | ❌ not implemented | ## Follow-up candidates 1. Broker-restart / SUBACK-failure live tests (closes gap 2). 2. A live wildcard-across-reconnect test (closes gap 3). 3. A concurrent-two-driver test pinning the client-id hazard (closes gap 6). 4. The Sparkplug edge-node simulator + its rebirth/seq-gap/death matrix (P2). ## Key fixture / config files - `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml` - `tests/Drivers/…/Docker/mosquitto.conf` · `Docker/gen-fixture-material.sh` · `Docker/publisher/publish.sh` - `tests/Drivers/…/MqttFixture.cs` · `PlainMqttLiveTests.cs` - [`infra/README.md`](../../infra/README.md) §3 — bring-up, endpoints, CA fetch