"""Port-resolution logic for the MQTT bridge: the stateless pre-flight gate and standard/dynamic OCF port discovery in PushBridge. The DTLS probe is faked so these run without hardware; only routing and selection are exercised. """ import logging import types import pytest import mqtt_demo.bridge as bridge def _mk_bridge(ocf_port, default=49155, discovered=None): """A PushBridge shell with only the attributes _resolve_port touches, bypassing the heavyweight __init__ (MQTT client, cert paths, …).""" b = bridge.PushBridge.__new__(bridge.PushBridge) b.app = types.SimpleNamespace(ip='192.0.2.9', ocf_port=ocf_port, index=0) b.descriptor = types.SimpleNamespace(default_observe_port=default) b._discovered_port = discovered b.log = logging.getLogger('test-bridge') return b def _fake_port_probe(live_ports): """Return a one-port probe stand-in for the selected live ports.""" def fake(ip, port, **kw): alive = port in live_ports return types.SimpleNamespace( port=port, is_dtls_server=alive, ) return fake def _fake_port_set(live_ports): """Return an aggregate probe stand-in with explicit ambiguity.""" def fake(ip, ports, **kw): live = tuple(port for port in ports if port in live_ports) if len(live) == 1: outcome = 'selected' selected_port = live[0] elif live: outcome = 'ambiguous' selected_port = None else: outcome = 'unreachable' selected_port = None return types.SimpleNamespace( outcome=outcome, selected_port=selected_port, ) return fake def test_pinned_live_port_is_gated_and_returned(monkeypatch): monkeypatch.setattr(bridge, 'probe_dtls_port', _fake_port_probe({49155})) b = _mk_bridge(ocf_port=49155) assert b._resolve_port() == 49155 def test_pinned_dead_port_raises_for_backoff(monkeypatch): monkeypatch.setattr(bridge, 'probe_dtls_port', _fake_port_probe(set())) b = _mk_bridge(ocf_port=49155) with pytest.raises(ConnectionError): b._resolve_port() def test_autodiscovery_finds_and_caches_live_port(monkeypatch): # Only 49154 answers; it isn't the descriptor default, so discovery is # what finds it — and it must be cached for the next reconnect. monkeypatch.setattr(bridge, 'probe_dtls_ports', _fake_port_set({49154})) b = _mk_bridge(ocf_port=None, default=49155) assert b._resolve_port() == 49154 assert b._discovered_port == 49154 def test_autodiscovery_refuses_ambiguous_live_ports(monkeypatch): monkeypatch.setattr( bridge, 'probe_dtls_ports', _fake_port_set({5684, 49154}), ) b = _mk_bridge(ocf_port=None, default=49155) with pytest.raises(ConnectionError, match='multiple DTLS listeners'): b._resolve_port() def test_cached_live_port_is_reused_without_rediscovery(monkeypatch): # Cached 49156 and the default 49155 are both live; the cache-first # path must return the previously proven port without an ambiguous # full-set probe. monkeypatch.setattr( bridge, 'probe_dtls_port', _fake_port_probe({49155, 49156}), ) b = _mk_bridge(ocf_port=None, default=49155, discovered=49156) assert b._resolve_port() == 49156 def test_autodiscovery_all_dead_raises_and_clears_cache(monkeypatch): monkeypatch.setattr(bridge, 'probe_dtls_port', _fake_port_probe(set())) monkeypatch.setattr(bridge, 'probe_dtls_ports', _fake_port_set(set())) b = _mk_bridge(ocf_port=None, discovered=49154) with pytest.raises(ConnectionError): b._resolve_port() assert b._discovered_port is None def test_candidate_ports_cover_band_plus_default(monkeypatch): b = _mk_bridge(ocf_port=None, default=49200) cands = b._candidate_ports() assert set(bridge.OCF_PORT_BAND) <= set(cands) assert bridge.OCF_STANDARD_SECURE_PORT in cands assert 49200 in cands assert cands == sorted(cands)