You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ADN-DMR-Peer-Server/tests/application/test_peer_single_downlink.py

335 lines
12 KiB

"""Per-peer SINGLE=1 downlink exclusivity (inject-only multi-hotspot)."""
from __future__ import annotations
from adn_server.domain import bytes_3, bytes_4, int_id
from adn_server.application.bridge.helpers import (
clear_peer_ua_sessions,
clear_peer_rx_status_slots,
peer_should_receive_group_voice,
peer_single_blocks_group_voice,
peer_single_blocks_uplink,
register_peer_ua_multi_tg,
register_peer_ua_session,
seed_peer_ua_session_from_status,
)
def _sys_cfg() -> dict:
return {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 10}
def _peer_id() -> bytes:
return bytes_4(730039101)
def test_single_blocks_other_static_tg_while_session_on_static() -> None:
"""Indigo on 7305 blocks RX on 730 even when both are in OPTIONS."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_single_session_survives_on_sys_cfg_store() -> None:
"""Session lives on ``sys_cfg['_PEER_UA_SESSIONS']``, not only the peer dict."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
peer.pop("_UA_SESSION", None)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_single_uses_peer_options_timer_not_yaml_default() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = {"SINGLE_MODE": False, "DEFAULT_UA_TIMER": 60}
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
still_locked = now + 5 * 60 - 1
expired = now + 5 * 60 + 1
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=still_locked
)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=expired
)
def test_single_allows_other_tg_after_timer_expires() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
expired = now + 5 * 60 + 1
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=expired
)
def test_single_zero_allows_multiple_static_tgs() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=0;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 60
)
def test_tg4000_clears_single_session() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
clear_peer_ua_sessions(peer, sys_cfg, peer_id, slot=2)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_seed_session_from_status_after_options() -> None:
"""TX on 7305 before RPTO SINGLE=1 — OPTIONS must seed the lock."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;", "CONNECTED": 999_000.0}
peer_id = _peer_id()
sys_cfg = _sys_cfg()
status = {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 999_500.0}
now = 1_000_000.0
seed_peer_ua_session_from_status(
peer, peer_id, 2, status, sys_cfg, now=now,
)
assert not peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_seed_ignores_stale_status_from_before_reconnect() -> None:
"""OPTIONS after reconnect must not restore indigo from pre-login STATUS."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;", "CONNECTED": 2_000_000.0}
peer_id = _peer_id()
sys_cfg = _sys_cfg()
status = {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 1_000_000.0}
now = 2_000_100.0
seed_peer_ua_session_from_status(
peer, peer_id, 2, status, sys_cfg, now=now,
)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_disconnect_clears_session_on_peer_and_sys_cfg() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
clear_peer_ua_sessions(peer, sys_cfg, peer_id)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_disconnect_clears_stale_rx_status() -> None:
peer_id = _peer_id()
status = {
2: {"RX_PEER": peer_id, "RX_TGID": bytes_3(7305), "RX_TIME": 1_000_000.0},
}
clear_peer_rx_status_slots(status, peer_id)
assert status[2]["RX_PEER"] == b"\x00"
assert int_id(status[2]["RX_TGID"]) == 0
def test_single_never_blocks_uplink_tx_switches_indigo() -> None:
"""SINGLE=1 blocks RX on other TGs, not local TX; new TX replaces the session."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert not peer_single_blocks_uplink(peer, peer_id, 2, 730, sys_cfg, now=now + 10)
assert peer_single_blocks_group_voice(peer, 2, 730, sys_cfg, peer_id=peer_id, now=now + 10)
register_peer_ua_session(peer, peer_id, 2, 730, sys_cfg, now=now + 120)
assert not peer_single_blocks_group_voice(peer, 2, 730, sys_cfg, peer_id=peer_id, now=now + 121)
assert peer_single_blocks_group_voice(peer, 2, 7305, sys_cfg, peer_id=peer_id, now=now + 121)
def test_single_still_blocks_non_static_dynamic() -> None:
"""Dynamic UA not in OPTIONS stays exclusive to the session owner."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert not peer_should_receive_group_voice(
peer, 2, 730444, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_cross_peer_static_tx_does_not_reach_locked_peer() -> None:
"""730039101 indigo 7305 must not hear 730 when 730039210 keys up on 730."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7305, sys_cfg, now=now)
register_peer_ua_session(peer_b, id_b, 2, 730, sys_cfg, now=now + 5)
assert not peer_should_receive_group_voice(
peer_a, 2, 730, peer_id=id_a, connected_count=3, sys_cfg=sys_cfg, now=now + 10
)
def test_active_bridge_does_not_fan_out_without_static_tg() -> None:
"""Regression: ACTIVE BRIDGES leg must not deliver to peers missing TG in OPTIONS."""
peer_without = {"OPTIONS": b"TS2=91;"}
peer_with = {"OPTIONS": b"TS2=7305;"}
bridges = {
"7305": [
{
"SYSTEM": "SYSTEM",
"TS": 2,
"ACTIVE": True,
"TO_TYPE": "ON",
}
]
}
assert not peer_should_receive_group_voice(
peer_without,
2,
7305,
system="SYSTEM",
bridges=bridges,
connected_count=8,
sys_cfg=_sys_cfg(),
)
assert peer_should_receive_group_voice(
peer_with,
2,
7305,
system="SYSTEM",
bridges=bridges,
connected_count=8,
sys_cfg=_sys_cfg(),
)
def test_dynamic_ua_delivers_only_to_session_owner() -> None:
"""UA TG not in static OPTIONS — only the activating SINGLE peer receives it."""
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
other = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
other_id = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 730444, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 730444, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
assert not peer_should_receive_group_voice(
other, 2, 730444, peer_id=other_id, connected_count=8, sys_cfg=sys_cfg, now=now + 10
)
def test_peer_without_static_tgs_receives_nothing_until_dynamic() -> None:
"""No OPTIONS static list → no downlink; dynamic UA session is the only path."""
peer = {"OPTIONS": b"Type=HBlink;SINGLE=1;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
assert not peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=1, sys_cfg=sys_cfg, now=now + 10
)
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=1, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_dynamic_heard_when_both_peers_keyed() -> None:
"""SINGLE=0: HS1 and HS2 both keyed 7304 → each hears the other's TX on 7304."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7304, sys_cfg, now=now)
register_peer_ua_session(peer_b, id_b, 2, 7304, sys_cfg, now=now + 5)
assert peer_should_receive_group_voice(
peer_a, 2, 7304, peer_id=id_a, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
assert peer_should_receive_group_voice(
peer_b, 2, 7304, peer_id=id_b, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_dynamic_not_heard_without_local_key() -> None:
"""SINGLE=0: dynamic 7304 only reaches peers that keyed it (not the other HS)."""
peer_a = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
peer_b = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
id_a = bytes_4(730039101)
id_b = bytes_4(730039210)
now = 1_000_000.0
register_peer_ua_session(peer_a, id_a, 2, 7304, sys_cfg, now=now)
assert peer_should_receive_group_voice(
peer_a, 2, 7304, peer_id=id_a, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
assert not peer_should_receive_group_voice(
peer_b, 2, 7304, peer_id=id_b, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_single_zero_tg4000_clears_multi_dynamic() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=0;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_multi_tg(peer, peer_id, 2, 7304, sys_cfg)
clear_peer_ua_sessions(peer, sys_cfg, peer_id, slot=2)
assert not peer_should_receive_group_voice(
peer, 2, 7304, peer_id=peer_id, connected_count=2, sys_cfg=sys_cfg, now=now + 10
)
def test_new_tx_replaces_single_session_tg() -> None:
peer = {"OPTIONS": b"TS2=730,7305;SINGLE=1;TIMER=5;"}
sys_cfg = _sys_cfg()
peer_id = _peer_id()
now = 1_000_000.0
register_peer_ua_session(peer, peer_id, 2, 7305, sys_cfg, now=now)
register_peer_ua_session(peer, peer_id, 2, 730, sys_cfg, now=now + 120)
assert peer_should_receive_group_voice(
peer, 2, 730, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 121
)
assert not peer_should_receive_group_voice(
peer, 2, 7305, peer_id=peer_id, connected_count=8, sys_cfg=sys_cfg, now=now + 121
)

Powered by TurnKey Linux.