This commit is contained in:
Bastian Wagner
2026-08-15 20:54:35 +02:00
parent 2aba1265af
commit 420d089760
9 changed files with 178 additions and 8 deletions

View File

@@ -9,7 +9,7 @@ from sqlalchemy.pool import StaticPool
from app.config import Settings
from app.db.models import Activity, Base, HealthState
from app.db.repositories import ActivityRepository, SyncRunRepository, UserRepository
from app.db.repositories import ActivityRepository, SyncRunRepository, SystemLogRepository, UserRepository
from app.main import create_app
@@ -44,6 +44,11 @@ def sync_run_repository(db_session: Session) -> SyncRunRepository:
return SyncRunRepository(db_session)
@pytest.fixture
def system_log_repository(db_session: Session) -> SystemLogRepository:
return SystemLogRepository(db_session)
@pytest.fixture
def app(tmp_path: Path):
settings = Settings(

View File

@@ -64,3 +64,32 @@ def test_activity_external_id_is_unique_per_user(user_repository, activity_repos
assert inserted_again is False
assert created.id == same.id
assert same.status == ActivityStatus.DISCOVERED
def test_system_log_lists_most_recent_first(system_log_repository, user_repository) -> None:
user = user_repository.create(
name="Max",
enabled=True,
health_state=HealthState.HEALTHY,
mywhoosh_email_enc="mw-1",
mywhoosh_password_enc="mw-pw-1",
garmin_email_enc="g-1",
garmin_password_enc="g-pw-1",
)
system_log_repository.add(source="email_notification", message="first failure", user_id=user.id)
system_log_repository.add(source="email_notification", message="second failure", user_id=user.id)
entries = system_log_repository.list_recent()
assert [entry.message for entry in entries] == ["second failure", "first failure"]
assert entries[0].source == "email_notification"
assert entries[0].user_id == user.id
def test_system_log_respects_limit(system_log_repository) -> None:
for i in range(5):
system_log_repository.add(source="test", message=f"entry {i}")
entries = system_log_repository.list_recent(limit=2)
assert len(entries) == 2

View File

@@ -4,7 +4,7 @@ import pytest
from sqlalchemy import select
from app.db.models import ActivityStatus, HealthState, SyncRun, SyncRunStatus, SyncUser
from app.db.repositories import UserRepository
from app.db.repositories import SystemLogRepository, UserRepository
from app.garmin.uploader import UploadResult
from app.mywhoosh.client import MyWhooshDeviceConflictError
from app.mywhoosh.models import MyWhooshActivity
@@ -305,6 +305,44 @@ async def test_does_not_renotify_for_unresolved_unchanged_reason(session_factory
assert len(notifier.sent) == 1
class FailingNotifier:
def send(self, *, to_address: str, subject: str, body: str) -> None:
raise RuntimeError("SMTP connection refused")
@pytest.mark.asyncio
async def test_email_send_failure_is_recorded_in_system_log_and_does_not_break_sync(
session_factory, cipher, settings
) -> None:
with session_factory() as session:
user = _create_user(session, cipher)
user.notify_email_enabled = True
user.notification_email = "alerts@example.com"
session.commit()
user_id = user.id
manager = SyncManager(
session_factory=session_factory,
credential_cipher=cipher,
settings=settings,
mywhoosh_factory=lambda token_store: DeviceConflictMyWhooshClient(),
garmin_factory=lambda email, password, tokenstore: FakeGarminUploader(),
fit_converter=FakeFitConverter(),
notifier=FailingNotifier(),
)
outcome = await manager.sync_user(user_id)
assert outcome.status == "failed"
with session_factory() as session:
entries = SystemLogRepository(session).list_recent()
assert len(entries) == 1
assert entries[0].source == "email_notification"
assert entries[0].user_id == user_id
assert "SMTP connection refused" in entries[0].message
assert "alerts@example.com" in entries[0].message
@pytest.mark.asyncio
async def test_mfa_code_reaches_real_garmin_uploader_via_sync_manager(
session_factory, cipher, settings, seeded_user: SyncUser

View File

@@ -1,5 +1,7 @@
from fastapi.testclient import TestClient
from app.db.repositories import SystemLogRepository
def test_manual_sync_calls_shared_manager(authenticated_client, fake_sync_manager) -> None:
response = authenticated_client.post(
@@ -60,3 +62,30 @@ def test_system_page_shows_scheduler_state(app, authenticated_client) -> None:
assert "1.0.0" in response.text
assert "5" in response.text # sync_interval_minutes
assert "0" in response.text # user_count / activity_count fresh DB
class _FakeScheduler:
def __init__(self) -> None:
self.last_tick = None
self.next_tick = None
def test_system_page_shows_empty_log_state(app, authenticated_client) -> None:
app.state.scheduler = _FakeScheduler()
response = authenticated_client.get("/system")
assert response.status_code == 200
assert "No system log entries" in response.text
def test_system_page_shows_recorded_log_entries(app, authenticated_client) -> None:
app.state.scheduler = _FakeScheduler()
with app.state.session_factory() as session:
SystemLogRepository(session).add(
source="email_notification", message="Failed to email alerts@example.com: SMTP timeout"
)
response = authenticated_client.get("/system")
assert response.status_code == 200
assert "email_notification" in response.text
assert "SMTP timeout" in response.text