Files
pyMC_Repeater/tests/test_sqlite_handler_easy.py
T
agessaman 2432332f87 refactor(airtime): introduce refresh_radio_params method for dynamic modulation updates
Added a new method to the AirtimeManager class to refresh modulation parameters without clearing transmission history. Updated ConfigManager to call this method after applying live radio configurations. Enhanced tests to verify that modulation updates occur correctly during live updates.
2026-07-17 17:38:11 -07:00

705 lines
21 KiB
Python

import base64
import sys
import types
from pathlib import Path
import pytest
from repeater.data_acquisition.sqlite_handler import SQLiteHandler
def _make_handler(tmp_path: Path) -> SQLiteHandler:
return SQLiteHandler(tmp_path)
def test_api_token_crud_cycle(tmp_path):
h = _make_handler(tmp_path)
token_id = h.create_api_token("svc-a", "hash-a")
assert isinstance(token_id, int)
verified = h.verify_api_token("hash-a")
assert verified is not None
assert verified["id"] == token_id
assert verified["name"] == "svc-a"
listed = h.list_api_tokens()
assert any(t["id"] == token_id for t in listed)
assert h.revoke_api_token(token_id) is True
assert h.verify_api_token("hash-a") is None
assert h.revoke_api_token(token_id) is False
def test_transport_key_crud_cycle(tmp_path):
h = _make_handler(tmp_path)
key_id = h.create_transport_key(
name="root",
flood_policy="allow",
transport_key="dGVzdC1rZXk=", # base64('test-key')
)
assert isinstance(key_id, int)
row = h.get_transport_key_by_id(key_id)
assert row is not None
assert row["name"] == "root"
assert row["flood_policy"] == "allow"
# No fields to update returns False by design.
assert h.update_transport_key(key_id) is False
assert h.update_transport_key(key_id, name="child", flood_policy="deny") is True
row2 = h.get_transport_key_by_id(key_id)
assert row2 is not None
assert row2["name"] == "child"
assert row2["flood_policy"] == "deny"
all_rows = h.get_transport_keys()
assert len(all_rows) == 1
assert all_rows[0]["id"] == key_id
assert h.delete_transport_key(key_id) is True
assert h.get_transport_key_by_id(key_id) is None
assert h.delete_transport_key(key_id) is False
def test_generate_transport_key_uses_implicit_hashtag_region(tmp_path, monkeypatch):
h = _make_handler(tmp_path)
captured = {}
fake_transport_keys = types.ModuleType("openhop_core.protocol.transport_keys")
def _fake_get_auto_key_for(name: str) -> bytes:
captured["name"] = name
return b"0123456789abcdef"
fake_transport_keys.get_auto_key_for = _fake_get_auto_key_for
fake_protocol = types.ModuleType("openhop_core.protocol")
fake_protocol.transport_keys = fake_transport_keys
fake_core = types.ModuleType("openhop_core")
fake_core.protocol = fake_protocol
monkeypatch.setitem(sys.modules, "openhop_core", fake_core)
monkeypatch.setitem(sys.modules, "openhop_core.protocol", fake_protocol)
monkeypatch.setitem(sys.modules, "openhop_core.protocol.transport_keys", fake_transport_keys)
generated = h.generate_transport_key("eu")
generated_bytes = base64.b64decode(generated)
assert captured["name"] == "eu"
assert generated_bytes == b"0123456789abcdef"
assert len(generated_bytes) == 16
def test_room_messages_and_sync_flow(tmp_path):
h = _make_handler(tmp_path)
room_hash = "0x42"
a_pub = "a" * 64
b_pub = "b" * 64
m1 = h.insert_room_message(room_hash, a_pub, "hello", post_timestamp=100.0)
m2 = h.insert_room_message(room_hash, b_pub, "world", post_timestamp=200.0)
assert isinstance(m1, int)
assert isinstance(m2, int)
assert h.get_room_message_count(room_hash) == 2
# get_room_messages sorts by post_timestamp DESC
msgs = h.get_room_messages(room_hash, limit=10, offset=0)
assert len(msgs) == 2
assert msgs[0]["message_text"] == "world"
since = h.get_messages_since(room_hash, since_timestamp=150.0, limit=10)
assert len(since) == 1
assert since[0]["message_text"] == "world"
unsynced_for_a = h.get_unsynced_messages(room_hash, client_pubkey=a_pub, sync_since=0.0)
assert len(unsynced_for_a) == 1
assert unsynced_for_a[0]["author_pubkey"] == b_pub
assert h.get_unsynced_count(room_hash, client_pubkey=a_pub, sync_since=0.0) == 1
# Client sync upsert/get/list
assert h.upsert_client_sync(room_hash, a_pub, sync_since=50.0, last_activity=123.0) is True
sync = h.get_client_sync(room_hash, a_pub)
assert sync is not None
assert sync["sync_since"] == 50.0
clients = h.get_all_room_clients(room_hash)
assert len(clients) == 1
assert clients[0]["client_pubkey"] == a_pub
assert h.delete_room_message(room_hash, int(m1)) is True
assert h.delete_room_message(room_hash, int(m1)) is False
deleted = h.clear_room_messages(room_hash)
assert deleted == 1
assert h.get_room_message_count(room_hash) == 0
def test_store_and_delete_advert(tmp_path):
h = _make_handler(tmp_path)
h.store_advert(
{
"timestamp": 123.0,
"pubkey": "pk1",
"node_name": "node-1",
"is_repeater": True,
"route_type": 1,
"contact_type": "neighbor",
"latitude": 1.0,
"longitude": 2.0,
"rssi": -88,
"snr": 7.5,
"is_new_neighbor": True,
"zero_hop": True,
}
)
with h._connect() as conn:
row = conn.execute("SELECT id FROM adverts WHERE pubkey = ?", ("pk1",)).fetchone()
assert row is not None
advert_id = int(row[0])
assert h.delete_advert(advert_id) is True
assert h.delete_advert(advert_id) is False
def test_store_packet_returns_inserted_row_id(tmp_path):
h = _make_handler(tmp_path)
packet_id = h.store_packet(
{
"timestamp": 123.0,
"type": 1,
"route": 2,
"length": 3,
"transmitted": True,
"packet_hash": "pkt-1",
}
)
assert isinstance(packet_id, int)
assert packet_id > 0
with h._connect() as conn:
row = conn.execute(
"SELECT id, type, route, length FROM packets WHERE id = ?",
(packet_id,),
).fetchone()
assert row is not None
assert row[0] == packet_id
assert row[1] == 1
assert row[2] == 2
assert row[3] == 3
def test_packets_table_has_upstream_columns_and_index(tmp_path):
h = _make_handler(tmp_path)
with h._connect() as conn:
cols = conn.execute("PRAGMA table_info(packets)").fetchall()
col_names = {col[1] for col in cols}
idx_rows = conn.execute("PRAGMA index_list(packets)").fetchall()
idx_names = {row[1] for row in idx_rows}
assert "upstream_hash" in col_names
assert "upstream_hash_size" in col_names
assert "idx_packets_upstream_time" in idx_names
def test_store_packet_persists_upstream_fields(tmp_path):
h = _make_handler(tmp_path)
packet_id = h.store_packet(
{
"timestamp": 200.0,
"type": 1,
"route": 1,
"length": 9,
"transmitted": False,
"packet_hash": "pkt-upstream",
"upstream_hash": "AB",
"upstream_hash_size": 2,
"original_path": ["CD", "AB"],
}
)
row = h.get_packet_by_id(int(packet_id))
assert row is not None
assert row["upstream_hash"] == "AB"
assert row["upstream_hash_size"] == 2
def test_neighbor_link_history_uses_packets_table_and_filters_hash_and_size(tmp_path):
h = _make_handler(tmp_path)
base_ts = 1_700_000_000.0
h.store_packet(
{
"timestamp": base_ts,
"type": 4,
"route": 1,
"length": 10,
"rssi": -80,
"snr": 3.5,
"score": 0.4,
"is_duplicate": False,
"packet_hash": "match-1",
"upstream_hash": "AA",
"upstream_hash_size": 1,
"original_path": ["10", "AA"],
}
)
h.store_packet(
{
"timestamp": base_ts + 1.0,
"type": 5,
"route": 1,
"length": 11,
"rssi": -82,
"snr": 2.5,
"score": 0.35,
"is_duplicate": True,
"packet_hash": "match-2",
"upstream_hash": "AA",
"upstream_hash_size": 1,
"original_path": ["20", "30", "AA"],
}
)
# Same hash but different width: must not be merged.
h.store_packet(
{
"timestamp": base_ts + 2.0,
"type": 6,
"route": 1,
"length": 12,
"packet_hash": "wrong-width",
"upstream_hash": "AA",
"upstream_hash_size": 2,
"original_path": ["00AA"],
}
)
# Different hash: must be filtered out.
h.store_packet(
{
"timestamp": base_ts + 3.0,
"type": 7,
"route": 1,
"length": 13,
"packet_hash": "wrong-hash",
"upstream_hash": "BB",
"upstream_hash_size": 1,
"original_path": ["BB"],
}
)
rows = h.get_neighbor_link_history(peer_hash="aa", path_hash_size=1, hours=50000, limit=100)
assert [row["packet_hash"] for row in rows] == ["match-1", "match-2"]
assert rows[0]["path_hop_count"] == 2
assert rows[1]["path_hop_count"] == 3
assert rows[1]["is_duplicate"] is True
def test_recent_packet_queries_include_ids_and_preserve_duplicate_hash_rows(tmp_path):
h = _make_handler(tmp_path)
first_id = h.store_packet(
{
"timestamp": 100.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": False,
"is_duplicate": False,
"packet_hash": "same-hash",
}
)
second_id = h.store_packet(
{
"timestamp": 101.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": True,
"is_duplicate": True,
"packet_hash": "same-hash",
}
)
recent = h.get_recent_packets(limit=10)
assert len(recent) == 2
assert {packet["id"] for packet in recent} == {first_id, second_id}
assert [packet["packet_hash"] for packet in recent] == ["same-hash", "same-hash"]
filtered = h.get_filtered_packets(limit=10)
assert len(filtered) == 2
assert {packet["id"] for packet in filtered} == {first_id, second_id}
by_id = h.get_packet_by_id(int(second_id))
assert by_id is not None
assert by_id["id"] == second_id
assert by_id["packet_hash"] == "same-hash"
def test_verify_api_token_last_used_throttle(tmp_path, monkeypatch):
h = _make_handler(tmp_path)
h._api_token_last_used_interval_sec = 300
now = {"v": 1000.0}
monkeypatch.setattr("repeater.data_acquisition.sqlite_handler.time.time", lambda: now["v"])
token_id = h.create_api_token("svc-throttle", "hash-throttle")
assert token_id > 0
assert h.verify_api_token("hash-throttle") is not None
with h._connect() as conn:
first = conn.execute(
"SELECT last_used FROM api_tokens WHERE id = ?", (token_id,)
).fetchone()[0]
assert first == 1000.0
now["v"] = 1010.0
assert h.verify_api_token("hash-throttle") is not None
with h._connect() as conn:
second = conn.execute(
"SELECT last_used FROM api_tokens WHERE id = ?", (token_id,)
).fetchone()[0]
assert second == 1000.0
now["v"] = 1401.0
assert h.verify_api_token("hash-throttle") is not None
with h._connect() as conn:
third = conn.execute(
"SELECT last_used FROM api_tokens WHERE id = ?", (token_id,)
).fetchone()[0]
assert third == 1401.0
def test_store_advert_zero_hop_signal_handling(tmp_path):
h = _make_handler(tmp_path)
h.store_advert(
{
"timestamp": 10.0,
"pubkey": "pk-z",
"node_name": "node-z",
"is_repeater": False,
"route_type": 1,
"contact_type": "neighbor",
"rssi": -80,
"snr": 5.0,
"is_new_neighbor": True,
"zero_hop": True,
}
)
# Multi-hop update must preserve previous zero-hop signal quality.
h.store_advert(
{
"timestamp": 20.0,
"pubkey": "pk-z",
"node_name": "node-z-2",
"is_repeater": False,
"route_type": 2,
"contact_type": "neighbor",
"rssi": -50,
"snr": 9.0,
"is_new_neighbor": False,
"zero_hop": False,
}
)
with h._connect() as conn:
row = conn.execute(
"SELECT rssi, snr, zero_hop, advert_count FROM adverts WHERE pubkey = ?",
("pk-z",),
).fetchone()
assert row is not None
assert row[0] == -80
assert row[1] == 5.0
assert bool(row[2]) is True
assert row[3] == 2
# New zero-hop update should refresh signal quality.
h.store_advert(
{
"timestamp": 30.0,
"pubkey": "pk-z",
"node_name": "node-z-3",
"is_repeater": False,
"route_type": 1,
"contact_type": "neighbor",
"rssi": -60,
"snr": 6.5,
"is_new_neighbor": False,
"zero_hop": True,
}
)
with h._connect() as conn:
row2 = conn.execute(
"SELECT rssi, snr, zero_hop, advert_count FROM adverts WHERE pubkey = ?",
("pk-z",),
).fetchone()
assert row2 is not None
assert row2[0] == -60
assert row2[1] == 6.5
assert bool(row2[2]) is True
assert row2[3] == 3
def test_sync_transport_keys_validation_and_tree_apply(tmp_path, monkeypatch):
h = _make_handler(tmp_path)
with pytest.raises(ValueError, match="must be a list"):
h.sync_transport_keys({"bad": True})
with pytest.raises(ValueError, match="Duplicate node_id"):
h.sync_transport_keys(
[
{"node_id": "1", "name": "a", "flood_policy": "allow"},
{"node_id": "1", "name": "b", "flood_policy": "deny"},
]
)
def test_sync_transport_keys_parent_and_tree_apply(tmp_path, monkeypatch):
h = _make_handler(tmp_path)
with pytest.raises(ValueError, match="Parent node 'missing'"):
h.sync_transport_keys(
[
{
"node_id": "c1",
"name": "child",
"flood_policy": "allow",
"parent_node_id": "missing",
}
]
)
monkeypatch.setattr(h, "generate_transport_key", lambda _name: "GEN-KEY")
applied = h.sync_transport_keys(
[
{
"node_id": "root",
"name": "root-name",
"flood_policy": "allow",
"transport_key": "ROOT-KEY",
},
{
"node_id": "child",
"name": "child-name",
"flood_policy": "deny",
"parent_node_id": "root",
"transport_key": None,
},
]
)
assert applied == {"applied_nodes": 2, "generated_keys": 1}
with h._connect() as conn:
rows = conn.execute(
"SELECT id, name, flood_policy, transport_key, parent_id FROM transport_keys ORDER BY id"
).fetchall()
assert len(rows) == 2
assert rows[0][1] == "root-name"
assert rows[0][3] == "ROOT-KEY"
assert rows[1][1] == "child-name"
assert rows[1][2] == "deny"
assert rows[1][3] == "GEN-KEY"
assert rows[1][4] == rows[0][0]
def test_get_lbt_diagnostics_aggregates_retry_distribution_and_summary(tmp_path):
h = _make_handler(tmp_path)
packets = [
{
"timestamp": 10.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": True,
"packet_hash": "lbt-1",
"lbt_attempts": 0,
"lbt_channel_busy": False,
},
{
"timestamp": 20.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": True,
"packet_hash": "lbt-2",
"lbt_attempts": 1,
"lbt_channel_busy": True,
},
{
"timestamp": 30.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": True,
"packet_hash": "lbt-3",
"lbt_attempts": 2,
"lbt_channel_busy": True,
},
{
"timestamp": 40.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": False,
"drop_reason": "TX failed",
"packet_hash": "lbt-4",
"lbt_attempts": 4,
"lbt_channel_busy": True,
},
{
# Excluded from TX-path diagnostics by filter.
"timestamp": 50.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": False,
"drop_reason": "Duplicate",
"packet_hash": "lbt-excluded",
"lbt_attempts": 0,
"lbt_channel_busy": False,
},
{
"timestamp": 70.0,
"type": 1,
"route": 1,
"length": 8,
"transmitted": True,
"packet_hash": "lbt-5",
"lbt_attempts": 0,
"lbt_channel_busy": False,
},
]
for record in packets:
h.store_packet(record)
out = h.get_lbt_diagnostics(
start_timestamp=0,
end_timestamp=180,
bucket_seconds=60,
severe_attempt_threshold=4,
)
summary = out["summary"]
assert summary["total_transmissions"] == 5
assert summary["total_attempts"] == 12
assert summary["first_attempt_success"] == 2
assert summary["retry_packets"] == 3
assert summary["retry_rate_pct"] == pytest.approx(60.0)
assert summary["avg_attempts"] == pytest.approx(2.4)
assert summary["max_attempts"] == 5
assert summary["median_attempts"] == pytest.approx(2.0)
assert summary["p95_attempts"] == pytest.approx(5.0)
assert summary["attempts_1"] == 2
assert summary["attempts_2"] == 1
assert summary["attempts_3"] == 1
assert summary["attempts_4_plus"] == 1
assert summary["attempts_3_plus"] == 2
assert summary["failed_transmissions"] == 1
assert summary["busy_channel_events"] == 3
assert summary["severe_contention_count"] == 1
assert summary["has_lbt_data"] is True
assert summary["worst_bucket"] is not None
assert summary["worst_bucket"]["timestamp"] == 0
buckets = {int(b["timestamp"]): b for b in out["buckets"]}
first = buckets[0]
assert first["transmissions"] == 4
assert first["retry_packets"] == 3
assert first["retry_rate_pct"] == pytest.approx(75.0)
assert first["first_attempt_success_rate_pct"] == pytest.approx(25.0)
assert first["attempts_4_plus"] == 1
assert first["severe_contention_count"] == 1
assert first["failed_transmissions"] == 1
second = buckets[60]
assert second["transmissions"] == 1
assert second["retry_packets"] == 0
assert second["retry_rate_pct"] == pytest.approx(0.0)
assert second["avg_attempts"] == pytest.approx(1.0)
packet_types = out["packet_types"]
assert len(packet_types) == 1
assert packet_types[0]["packet_type"] == 1
assert packet_types[0]["transmissions"] == 5
assert packet_types[0]["retry_packets"] == 3
packet_type_buckets = out["packet_type_buckets"]
assert len(packet_type_buckets) == 2
first_type_bucket = packet_type_buckets[0]
assert first_type_bucket["packet_type"] == 1
assert first_type_bucket["timestamp"] == 0
assert first_type_bucket["retry_rate_pct"] == pytest.approx(75.0)
assert first_type_bucket["attempts_3_plus_pct"] == pytest.approx(50.0)
def test_get_lbt_diagnostics_empty_range_preserves_no_data_distinction(tmp_path):
h = _make_handler(tmp_path)
out = h.get_lbt_diagnostics(
start_timestamp=0,
end_timestamp=180,
bucket_seconds=60,
severe_attempt_threshold=4,
)
summary = out["summary"]
assert summary["total_transmissions"] == 0
assert summary["retry_rate_pct"] is None
assert summary["first_attempt_success_rate_pct"] is None
assert summary["avg_attempts"] is None
assert summary["has_lbt_data"] is False
assert len(out["buckets"]) >= 3
for bucket in out["buckets"]:
assert bucket["transmissions"] == 0
assert bucket["retry_rate_pct"] is None
assert bucket["first_attempt_success_rate_pct"] is None
assert bucket["avg_attempts"] is None
assert out["packet_types"] == []
assert out["packet_type_buckets"] == []
def test_cleanup_old_data_accepts_companion_events_days(tmp_path):
"""engine.py forwards companion_events_days; accepting it must not break
packet retention cleanup (the pre-fix TypeError was swallowed upstream).
"""
import time
h = _make_handler(tmp_path)
old_ts = time.time() - (40 * 86400)
h.store_packet(
{
"timestamp": old_ts,
"type": 1,
"route": 2,
"length": 3,
"transmitted": False,
"packet_hash": "pkt-old",
}
)
h.cleanup_old_data(days=31, companion_events_days=14)
with h._connect() as conn:
count = conn.execute("SELECT COUNT(*) FROM packets").fetchone()[0]
assert count == 0