mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-07-27 03:52:47 +02:00
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""SQLite regression tests for raw MeshCore ADVERT contact exchange."""
|
|
|
|
import asyncio
|
|
import sqlite3
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from openhop_core.companion.models import Contact
|
|
from openhop_core.protocol import LocalIdentity
|
|
from repeater.companion.bridge import RepeaterCompanionBridge
|
|
from repeater.companion.frame_server import CompanionFrameServer
|
|
from repeater.data_acquisition.sqlite_handler import SQLiteHandler
|
|
|
|
|
|
# The same literal MeshCore Packet::writeTo ADVERT fixture exercised by Core.
|
|
# It deliberately includes header/path bytes and a signature, so BLOB handling
|
|
# is tested with the real protocol shape instead of a synthetic text payload.
|
|
MESHCORE_ADVERT_WIRE = bytes.fromhex(
|
|
"1100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
|
|
"7856341200844b4710c3083898f85b990be03edd18c9c4d05b82590915f72d3f04"
|
|
"ac2eefc5446a5379bf25fbbbc7fcab87570e7552ed7d30def3291ea495be646cb1"
|
|
"ef078146776d566563"
|
|
)
|
|
MESHCORE_ADVERT_PUBKEY = bytes.fromhex(
|
|
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
|
|
)
|
|
COMPANION_HASH = "0x42"
|
|
|
|
|
|
async def _drain_loopback() -> None:
|
|
for _ in range(3):
|
|
await asyncio.sleep(0)
|
|
|
|
|
|
def _contact(raw_advert=MESHCORE_ADVERT_WIRE) -> dict:
|
|
return {
|
|
"pubkey": MESHCORE_ADVERT_PUBKEY,
|
|
"name": "FwmVec",
|
|
"adv_type": 1,
|
|
"flags": 0,
|
|
"out_path_len": -1,
|
|
"out_path": b"",
|
|
"last_advert_timestamp": 0x12345678,
|
|
"last_advert_packet": raw_advert,
|
|
"lastmod": 123,
|
|
"gps_lat": 0.0,
|
|
"gps_lon": 0.0,
|
|
"sync_since": 0,
|
|
}
|
|
|
|
|
|
def _bridge(sqlite_handler) -> RepeaterCompanionBridge:
|
|
return RepeaterCompanionBridge(
|
|
LocalIdentity(),
|
|
AsyncMock(return_value=True),
|
|
sqlite_handler=sqlite_handler,
|
|
companion_hash=COMPANION_HASH,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_message_before_first_client_connection_survives_restart(tmp_path):
|
|
handler = SQLiteHandler(tmp_path)
|
|
bridge = _bridge(handler)
|
|
server = CompanionFrameServer(bridge, COMPANION_HASH, port=0, sqlite_handler=handler)
|
|
with patch("repeater.companion.frame_server._BaseFrameServer.start", AsyncMock()):
|
|
await server.start()
|
|
assert server._client_writer is None
|
|
await bridge._handle_new_message(
|
|
{
|
|
"contact_pubkey": "01" * 32,
|
|
"message_text": "before-first-connect",
|
|
"timestamp": 123,
|
|
"txt_type": 0,
|
|
}
|
|
)
|
|
assert bridge.message_queue.is_empty()
|
|
|
|
restarted_handler = SQLiteHandler(tmp_path)
|
|
message = restarted_handler.companion_pop_message(COMPANION_HASH)
|
|
assert message is not None
|
|
assert message["text"] == "before-first-connect"
|
|
assert message["timestamp"] == 123
|
|
|
|
|
|
def test_bulk_save_load_preserves_raw_advert_blob_and_upsert_replaces_it(tmp_path):
|
|
handler = SQLiteHandler(tmp_path)
|
|
assert handler.companion_save_contacts(COMPANION_HASH, [_contact()])
|
|
assert (
|
|
handler.companion_load_contacts(COMPANION_HASH)[0]["last_advert_packet"]
|
|
== MESHCORE_ADVERT_WIRE
|
|
)
|
|
|
|
updated = MESHCORE_ADVERT_WIRE[:-1] + b"!"
|
|
assert handler.companion_upsert_contact(COMPANION_HASH, _contact(updated))
|
|
assert handler.companion_load_contacts(COMPANION_HASH)[0]["last_advert_packet"] == updated
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_persist_restart_and_export_are_byte_exact(tmp_path):
|
|
handler = SQLiteHandler(tmp_path)
|
|
first = _bridge(handler)
|
|
|
|
assert first.import_contact(MESHCORE_ADVERT_WIRE) is True
|
|
await _drain_loopback()
|
|
contact = first.get_contact_by_key(MESHCORE_ADVERT_PUBKEY)
|
|
assert contact is not None
|
|
|
|
# RepeaterFrameServer is the production persistence adapter used by the
|
|
# contact-updated callback. Persist through it rather than writing a row
|
|
# directly, then rebuild state exactly as the daemon restart path does.
|
|
server = CompanionFrameServer(first, COMPANION_HASH, port=0, sqlite_handler=handler)
|
|
await server._persist_contact(contact)
|
|
|
|
restarted_handler = SQLiteHandler(tmp_path)
|
|
rows = restarted_handler.companion_load_contacts(COMPANION_HASH)
|
|
assert rows is not None and len(rows) == 1
|
|
assert rows[0]["last_advert_packet"] == MESHCORE_ADVERT_WIRE
|
|
|
|
restarted = _bridge(restarted_handler)
|
|
records = []
|
|
for row in rows:
|
|
record = dict(row)
|
|
record["public_key"] = record.pop("pubkey")
|
|
records.append(record)
|
|
restarted.contacts.load_from_dicts(records)
|
|
|
|
assert restarted.export_contact(MESHCORE_ADVERT_PUBKEY) == MESHCORE_ADVERT_WIRE
|
|
|
|
|
|
def test_migration_adds_nullable_advert_blob_without_losing_existing_contacts(tmp_path):
|
|
handler = SQLiteHandler(tmp_path)
|
|
conn = sqlite3.connect(str(handler.sqlite_path))
|
|
conn.execute(
|
|
"DELETE FROM migrations "
|
|
"WHERE migration_name = 'add_last_advert_packet_to_companion_contacts'"
|
|
)
|
|
conn.execute("ALTER TABLE companion_contacts RENAME TO companion_contacts_old")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE companion_contacts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
companion_hash TEXT NOT NULL,
|
|
pubkey BLOB NOT NULL,
|
|
name TEXT NOT NULL,
|
|
adv_type INTEGER NOT NULL DEFAULT 0,
|
|
flags INTEGER NOT NULL DEFAULT 0,
|
|
out_path_len INTEGER NOT NULL DEFAULT -1,
|
|
out_path BLOB,
|
|
last_advert_timestamp INTEGER NOT NULL DEFAULT 0,
|
|
lastmod INTEGER NOT NULL DEFAULT 0,
|
|
gps_lat REAL NOT NULL DEFAULT 0,
|
|
gps_lon REAL NOT NULL DEFAULT 0,
|
|
sync_since INTEGER NOT NULL DEFAULT 0,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO companion_contacts "
|
|
"(companion_hash, pubkey, name, updated_at) VALUES (?, ?, ?, ?)",
|
|
(COMPANION_HASH, MESHCORE_ADVERT_PUBKEY, "old-contact", 1.0),
|
|
)
|
|
conn.execute("DROP TABLE companion_contacts_old")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
migrated = SQLiteHandler(tmp_path)
|
|
columns = {
|
|
row[1]
|
|
for row in sqlite3.connect(str(migrated.sqlite_path))
|
|
.execute("PRAGMA table_info(companion_contacts)")
|
|
.fetchall()
|
|
}
|
|
assert "last_advert_packet" in columns
|
|
rows = migrated.companion_load_contacts(COMPANION_HASH)
|
|
assert rows is not None and rows[0]["name"] == "old-contact"
|
|
assert rows[0]["last_advert_packet"] is None
|
|
|
|
|
|
def test_persistence_adapter_keeps_raw_advert_bytes():
|
|
contact = Contact(
|
|
public_key=MESHCORE_ADVERT_PUBKEY,
|
|
name="FwmVec",
|
|
last_advert_packet=MESHCORE_ADVERT_WIRE,
|
|
)
|
|
record = CompanionFrameServer._contact_to_dict(contact)
|
|
assert record["last_advert_packet"] == MESHCORE_ADVERT_WIRE
|