117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""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)
|