"""Tests for multi-path workout-JSON pull and HTTP fall-through (part 3). Covers the fix for the path mismatch where the app writes ``/sdcard/workout_result.json`` (primary) but the locker only checked the app-external fallback path. The locker now pulls every candidate path, prefers the one dated today, and falls through to the HTTP scan when an ADB pull yields no usable JSON even though a device is connected. """ from __future__ import annotations import json from pathlib import Path import time from unittest.mock import MagicMock, patch from screen_locker._constants import WORKOUT_APP_JSON_REMOTES from screen_locker.tests.conftest import create_locker _PRIMARY, _FALLBACK = WORKOUT_APP_JSON_REMOTES def _today() -> str: """Return today's date as the app stamps it (local YYYY-MM-DD).""" return time.strftime("%Y-%m-%d") def _adb_pull_from(path_contents: dict[str, str]) -> MagicMock: """Build a fake ``_run_adb`` that writes per-remote content to the dest file. ``path_contents`` maps a remote path to the JSON text the device would return for it. A remote absent from the map simulates a missing file (``adb pull`` failure). """ def fake_run_adb(args: list[str]) -> tuple[bool, str]: if not args or args[0] != "pull": return False, "" remote, dest = args[1], args[2] content = path_contents.get(remote) if content is None: return False, "" Path(dest).write_text(content, encoding="utf-8") return True, "" return MagicMock(side_effect=fake_run_adb) class TestPullWorkoutAppJsonMultiPath: """Tests for _pull_workout_app_json across multiple candidate paths.""" def test_prefers_todays_data_over_stale_primary( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """A stale primary file must not shadow today's fallback file.""" locker = create_locker(mock_tk, tmp_path) stale = json.dumps({"date": "2000-01-01", "exercises": ["old"]}) fresh = json.dumps({"date": _today(), "exercises": ["new"]}) fake = _adb_pull_from({_PRIMARY: stale, _FALLBACK: fresh}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is not None assert result["date"] == _today() assert result["exercises"] == ["new"] def test_returns_primary_when_dated_today( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """Primary path dated today is returned without needing the fallback.""" locker = create_locker(mock_tk, tmp_path) fresh = json.dumps({"date": _today(), "exercises": ["primary"]}) fake = _adb_pull_from({_PRIMARY: fresh}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is not None assert result["exercises"] == ["primary"] def test_falls_back_to_stale_when_none_today( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """With no today's file, the first parseable payload is returned.""" locker = create_locker(mock_tk, tmp_path) stale = json.dumps({"date": "2000-01-01", "exercises": ["old"]}) fake = _adb_pull_from({_FALLBACK: stale}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is not None assert result["date"] == "2000-01-01" def test_keeps_first_stale_when_multiple_non_today( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """With several non-today files, the first parseable one is kept.""" locker = create_locker(mock_tk, tmp_path) first = json.dumps({"date": "2000-01-01", "exercises": ["primary"]}) second = json.dumps({"date": "1999-12-31", "exercises": ["fallback"]}) fake = _adb_pull_from({_PRIMARY: first, _FALLBACK: second}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is not None assert result["exercises"] == ["primary"] def test_skips_unparseable_payload( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """A corrupt file at one path doesn't block today's file at another.""" locker = create_locker(mock_tk, tmp_path) fresh = json.dumps({"date": _today(), "exercises": ["new"]}) fake = _adb_pull_from({_PRIMARY: "{not valid json", _FALLBACK: fresh}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is not None assert result["date"] == _today() def test_returns_none_when_no_candidate_pulls( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: """Returns None when no candidate path yields a file.""" locker = create_locker(mock_tk, tmp_path) fake = _adb_pull_from({}) with patch.object(locker, "_run_adb", fake): result = locker._pull_workout_app_json() assert result is None