diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx index 02a8700d..7c5147f0 100644 --- a/ZB.MOM.WW.OtOpcUa.slnx +++ b/ZB.MOM.WW.OtOpcUa.slnx @@ -108,6 +108,7 @@ + diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore new file mode 100644 index 00000000..b69c261a --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore @@ -0,0 +1,14 @@ +# Fixture credential + TLS material produced by gen-fixture-material.sh. +# +# NEVER commit anything in here: it holds the broker password hash, the fixture CA's +# PRIVATE key and the server's private key. The generator script is the artifact in +# git; its output is regenerated per machine. +secrets/ + +# Belt-and-braces: catch key/cert material written directly into this directory by a +# hand-run openssl command that forgot the secrets/ prefix. +*.key +*.crt +*.csr +*.srl +passwd diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml new file mode 100644 index 00000000..a1ecfec7 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml @@ -0,0 +1,77 @@ +# MQTT integration-test fixture — Eclipse Mosquitto (auth + TLS) + a JSON publisher. +# +# PREREQUISITE — run this first, in this directory: +# +# MQTT_FIXTURE_PASSWORD='' ./gen-fixture-material.sh +# +# It writes ./secrets/ (password file + CA + server certificate/key), which is +# gitignored and which both services below mount. Without it the broker will not +# start: there is no anonymous fallback, by design. +# +# Bring up (on the shared Docker host — see infra/README.md §1): +# +# ssh dohertj2@10.100.0.35 'cd /opt/otopcua-mqtt \ +# && MQTT_FIXTURE_USERNAME=otopcua MQTT_FIXTURE_PASSWORD= docker compose up -d' +# +# Endpoints: 10.100.0.35:8883 (TLS + auth) · 10.100.0.35:1883 (plaintext + auth, smoke) +# +# Unlike the Modbus / S7 fixtures there are no compose profiles: both services are +# always wanted together (a broker with nothing publishing into it tests nothing), and +# they bind different ports so nothing contends. +services: + mosquitto: + image: eclipse-mosquitto:2.0.22 + container_name: otopcua-mosquitto + restart: unless-stopped + # Every lmxopcua fixture container carries this so the shared Docker host stays + # discoverable with `docker ps --filter label=project=lmxopcua`. + labels: + project: lmxopcua + ports: + - "1883:1883" + - "8883:8883" + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - ./secrets:/mosquitto/secrets:ro + healthcheck: + # Authenticated round-trip against the broker's own $SYS tree ($$ escapes the + # compose variable syntax). Proves the password file loaded and the listener + # accepts a real session — a bare port check would go green on a broker that + # rejects every CONNECT. + test: + - CMD-SHELL + - >- + mosquitto_sub -h 127.0.0.1 -p 1883 + -u "$$MQTT_FIXTURE_USERNAME" -P "$$MQTT_FIXTURE_PASSWORD" + -t '$$SYS/broker/uptime' -C 1 -W 3 + interval: 5s + timeout: 8s + retries: 12 + start_period: 5s + environment: + # Consumed only by the healthcheck above; the broker itself reads the hashed + # password file. Supplied by the operator's environment at `docker compose up` — + # never defaulted here, so a missing value fails loudly instead of silently + # provisioning a known-password broker. + MQTT_FIXTURE_USERNAME: ${MQTT_FIXTURE_USERNAME:?set MQTT_FIXTURE_USERNAME (must match gen-fixture-material.sh)} + MQTT_FIXTURE_PASSWORD: ${MQTT_FIXTURE_PASSWORD:?set MQTT_FIXTURE_PASSWORD (must match gen-fixture-material.sh)} + + publisher: + image: eclipse-mosquitto:2.0.22 + container_name: otopcua-mqtt-publisher + restart: unless-stopped + labels: + project: lmxopcua + depends_on: + mosquitto: + condition: service_healthy + volumes: + - ./publisher/publish.sh:/publish.sh:ro + environment: + BROKER_HOST: mosquitto + BROKER_PORT: "1883" + TOPIC_PREFIX: ${MQTT_FIXTURE_TOPIC_PREFIX:-otopcua/fixture} + PUBLISH_INTERVAL: ${MQTT_FIXTURE_PUBLISH_INTERVAL:-2} + MQTT_FIXTURE_USERNAME: ${MQTT_FIXTURE_USERNAME:?set MQTT_FIXTURE_USERNAME} + MQTT_FIXTURE_PASSWORD: ${MQTT_FIXTURE_PASSWORD:?set MQTT_FIXTURE_PASSWORD} + entrypoint: ["/bin/sh", "/publish.sh"] diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh new file mode 100755 index 00000000..3b72538f --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env sh +# --------------------------------------------------------------------------- +# Generates the MQTT fixture's password file + TLS material into ./secrets/. +# +# NOTHING this script writes is committed: ./secrets/ is excluded by the sibling +# .gitignore. The SCRIPT is the artifact in git, the OUTPUT never is. Re-run it on any +# machine that needs the fixture (or on the Docker host); the material is throwaway by +# construction — a self-issued CA, a 2048-bit server key, and whatever password the +# operator supplies in the environment. +# +# Usage: +# MQTT_FIXTURE_PASSWORD='' ./gen-fixture-material.sh +# +# Environment: +# MQTT_FIXTURE_PASSWORD (required) broker password. No default — a committed or +# defaulted password is exactly what the plan's +# "secrets from env, never committed" rule forbids. +# MQTT_FIXTURE_USERNAME broker username (default: otopcua) +# MQTT_FIXTURE_SAN_IPS comma-separated IP SANs for the server certificate +# (default: 10.100.0.35,127.0.0.1) +# MQTT_FIXTURE_SAN_DNS comma-separated DNS SANs +# (default: localhost,mosquitto) +# MQTT_FIXTURE_CERT_CN server certificate CN (default: first IP SAN) +# MQTT_FIXTURE_CERT_DAYS certificate lifetime in days (default: 825) +# MQTT_FIXTURE_SECRETS_DIR output directory (default: