mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Add outgoing message region tagging. Closes #35.
This commit is contained in:
+88
-24
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 33 # All migrations run
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 34 # All migrations run
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# 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 == 33 # All migrations run
|
||||
assert applied1 == 34 # All migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 33
|
||||
assert await get_version(conn) == 34
|
||||
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 == 33
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 34
|
||||
assert await get_version(conn) == 34
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,10 +374,10 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13 (plus 14-33 which also run)
|
||||
# Run migration 13 (plus 14-34 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 21
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 22
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# 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) == 33
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -575,8 +575,8 @@ class TestMigration018:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 16 # Migrations 18-33 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 17 # Migrations 18-34 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 34
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -648,7 +648,7 @@ class TestMigration019:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 33
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -714,8 +714,8 @@ class TestMigration020:
|
||||
assert (await cursor.fetchone())[0] == "delete"
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 14 # Migrations 20-33
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 15 # Migrations 20-34
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify WAL mode
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -745,7 +745,7 @@ class TestMigration020:
|
||||
await set_version(conn, 20)
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 13 # Migrations 21-33 still run
|
||||
assert applied == 14 # Migrations 21-34 still run
|
||||
|
||||
# Still WAL + INCREMENTAL
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -803,8 +803,8 @@ class TestMigration028:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 6
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 7
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify payload_hash column is now BLOB
|
||||
cursor = await conn.execute("PRAGMA table_info(raw_packets)")
|
||||
@@ -873,8 +873,8 @@ class TestMigration028:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 6 # Version still bumped
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 7 # Version still bumped
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify data unchanged
|
||||
cursor = await conn.execute("SELECT payload_hash FROM raw_packets")
|
||||
@@ -923,8 +923,8 @@ class TestMigration032:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 2
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 3
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify all columns exist with correct defaults
|
||||
cursor = await conn.execute(
|
||||
@@ -943,6 +943,70 @@ class TestMigration032:
|
||||
await conn.close()
|
||||
|
||||
|
||||
class TestMigration034:
|
||||
"""Test migration 034: add flood_scope column to app_settings."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_adds_flood_scope_column(self):
|
||||
"""Migration adds flood_scope column with empty string default."""
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
conn.row_factory = aiosqlite.Row
|
||||
try:
|
||||
await set_version(conn, 33)
|
||||
|
||||
# Create app_settings without flood_scope (pre-migration schema)
|
||||
await conn.execute("""
|
||||
CREATE TABLE app_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
max_radio_contacts INTEGER DEFAULT 200,
|
||||
favorites TEXT DEFAULT '[]',
|
||||
auto_decrypt_dm_on_advert INTEGER DEFAULT 0,
|
||||
sidebar_sort_order TEXT DEFAULT 'recent',
|
||||
last_message_times TEXT DEFAULT '{}',
|
||||
preferences_migrated INTEGER DEFAULT 0,
|
||||
advert_interval INTEGER DEFAULT 0,
|
||||
last_advert_time INTEGER DEFAULT 0,
|
||||
bots TEXT DEFAULT '[]',
|
||||
mqtt_broker_host TEXT DEFAULT '',
|
||||
mqtt_broker_port INTEGER DEFAULT 1883,
|
||||
mqtt_username TEXT DEFAULT '',
|
||||
mqtt_password TEXT DEFAULT '',
|
||||
mqtt_use_tls INTEGER DEFAULT 0,
|
||||
mqtt_tls_insecure INTEGER DEFAULT 0,
|
||||
mqtt_topic_prefix TEXT DEFAULT 'meshcore',
|
||||
mqtt_publish_messages INTEGER DEFAULT 0,
|
||||
mqtt_publish_raw_packets INTEGER DEFAULT 0,
|
||||
community_mqtt_enabled INTEGER DEFAULT 0,
|
||||
community_mqtt_iata TEXT DEFAULT '',
|
||||
community_mqtt_broker_host TEXT DEFAULT 'mqtt-us-v1.letsmesh.net',
|
||||
community_mqtt_broker_port INTEGER DEFAULT 443,
|
||||
community_mqtt_email TEXT DEFAULT ''
|
||||
)
|
||||
""")
|
||||
await conn.execute("INSERT INTO app_settings (id) VALUES (1)")
|
||||
# Channels table needed for migration 33
|
||||
await conn.execute("""
|
||||
CREATE TABLE channels (
|
||||
key TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
is_hashtag INTEGER DEFAULT 0,
|
||||
on_radio INTEGER DEFAULT 0
|
||||
)
|
||||
""")
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 1
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
# Verify column exists with correct default
|
||||
cursor = await conn.execute("SELECT flood_scope FROM app_settings WHERE id = 1")
|
||||
row = await cursor.fetchone()
|
||||
assert row["flood_scope"] == ""
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
class TestMigration033:
|
||||
"""Test migration 033: seed #remoteterm channel."""
|
||||
|
||||
@@ -975,8 +1039,8 @@ class TestMigration033:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 1
|
||||
assert await get_version(conn) == 33
|
||||
assert applied == 2
|
||||
assert await get_version(conn) == 34
|
||||
|
||||
cursor = await conn.execute(
|
||||
"SELECT key, name, is_hashtag, on_radio FROM channels WHERE key = ?",
|
||||
|
||||
@@ -495,11 +495,13 @@ class TestPostConnectSetupOrdering:
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_runs_before_auto_fetch(self):
|
||||
"""drain_pending_messages must be called BEFORE start_auto_message_fetching."""
|
||||
from app.models import AppSettings
|
||||
from app.radio import RadioManager
|
||||
|
||||
rm = RadioManager()
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.start_auto_message_fetching = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
rm._meshcore = mock_mc
|
||||
|
||||
call_order = []
|
||||
@@ -517,6 +519,11 @@ class TestPostConnectSetupOrdering:
|
||||
patch("app.event_handlers.register_event_handlers"),
|
||||
patch("app.keystore.export_and_store_private_key", new_callable=AsyncMock),
|
||||
patch("app.radio_sync.sync_radio_time", new_callable=AsyncMock),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=AppSettings(),
|
||||
),
|
||||
patch("app.radio_sync.sync_and_offload_all", new_callable=AsyncMock, return_value={}),
|
||||
patch("app.radio_sync.start_periodic_sync"),
|
||||
patch("app.radio_sync.send_advertisement", new_callable=AsyncMock, return_value=False),
|
||||
@@ -537,11 +544,13 @@ class TestPostConnectSetupOrdering:
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_sets_and_clears_in_progress_flag(self):
|
||||
"""is_setup_in_progress is True during setup and False after."""
|
||||
from app.models import AppSettings
|
||||
from app.radio import RadioManager
|
||||
|
||||
rm = RadioManager()
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.start_auto_message_fetching = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
rm._meshcore = mock_mc
|
||||
|
||||
observed_during = None
|
||||
@@ -555,6 +564,11 @@ class TestPostConnectSetupOrdering:
|
||||
patch("app.event_handlers.register_event_handlers"),
|
||||
patch("app.keystore.export_and_store_private_key", new_callable=AsyncMock),
|
||||
patch("app.radio_sync.sync_radio_time", new_callable=AsyncMock),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=AppSettings(),
|
||||
),
|
||||
patch("app.radio_sync.sync_and_offload_all", new_callable=AsyncMock, return_value={}),
|
||||
patch("app.radio_sync.start_periodic_sync"),
|
||||
patch("app.radio_sync.send_advertisement", new_callable=AsyncMock, return_value=False),
|
||||
@@ -606,3 +620,71 @@ class TestPostConnectSetupOrdering:
|
||||
# Should not raise or call any functions
|
||||
await rm.post_connect_setup()
|
||||
assert rm.is_setup_in_progress is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_applied_during_setup(self):
|
||||
"""Non-empty flood_scope from settings is applied during post_connect_setup."""
|
||||
from app.models import AppSettings
|
||||
from app.radio import RadioManager
|
||||
|
||||
rm = RadioManager()
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.start_auto_message_fetching = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
rm._meshcore = mock_mc
|
||||
|
||||
mock_settings = AppSettings(flood_scope="#TestRegion")
|
||||
|
||||
with (
|
||||
patch("app.event_handlers.register_event_handlers"),
|
||||
patch("app.keystore.export_and_store_private_key", new_callable=AsyncMock),
|
||||
patch("app.radio_sync.sync_radio_time", new_callable=AsyncMock),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_settings,
|
||||
),
|
||||
patch("app.radio_sync.sync_and_offload_all", new_callable=AsyncMock, return_value={}),
|
||||
patch("app.radio_sync.start_periodic_sync"),
|
||||
patch("app.radio_sync.send_advertisement", new_callable=AsyncMock, return_value=False),
|
||||
patch("app.radio_sync.start_periodic_advert"),
|
||||
patch("app.radio_sync.drain_pending_messages", new_callable=AsyncMock, return_value=0),
|
||||
patch("app.radio_sync.start_message_polling"),
|
||||
):
|
||||
await rm.post_connect_setup()
|
||||
|
||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_empty_resets_during_setup(self):
|
||||
"""Empty flood_scope calls set_flood_scope("") during post_connect_setup."""
|
||||
from app.models import AppSettings
|
||||
from app.radio import RadioManager
|
||||
|
||||
rm = RadioManager()
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.start_auto_message_fetching = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
rm._meshcore = mock_mc
|
||||
|
||||
mock_settings = AppSettings(flood_scope="")
|
||||
|
||||
with (
|
||||
patch("app.event_handlers.register_event_handlers"),
|
||||
patch("app.keystore.export_and_store_private_key", new_callable=AsyncMock),
|
||||
patch("app.radio_sync.sync_radio_time", new_callable=AsyncMock),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_settings,
|
||||
),
|
||||
patch("app.radio_sync.sync_and_offload_all", new_callable=AsyncMock, return_value={}),
|
||||
patch("app.radio_sync.start_periodic_sync"),
|
||||
patch("app.radio_sync.send_advertisement", new_callable=AsyncMock, return_value=False),
|
||||
patch("app.radio_sync.start_periodic_advert"),
|
||||
patch("app.radio_sync.drain_pending_messages", new_callable=AsyncMock, return_value=0),
|
||||
patch("app.radio_sync.start_message_polling"),
|
||||
):
|
||||
await rm.post_connect_setup()
|
||||
|
||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("")
|
||||
|
||||
@@ -507,6 +507,7 @@ class TestAppSettingsRepository:
|
||||
"community_mqtt_broker_host": "mqtt-us-v1.letsmesh.net",
|
||||
"community_mqtt_broker_port": 443,
|
||||
"community_mqtt_email": "",
|
||||
"flood_scope": "",
|
||||
}
|
||||
)
|
||||
mock_conn.execute = AsyncMock(return_value=mock_cursor)
|
||||
|
||||
@@ -192,6 +192,76 @@ class TestUpdateSettings:
|
||||
assert settings.community_mqtt_iata == ""
|
||||
assert settings.community_mqtt_email == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_round_trip(self, test_db):
|
||||
"""Flood scope should be saved and retrieved correctly."""
|
||||
result = await update_settings(AppSettingsUpdate(flood_scope="#MyRegion"))
|
||||
assert result.flood_scope == "#MyRegion"
|
||||
|
||||
fresh = await AppSettingsRepository.get()
|
||||
assert fresh.flood_scope == "#MyRegion"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_default_empty(self, test_db):
|
||||
"""Fresh DB should have flood_scope as empty string."""
|
||||
settings = await AppSettingsRepository.get()
|
||||
assert settings.flood_scope == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_whitespace_stripped(self, test_db):
|
||||
"""Flood scope should be stripped of whitespace."""
|
||||
result = await update_settings(AppSettingsUpdate(flood_scope=" #MyRegion "))
|
||||
assert result.flood_scope == "#MyRegion"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_applies_to_radio(self, test_db):
|
||||
"""When radio is connected, setting flood_scope calls set_flood_scope on radio."""
|
||||
mock_mc = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
|
||||
mock_rm = AsyncMock()
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_radio_op(name):
|
||||
yield mock_mc
|
||||
|
||||
mock_rm.radio_operation = mock_radio_op
|
||||
|
||||
with patch("app.radio.radio_manager", mock_rm):
|
||||
await update_settings(AppSettingsUpdate(flood_scope="#TestRegion"))
|
||||
|
||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flood_scope_empty_resets_radio(self, test_db):
|
||||
"""Setting flood_scope to empty calls set_flood_scope("") on radio."""
|
||||
# First set a non-empty scope
|
||||
await update_settings(AppSettingsUpdate(flood_scope="#TestRegion"))
|
||||
|
||||
mock_mc = AsyncMock()
|
||||
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||
|
||||
mock_rm = AsyncMock()
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_radio_op(name):
|
||||
yield mock_mc
|
||||
|
||||
mock_rm.radio_operation = mock_radio_op
|
||||
|
||||
with patch("app.radio.radio_manager", mock_rm):
|
||||
await update_settings(AppSettingsUpdate(flood_scope=""))
|
||||
|
||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("")
|
||||
|
||||
|
||||
class TestToggleFavorite:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user