screen-locker/screen_locker/tests/test_runnerup_webdav_source.py
2026-11-10 19:32:43 +01:00

188 lines
7.4 KiB
Python

"""RunnerUp exports are read from the WebDAV drop directory, phone optional.
RunnerUp's WebDAV synchroniser uploads every export to dufs on this machine
(``RUNNERUP_WEBDAV_DIRS``), minutes after the run ends, and endurain-import
moves them to ``processed/``. Reading those files credits the run without the
phone being adb-reachable: on 2026-09-16 the TCX was on disk at 20:08 and the
phone was only plugged in at 20:25.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker.tests._activity_helpers import tcx
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
# A 5.8 km / 70 min run WITH its trackpoints: lap totals alone credit nothing.
_TCX = tcx("Running", "18:58", moving_min=70, metres=5800)
def _webdav_tree(tmp_path: Path) -> tuple[Path, Path]:
inbox = tmp_path / "cloud" / "RunnerUp"
processed = inbox / "processed"
processed.mkdir(parents=True)
return inbox, processed
class TestLocalExportDiscovery:
"""``_find_runnerup_exports_for_date`` / ``_has_runnerup_source``."""
def test_local_files_found_without_adb(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Inbox and processed/ are both searched; adb is never asked."""
locker = create_locker(mock_tk, tmp_path)
inbox, processed = _webdav_tree(tmp_path)
(inbox / "RunnerUp_Pixel_6a_2026-09-16-18-58-28_Running.tcx").write_text(_TCX)
(processed / "RunnerUp_2026-09-16-18-58-28_Running.tcx").write_text(_TCX)
(inbox / "RunnerUp_2026-09-16-18-58-28_Running.gpx").write_text("<gpx/>")
(processed / "RunnerUp_2026-09-01-18-04-41_Running.tcx").write_text(_TCX)
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
object.__setattr__(locker, "_run_adb", MagicMock())
with patch(
"screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS",
(inbox, processed),
):
assert locker._has_runnerup_source() is True
found = locker._find_runnerup_exports_for_date("2026-09-16")
assert found == [
str(inbox / "RunnerUp_Pixel_6a_2026-09-16-18-58-28_Running.tcx"),
str(processed / "RunnerUp_2026-09-16-18-58-28_Running.tcx"),
]
locker._run_adb.assert_not_called()
def test_phone_paths_appended_after_local_when_adb_present(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""With a phone attached the adb listing follows the local copies."""
locker = create_locker(mock_tk, tmp_path)
inbox, processed = _webdav_tree(tmp_path)
(inbox / "RunnerUp_2026-09-16-18-58-28_Running.tcx").write_text(_TCX)
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=True))
object.__setattr__(
locker,
"_run_adb",
MagicMock(
return_value=(True, "RunnerUp_2026-09-16-18-58-28_Running.tcx\n")
),
)
with (
patch(
"screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS",
(inbox, processed),
),
patch(
"screen_locker._runnerup_verification.RUNNERUP_EXPORT_DIRS",
("/sdcard/Documents/RunnerUp",),
),
):
found = locker._find_runnerup_exports_for_date("2026-09-16")
assert found == [
str(inbox / "RunnerUp_2026-09-16-18-58-28_Running.tcx"),
"/sdcard/Documents/RunnerUp/RunnerUp_2026-09-16-18-58-28_Running.tcx",
]
def test_no_source_when_dir_missing_and_no_phone(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Neither directory nor phone → no source, verify reports no_phone."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
with (
patch(
"screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS",
(tmp_path / "absent",),
),
patch(
"screen_locker._runnerup_verification.check_clock_skew",
return_value=(True, ""),
),
):
assert locker._has_runnerup_source() is False
status, _ = locker._verify_runnerup_workout()
assert not locker._scan_and_fill_week_runnerup(tmp_path / "log.json")
assert status == "no_phone"
class TestLocalVerifyAndParse:
"""The local file is parsed in place; the root-DB fallback needs adb."""
def test_verify_today_from_local_file_without_adb(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""A today's TCX on disk verifies with the phone detached."""
locker = create_locker(mock_tk, tmp_path)
inbox, processed = _webdav_tree(tmp_path)
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
object.__setattr__(locker, "_run_adb", MagicMock())
with (
patch(
"screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS",
(inbox, processed),
),
patch(
"screen_locker._runnerup_verification.check_clock_skew",
return_value=(True, ""),
),
patch("screen_locker._runnerup_verification.today_str") as today,
):
today.return_value = "2026-09-16"
(inbox / "RunnerUp_2026-09-16-18-58-28_Running.tcx").write_text(_TCX)
status, message = locker._verify_runnerup_workout()
assert status == "verified"
assert "5.8 km" in message
locker._run_adb.assert_not_called()
def test_no_file_and_no_adb_is_not_verified_without_db_fallback(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""An empty drop dir with no phone cannot fall back to the root DB."""
locker = create_locker(mock_tk, tmp_path)
inbox, processed = _webdav_tree(tmp_path)
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
object.__setattr__(locker, "_verify_runnerup_via_db", MagicMock())
with (
patch(
"screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS",
(inbox, processed),
),
patch(
"screen_locker._runnerup_verification.check_clock_skew",
return_value=(True, ""),
),
):
status, message = locker._verify_runnerup_workout()
assert status == "not_verified"
assert "not adb-reachable" in message
locker._verify_runnerup_via_db.assert_not_called()
def test_pull_and_parse_reads_local_path_in_place(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""A path that exists locally is parsed without any adb pull."""
locker = create_locker(mock_tk, tmp_path)
tcx_file = tmp_path / "run.tcx"
tcx_file.write_text(_TCX)
object.__setattr__(locker, "_run_adb", MagicMock())
data = locker._pull_and_parse_tcx(str(tcx_file))
assert data is not None
assert data["distance_m"] == 5800.0
locker._run_adb.assert_not_called()