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.
153 lines
4.8 KiB
153 lines
4.8 KiB
# ADN DMR Peer Server - alias reload resilience when selfcare download fails
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from adn_server.infrastructure.persistence.alias_loader import DefaultAliasLoader, try_download
|
|
|
|
|
|
def _write_subscriber_file(path: Path, file_name: str, rid: int, callsign: str) -> None:
|
|
data = {"subscribers": [{"id": rid, "callsign": callsign}]}
|
|
(path / file_name).write_text(json.dumps(data), encoding="utf-8")
|
|
|
|
|
|
def test_try_download_failure_does_not_erase_existing_file(tmp_path: Path) -> None:
|
|
file_name = "subscriber_ids.json"
|
|
full = tmp_path / file_name
|
|
full.write_bytes(b'{"subscribers":[{"id":7300391,"callsign":"CE5RPY"}]}')
|
|
# stale_sec=0 forces download attempt; bad URL simulates selfcare down.
|
|
# max_attempts=1 keeps the test fast; retry behavior is covered separately below.
|
|
result = try_download(
|
|
tmp_path, file_name, "http://127.0.0.1:1/nope.json", stale_sec=0, max_attempts=1
|
|
)
|
|
assert "could not be downloaded" in result or "IOError" in result
|
|
assert full.read_bytes().startswith(b"{")
|
|
|
|
|
|
def test_try_download_retries_and_recovers_from_transient_failure(tmp_path: Path) -> None:
|
|
file_name = "peer_ids.json"
|
|
calls: list[float] = []
|
|
|
|
def _flaky_urlopen(url, context=None, timeout=None):
|
|
calls.append(timeout)
|
|
if len(calls) < 3:
|
|
raise OSError("connection refused")
|
|
return _FakeResponse(b'{"peers":[{"id":1,"callsign":"X"}]}')
|
|
|
|
with patch(
|
|
"adn_server.infrastructure.persistence.alias_loader.urlopen", side_effect=_flaky_urlopen
|
|
):
|
|
result = try_download(
|
|
tmp_path,
|
|
file_name,
|
|
"https://example.invalid/peer_ids.json",
|
|
stale_sec=0,
|
|
max_attempts=3,
|
|
retry_delay_sec=0,
|
|
)
|
|
assert "successfully downloaded" in result
|
|
assert len(calls) == 3
|
|
# first attempt uses the longer timeout, retries use the shorter one
|
|
assert calls[0] == 30
|
|
assert calls[1] == 10
|
|
|
|
|
|
def test_try_download_gives_up_after_max_attempts(tmp_path: Path) -> None:
|
|
file_name = "peer_ids.json"
|
|
calls: list[float] = []
|
|
|
|
def _always_fails(url, context=None, timeout=None):
|
|
calls.append(timeout)
|
|
raise OSError("connection refused")
|
|
|
|
with patch(
|
|
"adn_server.infrastructure.persistence.alias_loader.urlopen", side_effect=_always_fails
|
|
):
|
|
result = try_download(
|
|
tmp_path,
|
|
file_name,
|
|
"https://example.invalid/peer_ids.json",
|
|
stale_sec=0,
|
|
max_attempts=3,
|
|
retry_delay_sec=0,
|
|
)
|
|
assert "could not be downloaded" in result
|
|
assert len(calls) == 3
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, data: bytes) -> None:
|
|
self._data = data
|
|
|
|
def read(self) -> bytes:
|
|
return self._data
|
|
|
|
def __enter__(self) -> "_FakeResponse":
|
|
return self
|
|
|
|
def __exit__(self, *exc: object) -> None:
|
|
return None
|
|
|
|
|
|
def test_merge_reload_keeps_previous_sub_ids_on_empty_reload() -> None:
|
|
loader = DefaultAliasLoader()
|
|
config = {
|
|
"_SUB_IDS": {7300391: "CE5RPY"},
|
|
"_PEER_IDS": {730039101: "CE5RPY"},
|
|
"ALIASES": {"PATH": "."},
|
|
}
|
|
DefaultAliasLoader.merge_reload_into_config(
|
|
config,
|
|
loader,
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
{},
|
|
)
|
|
assert config["_SUB_IDS"] == {7300391: "CE5RPY"}
|
|
assert config["_PEER_IDS"] == {730039101: "CE5RPY"}
|
|
|
|
|
|
def test_load_id_dict_with_backup_uses_bak_on_checksum_mismatch(tmp_path: Path) -> None:
|
|
loader = DefaultAliasLoader()
|
|
file_name = "subscriber_ids.json"
|
|
_write_subscriber_file(tmp_path, file_name, 1111111, "BAD")
|
|
_write_subscriber_file(tmp_path, f"{file_name}.bak", 7300391, "GOOD")
|
|
loaded = loader._load_id_dict_with_backup(
|
|
tmp_path,
|
|
file_name,
|
|
"deadbeef",
|
|
"subscriber_ids",
|
|
)
|
|
assert loaded.get(7300391) == "GOOD"
|
|
|
|
|
|
def test_load_id_dict_with_backup_uses_bak_when_primary_missing(tmp_path: Path) -> None:
|
|
loader = DefaultAliasLoader()
|
|
file_name = "subscriber_ids.json"
|
|
# No primary file at all (e.g. first boot, download never succeeded), only a .bak
|
|
# from a previous successful run.
|
|
_write_subscriber_file(tmp_path, f"{file_name}.bak", 7300391, "GOOD")
|
|
loaded = loader._load_id_dict_with_backup(
|
|
tmp_path,
|
|
file_name,
|
|
None,
|
|
"subscriber_ids",
|
|
)
|
|
assert loaded.get(7300391) == "GOOD"
|
|
|
|
|
|
def test_load_server_tsv_with_backup_uses_bak_when_primary_missing(tmp_path: Path) -> None:
|
|
loader = DefaultAliasLoader()
|
|
file_name = "server_ids.tsv"
|
|
(tmp_path / f"{file_name}.bak").write_text(
|
|
"OPB Net ID\tCountry\n1234\tChile\n", encoding="utf-8"
|
|
)
|
|
loaded = loader._load_server_tsv_with_backup(tmp_path, file_name, None)
|
|
assert loaded.get("1234") == "Chile"
|