Files
Remote-Terminal-for-MeshCore/tests/test_settings_router.py
2026-03-06 14:44:47 -08:00

186 lines
6.5 KiB
Python

"""Tests for settings router endpoints and validation behavior."""
from unittest.mock import AsyncMock, patch
import pytest
from app.models import AppSettings
from app.repository import AppSettingsRepository
from app.routers.settings import (
AppSettingsUpdate,
FavoriteRequest,
MigratePreferencesRequest,
migrate_preferences,
toggle_favorite,
update_settings,
)
class TestUpdateSettings:
@pytest.mark.asyncio
async def test_forwards_only_provided_fields(self, test_db):
result = await update_settings(
AppSettingsUpdate(
max_radio_contacts=321,
advert_interval=3600,
)
)
assert result.max_radio_contacts == 321
assert result.advert_interval == 3600
@pytest.mark.asyncio
async def test_advert_interval_below_minimum_is_clamped_to_one_hour(self, test_db):
result = await update_settings(AppSettingsUpdate(advert_interval=600))
assert result.advert_interval == 3600
@pytest.mark.asyncio
async def test_advert_interval_zero_stays_disabled(self, test_db):
result = await update_settings(AppSettingsUpdate(advert_interval=0))
assert result.advert_interval == 0
@pytest.mark.asyncio
async def test_advert_interval_above_minimum_is_preserved(self, test_db):
result = await update_settings(AppSettingsUpdate(advert_interval=86400))
assert result.advert_interval == 86400
@pytest.mark.asyncio
async def test_empty_patch_returns_current_settings(self, test_db):
result = await update_settings(AppSettingsUpdate())
# Should return default settings without error
assert isinstance(result, AppSettings)
assert result.max_radio_contacts == 200 # default
@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
async def test_adds_when_not_favorited(self, test_db):
request = FavoriteRequest(type="contact", id="aa" * 32)
result = await toggle_favorite(request)
assert len(result.favorites) == 1
assert result.favorites[0].type == "contact"
assert result.favorites[0].id == "aa" * 32
@pytest.mark.asyncio
async def test_removes_when_already_favorited(self, test_db):
# Pre-add a favorite
await AppSettingsRepository.add_favorite("channel", "ABCD")
request = FavoriteRequest(type="channel", id="ABCD")
result = await toggle_favorite(request)
assert result.favorites == []
class TestMigratePreferences:
@pytest.mark.asyncio
async def test_maps_frontend_payload_and_returns_migrated_true(self, test_db):
request = MigratePreferencesRequest(
favorites=[FavoriteRequest(type="contact", id="aa" * 32)],
sort_order="alpha",
last_message_times={"contact-aaaaaaaaaaaa": 123},
)
response = await migrate_preferences(request)
assert response.migrated is True
assert response.settings.preferences_migrated is True
assert response.settings.sidebar_sort_order == "alpha"
assert len(response.settings.favorites) == 1
assert response.settings.favorites[0].type == "contact"
assert response.settings.favorites[0].id == "aa" * 32
assert response.settings.last_message_times == {"contact-aaaaaaaaaaaa": 123}
@pytest.mark.asyncio
async def test_returns_migrated_false_when_already_done(self, test_db):
# First migration
first_request = MigratePreferencesRequest(
favorites=[FavoriteRequest(type="contact", id="bb" * 32)],
sort_order="recent",
last_message_times={},
)
await migrate_preferences(first_request)
# Second attempt should be no-op
second_request = MigratePreferencesRequest(
favorites=[],
sort_order="recent",
last_message_times={},
)
response = await migrate_preferences(second_request)
assert response.migrated is False
assert response.settings.preferences_migrated is True