Files
Stride/tests/test_web_sync.py
T

146 lines
5.7 KiB
Python

import io
import json
from datetime import UTC, datetime, timedelta
from unittest.mock import Mock
import pytest
from cryptography.fernet import Fernet
from garminconnect import GarminConnectAuthenticationError
from app import create_app
from app.extensions import db
from app.models import GarminConnection, User
from app.sync import cipher, run_due, session_lock
def test_optional_recovery_failure_does_not_break_activity_sync(connected, monkeypatch):
app, _client, _api, _sync = connected
def failed_health(*_args):
raise RuntimeError("PRIVATE-PROVIDER-RESPONSE")
monkeypatch.setattr("app.recovery.sync_health", failed_health)
with app.app_context():
connection = db.session.scalar(db.select(GarminConnection))
connection.health_enabled = True
db.session.commit()
assert run_due() == 1
assert connection.failures == 0 and connection.last_success
assert "Activity sync completed" in connection.health_message
assert "PRIVATE-PROVIDER" not in connection.health_message
def test_recovery_login_failure_preserves_encrypted_session(connected):
app, _client, api, _sync = connected
api.login.side_effect = RuntimeError("PRIVATE-LOGIN-RESPONSE")
with app.app_context():
original = db.session.scalar(db.select(GarminConnection)).tokens
result = app.test_cli_runner().invoke(args=["backfill-health", "--email", "owner@example.test", "--days", "2"])
assert result.exit_code == 1 and "PRIVATE-LOGIN" not in result.output
with app.app_context():
connection = db.session.scalar(db.select(GarminConnection))
assert connection.tokens == original
assert not connection.health_enabled
assert cipher().decrypt(connection.tokens.encode())
@pytest.fixture
def connected(tmp_path, monkeypatch):
app = create_app(
{
"TESTING": True,
"SQLALCHEMY_DATABASE_URI": "sqlite://",
"SECRET_KEY": "testing",
"WTF_CSRF_ENABLED": False,
"RATELIMIT_ENABLED": False,
"LOCAL_SETUP": False,
"TOKEN_ENCRYPTION_KEY": Fernet.generate_key().decode(),
"DATA_ROOT": str(tmp_path / "data"),
}
)
app.instance_path = str(tmp_path / "instance")
api = Mock(profile_id=123)
api.client.dumps.return_value = '{"rotated":"private-session"}'
monkeypatch.setattr("app.sync.Garmin", lambda: api)
sync = Mock(return_value={"failed": 0})
monkeypatch.setattr("app.sync.sync_activities", sync)
with app.app_context():
db.create_all()
user = User(username="owner", email="owner@example.test", display_name="Owner")
user.set_password("long-test-password")
db.session.add(user)
db.session.commit()
client = app.test_client()
client.post("/login", data={"email": "owner@example.test", "password": "long-test-password"})
payload = json.dumps(
{k: "secret-value" for k in ("di_token", "di_refresh_token", "di_client_id")}
)
response = client.post(
"/garmin/connect", data={"tokens": (io.BytesIO(payload.encode()), "session.json")}
)
assert response.status_code == 302
return app, client, api, sync
def test_manual_scheduled_encryption_and_disconnect(connected):
app, client, _api, sync = connected
with app.app_context():
connection = db.session.scalar(db.select(GarminConnection))
assert "private-session" not in connection.tokens
assert run_due() == 1
assert connection.last_success
assert not connection.pending
assert run_due() == 0
connection.next_sync = datetime.now(UTC) - timedelta(minutes=1)
db.session.commit()
assert run_due() == 1
assert sync.call_count == 2
client.post("/garmin/schedule", data={"enabled": "no"})
client.post("/garmin/sync")
with app.app_context():
assert run_due() == 1
assert sync.call_count == 3
client.post("/garmin/disconnect")
with app.app_context():
assert run_due() == 0
assert db.session.scalar(db.select(GarminConnection)) is None
assert db.session.get(User, 1).local_account_key is None
def test_retry_and_rotated_tokens_survive_failure(connected):
app, client, _api, sync = connected
api = _api
sync.side_effect = RuntimeError("provider secret must not appear")
with app.app_context():
assert run_due() == 1
connection = db.session.scalar(db.select(GarminConnection))
assert connection.failures == 1
assert "provider secret" not in connection.message
assert (
Fernet(app.config["TOKEN_ENCRYPTION_KEY"]).decrypt(connection.tokens.encode())
== api.client.dumps.return_value.encode()
)
assert run_due() == 0
client.post("/garmin/sync")
api.login.side_effect = GarminConnectAuthenticationError("private error")
with app.app_context():
run_due()
connection = db.session.scalar(db.select(GarminConnection))
assert not connection.scheduled
assert "expired" in connection.message
def test_lock_prevents_overlapping_sync_and_disconnect(connected):
app, client, _api, sync = connected
with app.app_context(), session_lock(1):
run_due()
sync.assert_not_called()
response = client.post("/garmin/disconnect", follow_redirects=True)
assert b"sync is running" in response.data
assert db.session.scalar(db.select(GarminConnection)) is not None
def test_sync_requires_login_and_csrf(connected):
app, client, _api, _sync = connected
assert app.test_client().post("/garmin/sync").status_code == 302
app.config["WTF_CSRF_ENABLED"] = True
assert client.post("/garmin/sync").status_code == 400