diff --git a/app/migrations.py b/app/migrations.py index cb78940..e075caa 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -261,6 +261,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int: await set_version(conn, 32) applied += 1 + # Migration 33: Seed #remoteterm channel on initial install + if version < 33: + logger.info("Applying migration 33: seed #remoteterm channel") + await _migrate_033_seed_remoteterm_channel(conn) + await set_version(conn, 33) + applied += 1 + if applied > 0: logger.info( "Applied %d migration(s), schema now at version %d", applied, await get_version(conn) @@ -1926,3 +1933,21 @@ async def _migrate_032_add_community_mqtt_columns(conn: aiosqlite.Connection) -> await conn.execute(f"ALTER TABLE app_settings ADD COLUMN {col_name} {col_def}") await conn.commit() + + +async def _migrate_033_seed_remoteterm_channel(conn: aiosqlite.Connection) -> None: + """Seed the #remoteterm hashtag channel so new installs have it by default. + + Uses INSERT OR IGNORE so it's a no-op if the channel already exists + (e.g. existing users who already added it manually). The channels table + is created by the base schema before migrations run, so it always exists + in production. + """ + try: + await conn.execute( + "INSERT OR IGNORE INTO channels (key, name, is_hashtag, on_radio) VALUES (?, ?, ?, ?)", + ("8959AE053F2201801342A1DBDDA184F6", "#remoteterm", 1, 0), + ) + await conn.commit() + except Exception: + logger.debug("Skipping #remoteterm seed (channels table not ready)") diff --git a/tests/test_channels_router.py b/tests/test_channels_router.py index 963d30e..d046105 100644 --- a/tests/test_channels_router.py +++ b/tests/test_channels_router.py @@ -96,9 +96,9 @@ class TestSyncChannelsFromRadio: data = response.json() assert data["synced"] == 2 - # Verify channels in DB + # Verify channels in DB (2 synced + #remoteterm seed) channels = await ChannelRepository.get_all() - assert len(channels) == 2 + assert len(channels) == 3 keys = {ch.key for ch in channels} assert secret_a.hex().upper() in keys diff --git a/tests/test_migrations.py b/tests/test_migrations.py index a64e5aa..8b059bc 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -100,8 +100,8 @@ class TestMigration001: # Run migrations applied = await run_migrations(conn) - assert applied == 32 # All migrations run - assert await get_version(conn) == 32 + assert applied == 33 # All migrations run + assert await get_version(conn) == 33 # 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 == 32 # All migrations run + assert applied1 == 33 # All migrations run assert applied2 == 0 # No migrations on second run - assert await get_version(conn) == 32 + assert await get_version(conn) == 33 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 == 32 - assert await get_version(conn) == 32 + assert applied == 33 + assert await get_version(conn) == 33 finally: await conn.close() @@ -376,8 +376,8 @@ class TestMigration013: # Run migration 13 (plus 14-33 which also run) applied = await run_migrations(conn) - assert applied == 20 - assert await get_version(conn) == 32 + assert applied == 21 + assert await get_version(conn) == 33 # 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) == 32 + assert await get_version(conn) == 33 # Verify autoindex is gone cursor = await conn.execute( @@ -575,8 +575,8 @@ class TestMigration018: await conn.commit() applied = await run_migrations(conn) - assert applied == 15 # Migrations 18-32 run (18+19 skip internally) - assert await get_version(conn) == 32 + assert applied == 16 # Migrations 18-33 run (18+19 skip internally) + assert await get_version(conn) == 33 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) == 32 + assert await get_version(conn) == 33 # 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 == 13 # Migrations 20-32 - assert await get_version(conn) == 32 + assert applied == 14 # Migrations 20-33 + assert await get_version(conn) == 33 # 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 == 12 # Migrations 21-32 still run + assert applied == 13 # Migrations 21-33 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 == 5 - assert await get_version(conn) == 32 + assert applied == 6 + assert await get_version(conn) == 33 # 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 == 5 # Version still bumped - assert await get_version(conn) == 32 + assert applied == 6 # Version still bumped + assert await get_version(conn) == 33 # 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 == 1 - assert await get_version(conn) == 32 + assert applied == 2 + assert await get_version(conn) == 33 # Verify all columns exist with correct defaults cursor = await conn.execute( @@ -941,3 +941,94 @@ class TestMigration032: assert row["community_mqtt_email"] == "" finally: await conn.close() + + +class TestMigration033: + """Test migration 033: seed #remoteterm channel.""" + + @pytest.mark.asyncio + async def test_migration_seeds_remoteterm_channel(self): + """Migration inserts the #remoteterm channel for new installs.""" + conn = await aiosqlite.connect(":memory:") + conn.row_factory = aiosqlite.Row + try: + await set_version(conn, 32) + await conn.execute(""" + CREATE TABLE channels ( + key TEXT PRIMARY KEY, + name TEXT NOT NULL, + is_hashtag INTEGER DEFAULT 0, + on_radio INTEGER DEFAULT 0 + ) + """) + # Minimal app_settings so earlier migrations don't fail + await conn.execute(""" + CREATE TABLE app_settings ( + id INTEGER PRIMARY KEY, + community_mqtt_enabled INTEGER DEFAULT 0, + community_mqtt_iata TEXT DEFAULT '', + community_mqtt_broker_host TEXT DEFAULT '', + community_mqtt_broker_port INTEGER DEFAULT 443, + community_mqtt_email TEXT DEFAULT '' + ) + """) + await conn.commit() + + applied = await run_migrations(conn) + assert applied == 1 + assert await get_version(conn) == 33 + + cursor = await conn.execute( + "SELECT key, name, is_hashtag, on_radio FROM channels WHERE key = ?", + ("8959AE053F2201801342A1DBDDA184F6",), + ) + row = await cursor.fetchone() + assert row is not None + assert row["name"] == "#remoteterm" + assert row["is_hashtag"] == 1 + assert row["on_radio"] == 0 + finally: + await conn.close() + + @pytest.mark.asyncio + async def test_migration_does_not_overwrite_existing_channel(self): + """Migration is a no-op if #remoteterm already exists.""" + conn = await aiosqlite.connect(":memory:") + conn.row_factory = aiosqlite.Row + try: + await set_version(conn, 32) + 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.execute(""" + CREATE TABLE app_settings ( + id INTEGER PRIMARY KEY, + community_mqtt_enabled INTEGER DEFAULT 0, + community_mqtt_iata TEXT DEFAULT '', + community_mqtt_broker_host TEXT DEFAULT '', + community_mqtt_broker_port INTEGER DEFAULT 443, + community_mqtt_email TEXT DEFAULT '' + ) + """) + # Pre-existing channel with on_radio=1 (user added it to radio) + await conn.execute( + "INSERT INTO channels (key, name, is_hashtag, on_radio) VALUES (?, ?, ?, ?)", + ("8959AE053F2201801342A1DBDDA184F6", "#remoteterm", 1, 1), + ) + await conn.commit() + + await run_migrations(conn) + + cursor = await conn.execute( + "SELECT on_radio FROM channels WHERE key = ?", + ("8959AE053F2201801342A1DBDDA184F6",), + ) + row = await cursor.fetchone() + assert row["on_radio"] == 1 # Not overwritten + finally: + await conn.close() diff --git a/tests/test_statistics.py b/tests/test_statistics.py index acf4ee9..4773550 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -15,7 +15,7 @@ class TestStatisticsEmpty: assert result["contact_count"] == 0 assert result["repeater_count"] == 0 - assert result["channel_count"] == 0 + assert result["channel_count"] == 1 # #remoteterm seed from migration 33 assert result["total_packets"] == 0 assert result["decrypted_packets"] == 0 assert result["undecrypted_packets"] == 0 @@ -67,7 +67,7 @@ class TestStatisticsCounts: await conn.commit() result = await StatisticsRepository.get_all() - assert result["channel_count"] == 1 + assert result["channel_count"] == 2 # test-chan + #remoteterm seed @pytest.mark.asyncio async def test_message_type_counts(self, test_db):