mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-08 06:15:02 +02:00
Track advert path and use in mesh visualizer
Track advert path and use in mesh visualizer
This commit is contained in:
@@ -15,7 +15,7 @@ from meshcore import EventType
|
||||
|
||||
from app.database import Database
|
||||
from app.radio import radio_manager
|
||||
from app.repository import ContactRepository, MessageRepository
|
||||
from app.repository import ContactRepository, MessageRepository, RepeaterAdvertPathRepository
|
||||
|
||||
# Sample 64-char hex public keys for testing
|
||||
KEY_A = "aa" * 32 # aaaa...aa
|
||||
@@ -208,6 +208,50 @@ class TestGetContact:
|
||||
assert "ambiguous" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
class TestAdvertPaths:
|
||||
"""Test repeater advert path endpoints."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_repeater_advert_paths(self, test_db, client):
|
||||
repeater_key = KEY_A
|
||||
await _insert_contact(repeater_key, "R1", type=2)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_key, "1122", 1000)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_key, "3344", 1010)
|
||||
|
||||
response = await client.get("/api/contacts/repeaters/advert-paths?limit_per_repeater=1")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["repeater_key"] == repeater_key
|
||||
assert len(data[0]["paths"]) == 1
|
||||
assert data[0]["paths"][0]["path"] == "3344"
|
||||
assert data[0]["paths"][0]["next_hop"] == "33"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_contact_advert_paths_for_repeater(self, test_db, client):
|
||||
repeater_key = KEY_A
|
||||
await _insert_contact(repeater_key, "R1", type=2)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_key, "", 1000)
|
||||
|
||||
response = await client.get(f"/api/contacts/{repeater_key}/advert-paths")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["path"] == ""
|
||||
assert data[0]["next_hop"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_contact_advert_paths_rejects_non_repeater(self, test_db, client):
|
||||
await _insert_contact(KEY_A, "Alice", type=1)
|
||||
|
||||
response = await client.get(f"/api/contacts/{KEY_A}/advert-paths")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "not a repeater" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
class TestMarkRead:
|
||||
"""Test POST /api/contacts/{public_key}/mark-read."""
|
||||
|
||||
|
||||
+16
-16
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 21 # All migrations run
|
||||
assert await get_version(conn) == 21
|
||||
assert applied == 22 # All migrations run
|
||||
assert await get_version(conn) == 22
|
||||
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
||||
applied1 = await run_migrations(conn)
|
||||
applied2 = await run_migrations(conn)
|
||||
|
||||
assert applied1 == 21 # All 21 migrations run
|
||||
assert applied1 == 22 # All migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 21
|
||||
assert await get_version(conn) == 22
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -246,8 +246,8 @@ class TestMigration001:
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
# All migrations applied (version incremented) but no error
|
||||
assert applied == 21
|
||||
assert await get_version(conn) == 21
|
||||
assert applied == 22
|
||||
assert await get_version(conn) == 22
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,10 +374,10 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13 (plus 14-21 which also run)
|
||||
# Run migration 13 (plus 14-22 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 9
|
||||
assert await get_version(conn) == 21
|
||||
assert applied == 10
|
||||
assert await get_version(conn) == 22
|
||||
|
||||
# Verify bots array was created with migrated data
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
@@ -497,7 +497,7 @@ class TestMigration018:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 21
|
||||
assert await get_version(conn) == 22
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -571,8 +571,8 @@ class TestMigration018:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 4 # Migrations 18+19+20+21 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 21
|
||||
assert applied == 5 # Migrations 18+19+20+21+22 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 22
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -644,7 +644,7 @@ class TestMigration019:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 21
|
||||
assert await get_version(conn) == 22
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -710,8 +710,8 @@ class TestMigration020:
|
||||
assert (await cursor.fetchone())[0] == "delete"
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 2 # Migrations 20+21
|
||||
assert await get_version(conn) == 21
|
||||
assert applied == 3 # Migrations 20+21+22
|
||||
assert await get_version(conn) == 22
|
||||
|
||||
# Verify WAL mode
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -741,7 +741,7 @@ class TestMigration020:
|
||||
await set_version(conn, 20)
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 1 # Migration 21 still runs
|
||||
assert applied == 2 # Migrations 21+22 still run
|
||||
|
||||
# Still WAL + INCREMENTAL
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from app.database import Database
|
||||
from app.repository import MessageRepository
|
||||
from app.repository import ContactRepository, MessageRepository, RepeaterAdvertPathRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -298,6 +298,68 @@ class TestMessageRepositoryGetAckCount:
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestRepeaterAdvertPathRepository:
|
||||
"""Test storing and retrieving recent unique repeater advert paths."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_observation_upserts_and_tracks_count(self, test_db):
|
||||
repeater_key = "aa" * 32
|
||||
await ContactRepository.upsert({"public_key": repeater_key, "name": "R1", "type": 2})
|
||||
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_key, "112233", 1000)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_key, "112233", 1010)
|
||||
|
||||
paths = await RepeaterAdvertPathRepository.get_recent_for_repeater(repeater_key, limit=10)
|
||||
assert len(paths) == 1
|
||||
assert paths[0].path == "112233"
|
||||
assert paths[0].path_len == 3
|
||||
assert paths[0].next_hop == "11"
|
||||
assert paths[0].first_seen == 1000
|
||||
assert paths[0].last_seen == 1010
|
||||
assert paths[0].heard_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prunes_to_most_recent_n_unique_paths(self, test_db):
|
||||
repeater_key = "bb" * 32
|
||||
await ContactRepository.upsert({"public_key": repeater_key, "name": "R2", "type": 2})
|
||||
|
||||
await RepeaterAdvertPathRepository.record_observation(
|
||||
repeater_key, "aa", 1000, max_paths_per_repeater=2
|
||||
)
|
||||
await RepeaterAdvertPathRepository.record_observation(
|
||||
repeater_key, "bb", 1001, max_paths_per_repeater=2
|
||||
)
|
||||
await RepeaterAdvertPathRepository.record_observation(
|
||||
repeater_key, "cc", 1002, max_paths_per_repeater=2
|
||||
)
|
||||
|
||||
paths = await RepeaterAdvertPathRepository.get_recent_for_repeater(repeater_key, limit=10)
|
||||
assert [p.path for p in paths] == ["cc", "bb"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_for_all_repeaters_respects_limit(self, test_db):
|
||||
repeater_a = "cc" * 32
|
||||
repeater_b = "dd" * 32
|
||||
await ContactRepository.upsert({"public_key": repeater_a, "name": "RA", "type": 2})
|
||||
await ContactRepository.upsert({"public_key": repeater_b, "name": "RB", "type": 2})
|
||||
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_a, "01", 1000)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_a, "02", 1001)
|
||||
await RepeaterAdvertPathRepository.record_observation(repeater_b, "", 1002)
|
||||
|
||||
grouped = await RepeaterAdvertPathRepository.get_recent_for_all_repeaters(
|
||||
limit_per_repeater=1
|
||||
)
|
||||
by_key = {item.repeater_key: item.paths for item in grouped}
|
||||
|
||||
assert repeater_a in by_key
|
||||
assert repeater_b in by_key
|
||||
assert len(by_key[repeater_a]) == 1
|
||||
assert by_key[repeater_a][0].path == "02"
|
||||
assert by_key[repeater_b][0].path == ""
|
||||
assert by_key[repeater_b][0].next_hop is None
|
||||
|
||||
|
||||
class TestAppSettingsRepository:
|
||||
"""Test AppSettingsRepository parsing and migration edge cases."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user