290117660d
- deferment.md: node ACLs no longer authored-but-inert (#520 shipped through stage 7, opt-in Security:Acl:Enabled); failure-visibility trio mostly closed (01/S-1 + 03/S4 fixed, 06/S-1 partial); bulkhead + WriteIdempotentAttribute closed by R2-02; Sql Postgres+Oracle dialects shipped (d54f3c83); akka-hosting partials verified superseded - archreview/00-OVERALL.md: staleness banner on the carried-open block (04/C-1→R2-05, 03/S2+S3→R2-08, 03/P1→R2-07, driver fleet→R2-09/R2-11/#457, hygiene→R2-12, trio per above; what genuinely remains from theme 4) - archreview/plans/STATUS.md: carved out the true exceptions to "every Round-2 live gate GREEN" (R2-03 T11/T12 deferred-live, R2-10 T10 breaker-OPEN never forced); #459 closed by mesh Phase 7 drill D4 (c50ebcf7) - archreview/plans/INTEGRATION-SWEEP-STATUS.md: "follow-ups unstarted" → all 7 shipped as PRs #446–#452 - archreview/06-gateway-integrations.md: 06/S-1 STILL OPEN → PARTIAL (fail-closed advise + unconfirmed meter; residual cross-repo) - 2026-07-31-apikey-acl-enforcement-design.md: status → implemented through stage 7, #520 CLOSED 2026-08-01 - 2026-07-21-per-cluster-mesh-design.md: "Phases 1–7 not started" → PROGRAM COMPLETE 2026-07-24 (Phase 7c50ebcf7) - 2026-07-20-localdb-phase2-live-gate.md: merged as 06b5d81b; open design question shipped by mesh Phase 6 (checks 3/4 still never re-run on the three-mesh topology) - 2026-07-20-localdb-phase1-progress.md: Tasks 10–16 REMAIN → Phase 1 complete, live-gated 8/8, mergedc957db52- 2026-08-06-clusterclient-to-grpc-tracking.md: defect 4 OPEN → fixed byc0475f2d+e54883a8, merged @b360239e- 2026-08-06-clusterclient-to-grpc.md.tasks.json: Task 15 pending → completed (Phase 4 gate PASS 8/8 @0b194003, recordd0ac6fc5) - 2026-07-22-mesh-phase2-live-gate.md: DriverStatusPanel orphan → wired into /hosts by #546 (9420f8ef) - 2026-07-21-issue-485-live-gate.md: pointer from the "NOT fixed here" finding to the #486 fix (DriverHostActor.cs:1696-1717 + FetchAndCache mirror) - 2026-08-05-sql-postgres-oracle.md: chrome/AdminUI gate not deferred — PASSED 2026-08-05, found + fixed the Test-Connect provider gate (4e461951) - v2-ga-lab-gates-plan.md / v2-release-readiness.md / live-hardware-validation-runbooks.md: FOCAS live-CNC smoke #54 RESOLVED on a real 31i-B 2026-06-25 (cnc_rdparam caveat preserved) - Mqtt-Test-Fixture.md: STATE/DCMD "not implemented" → shipped + live-gated (#5082a7d1167, #511857a10b4) - 2026-08-01-continuous-historization-live-gate.md: poison-tag FIFO pin "filed, not fixed" → #538 fixed same day (e7fbe977, gate 4bdce910; Capacity default now 500k) - 2026-05-26-akka-hosting-alignment-plan.md.tasks.json: F8/F9/F10/F13/F14 partial → completed, superseded by the v3 address-space rework (Phase7Composer/ScriptedAlarmActor deleted; evaluators/SDK sinks/security policies production-wired) Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
134 lines
7.6 KiB
Markdown
134 lines
7.6 KiB
Markdown
# MQTT test fixture
|
|
|
|
Coverage map + gap inventory for the MQTT driver's harness.
|
|
|
|
**TL;DR:** the unit suite (581 tests) carries the mapping, indexing, Sparkplug state-machine and
|
|
failure-classification logic; the live suite (15 tests) proves the parts only a real broker can prove
|
|
— TLS, real authentication, CA pinning in both directions, retained-message seeding, that a rejected
|
|
CONNACK is actually surfaced, and the Sparkplug birth/alias/rebirth/death path against a real
|
|
edge-node simulator.
|
|
|
|
## 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), plus — behind the `sparkplug`
|
|
compose profile — **`otopcua-sparkplug-sim`**, a project-owned C# Sparkplug-B edge-node simulator
|
|
(`tests/Drivers/….Driver.Mqtt.IntegrationTests/SparkplugSimulator/`). 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.
|
|
|
|
### The Sparkplug simulator
|
|
|
|
Group **`OtOpcUaSim`**; edge nodes **`EdgeA`** and **`EdgeB`**; `EdgeA` additionally publishes device
|
|
**`Filler1`**. Node metrics `Temperature` (Float), `Pressure` (Double), `Count` (Int32), `Running`
|
|
(Boolean), `Serial` (String) plus the mandatory `bdSeq` and **`Node Control/Rebirth`** — the last one
|
|
deliberately carries a `/`, so it exercises the "a metric name is not a topic segment" rule. Device
|
|
metrics: `Temperature` (Float), `FillCount` (Int64), `Jammed` (Boolean). Cadence ~2 s.
|
|
|
|
It **subscribes to `spBv1.0/OtOpcUaSim/NCMD/{node}`** and re-births on a rebirth request, so it
|
|
exercises the real NCMD round trip rather than simulating it. Births are not retained (per spec), so
|
|
`docker restart otopcua-sparkplug-sim` is the way to force a fresh birth on demand:
|
|
|
|
```bash
|
|
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-mqtt && docker compose --profile sparkplug up -d'
|
|
ssh dohertj2@10.100.0.35 'docker restart otopcua-sparkplug-sim' # force a birth
|
|
ssh dohertj2@10.100.0.35 'docker logs -f otopcua-sparkplug-sim'
|
|
```
|
|
|
|
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. Re-gated for P2 (2026-07-25) against the Sparkplug simulator — see
|
|
[Mqtt.md](Mqtt.md).
|
|
|
|
## What it does NOT cover
|
|
|
|
### 1. Sparkplug primary-host STATE, and write-through
|
|
The simulator never observes a Host Application `STATE`, because the driver never publishes one
|
|
(`ActAsPrimaryHost` is unimplemented). Nor is there any DCMD/NCMD **command** coverage: rebirth is the
|
|
only NCMD the driver ever sends, and `IWritable` is deferred.
|
|
|
|
Sparkplug **seq-gap injection** is unit-proven only — the simulator always publishes a well-formed
|
|
monotonic `seq`, so no live test drives the gap → rebirth path end-to-end.
|
|
|
|
### 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 |
|
|
| Does a Sparkplug birth → alias → DATA → death cycle work? | ✅ yes (live, vs. the simulator) |
|
|
| Does an NCMD rebirth request actually get answered? | ✅ yes (the simulator subscribes + re-births) |
|
|
| Sparkplug seq-gap → rebirth | ❌ unit-only (the sim never skips a seq) |
|
|
| Sparkplug primary-host STATE / DCMD writes | ✅ both shipped + live-gated *(corrected 2026-08-07, truth sweep)* — NCMD/DCMD write-through as Gitea #508 (merge `2a7d1167`, 2026-07-31, 21/21 live) and primary-host STATE + Last Will as Gitea #511 (merge `857a10b4`, 2026-07-31, 5/5 live) |
|
|
|
|
## 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. A seq-gap injection switch on the simulator, closing the last unit-only Sparkplug leg.
|
|
|
|
## 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
|