07a4a5ff5e
Task 14 — the P1 milestone gate. Ran the live /run verification on a docker-dev
rig against the real Mosquitto TLS+auth fixture, and recorded the result.
The gate did its job: it found MQTT was unauthorable in production.
RawDriverTypeDialog's driver-type list is a hand-maintained array that nobody
added MQTT to, so the /raw "New driver" picker never offered it — with every
other layer (contracts, driver, browser, factory, host registration, typed tag
editor) complete and 266 green unit tests. Nothing tied that array to
DriverTypeNames and AdminUI has no bUnit, so no offline test could see it.
Fixed, plus RawDriverTypeDialogParityTests: a reflection parity guard that
fails for ANY DriverTypeNames entry missing from the picker. Verified
falsifiable — RED with "…cannot be authored from the /raw New-driver picker,
so they are unreachable in production: Mqtt" before the one-line fix.
A second gap is left OPEN and documented, not fixed (no task ever covered it):
MqttDriverForm / MqttDeviceForm do not exist, and DriverConfigModal/DeviceModal
have no raw-JSON fallback — so an operator can create an MQTT driver but cannot
author its broker endpoint or credentials. P1 is not operator-complete until
those two razor forms land. The gate authored config via SQL to get past it.
Live gate result (isolated 2-node MAIN stack, image built from this branch,
fixture at 10.100.0.35:8883, AllowUntrustedServerCertificate rather than
pinning the CA into the rig):
- typed MQTT tag editor renders and round-trips; Json shows the JSON-path
field, Raw/Scalar hide it; wildcard topic a/+/c blocked at save; data-type
list offers Float64 and no Double; qos/retainSeed absent on "(driver
default)" and qos:2 written as a number; isHistorized/historianTagname
survive a topic-only edit; SparkplugB renders the P2 placeholder and
switching back keeps Plain values; jsonPath is guidance not a gate in all
three shapes ($ seeded only for a brand-new blank tag, blank stays absent,
clearing saves fine)
- deployed, and both editor-written blobs resolved and served changing live
values through OPC UA (22.8 / 1629, StatusCode Good) — no BadNodeIdUnknown
- the bespoke #-observation browser drove a real broker session and rendered
the fixture's actual topic tree (line1/{counter,state,temperature},
noise/chatter, retained/seed); a Modbus device's picker still correctly
reports "Browsing unavailable"
Also found live and documented (not a code change): both nodes of a redundant
pair run the driver, so a FIXED clientId makes them evict each other forever —
the broker logs "already connected, closing old connection" and both nodes
reconnect every ~2s while still reporting Healthy. Unset (the default) is
correct; confirmed 0 reconnects/60s on both nodes after removing it.
Suites: offline 1134 passed / 0 failed (266 Driver.Mqtt + 730 AdminUI incl. the
3 new guards + 138 Core.Abstractions); live 7/7 against the fixture.
Docs: new docs/drivers/Mqtt.md + Mqtt-Test-Fixture.md (the per-driver + fixture
convention every other driver follows), MQTT rows in docs/drivers/README.md,
TestConnectProbes.md, root README.md, and infra/README.md §3.
CLAUDE.md drift corrected while adding the MQTT endpoints:
- the "every fixture carries project: lmxopcua" claim was false — only the
MQTT fixture does; the filter returns one stack, not the fleet
- lmxopcua-fix.ps1 is Windows-VM-only; on macOS drive the host over ssh/rsync
(and the docker-dev rig itself runs locally under OrbStack)
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
105 lines
5.4 KiB
Markdown
105 lines
5.4 KiB
Markdown
# 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
|