import logging from sqlalchemy import text from app.db.models import Activity, ActivityStatus def test_mfa_code_is_used_once_and_not_persisted(authenticated_client, fake_sync_manager, app, caplog) -> None: with caplog.at_level(logging.DEBUG): response = authenticated_client.post( "/users/1/garmin-mfa", data={"csrf_token": authenticated_client.csrf_token, "code": "123456"}, ) assert response.status_code == 200 assert fake_sync_manager.mfa_calls == [(1, "123456")] # Defense-in-depth: the MFA code must never end up written to the real # app database (the one authenticated_client's requests actually hit), # not some unrelated in-memory db. with app.state.session_factory() as session: persisted_text = " ".join(str(row) for row in session.execute(text("select * from sync_runs")).all()) assert "123456" not in persisted_text # The check that actually matters: the code must never be logged, # regardless of which SyncManager implementation is in play. assert "123456" not in caplog.text def test_mfa_code_rejects_empty_code(authenticated_client, fake_sync_manager) -> None: response = authenticated_client.post( "/users/1/garmin-mfa", data={"csrf_token": authenticated_client.csrf_token, "code": " "}, ) assert response.status_code == 400 assert fake_sync_manager.mfa_calls == [] def test_mfa_code_rejects_overlong_code(authenticated_client, fake_sync_manager) -> None: response = authenticated_client.post( "/users/1/garmin-mfa", data={"csrf_token": authenticated_client.csrf_token, "code": "1" * 21}, ) assert response.status_code == 400 assert fake_sync_manager.mfa_calls == [] def _seed_activity(app, *, status: ActivityStatus, last_completed_stage: ActivityStatus, retryable: bool, last_error: str | None = None) -> tuple[int, int]: """Seed a real Activity (and its owning user) in the app fixture's actual database -- the same database authenticated_client's HTTP requests hit -- and return (user_id, activity_id).""" from app.db.repositories import ActivityRepository, UserRepository with app.state.session_factory() as session: user = UserRepository(session).create( name="MFA Test User", enabled=True, mywhoosh_email_enc="mw@example.com", mywhoosh_password_enc="mw-pass", garmin_email_enc="garmin@example.com", garmin_password_enc="garmin-pass", ) activity_repo = ActivityRepository(session) activity, _ = activity_repo.get_or_create_discovered( user_id=user.id, mywhoosh_activity_id="mw-activity-1", activity_name="Test Activity", activity_timestamp=None, ) activity.status = status activity.last_completed_stage = last_completed_stage activity.retryable = retryable if last_error is not None: activity.last_error = last_error session.commit() return user.id, activity.id def test_retry_resets_and_calls_sync_for_retryable_failed_activity(authenticated_client, fake_sync_manager, app) -> None: user_id, activity_id = _seed_activity( app, status=ActivityStatus.FAILED, last_completed_stage=ActivityStatus.CONVERTED, retryable=True, last_error="some transient error", ) response = authenticated_client.post( f"/activities/{activity_id}/retry", data={"csrf_token": authenticated_client.csrf_token}, ) assert response.status_code == 200 assert fake_sync_manager.user_calls == [user_id] with app.state.session_factory() as session: reloaded = session.get(Activity, activity_id) assert reloaded.status == ActivityStatus.CONVERTED assert reloaded.last_error is None def test_retry_rejects_non_retryable_activity(authenticated_client, fake_sync_manager, app) -> None: user_id, activity_id = _seed_activity( app, status=ActivityStatus.FAILED, last_completed_stage=ActivityStatus.CONVERTED, retryable=False, last_error="permanent failure", ) response = authenticated_client.post( f"/activities/{activity_id}/retry", data={"csrf_token": authenticated_client.csrf_token}, ) assert response.status_code == 409 assert fake_sync_manager.user_calls == [] with app.state.session_factory() as session: reloaded = session.get(Activity, activity_id) assert reloaded.status == ActivityStatus.FAILED assert reloaded.retryable is False assert reloaded.last_error == "permanent failure" def test_retry_rejects_non_failed_activity(authenticated_client, fake_sync_manager, app) -> None: user_id, activity_id = _seed_activity( app, status=ActivityStatus.DISCOVERED, last_completed_stage=ActivityStatus.DISCOVERED, retryable=True, ) response = authenticated_client.post( f"/activities/{activity_id}/retry", data={"csrf_token": authenticated_client.csrf_token}, ) assert response.status_code == 409 assert fake_sync_manager.user_calls == []