mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 16:30:58 +01:00
Stands up Endurain behind the existing Caddy edge and imports RunnerUp activities into it. Endurain is a VIEWER: screen-locker's workout gate still reads the phone over adb and is untouched here, because a WebDAV drop folder is spoofable by anything that can write to ~/cloud and must not become a screen-unlock authority. Primary path is phone -> WebDAV (existing dufs) -> ~/cloud/RunnerUp/ -> importer -> Endurain, with an adb pull as fallback when a run has not been uploaded yet. A systemd user timer runs the importer every 15 min. Findings that shaped the implementation, each verified against the running system rather than assumed: - Endurain moved to Codeberg; the GitHub repo is a read-only mirror stale at v0.17.7 against v0.19.0. The image is pulled from Codeberg. - postgres:18 moved PGDATA to /var/lib/postgresql/18/docker. Upstream's compose example still mounts the pre-18 path, which fails as an unexplained "container is unhealthy"; the real cause is only visible in docker logs. - Endurain seeds a hardcoded admin/admin via an Alembic migration with no env override, and endurain.kuhy.duckdns.org already resolved publicly. Publishing is therefore a separate script that refuses to install the Caddy site while those credentials still work. - Endurain performs NO duplicate detection, so the importer owns dedupe on two axes: a content hash, and an activity key that collapses the .gpx/.tcx pair RunnerUp writes for a single run (different bytes, same run). - Ambiguous uploads (5xx/timeout) are never re-queued, since the activity may already have been committed; only definite 4xx rejections are staged for retry. The importer exits non-zero so the timer surfaces the problem. - RunnerUp has no background sync at all, so every upload is a manual tap. - Its WebDAV synchronizer needs HTTPS with a real cert (Android blocks cleartext at targetSdk 36; self-signed is broken upstream) and a URL with no trailing slash, and cannot MKCOL against dufs - so the inbox must pre-exist. The phone gets a dufs credential scoped to /RunnerUp, verified unable to read Keepass or gdrive_backups. Verified: 52 tests at 100% branch coverage; the timer imports 4 real runs and re-runs create zero duplicates; https://endurain.kuhy.duckdns.org serves 200 with a valid cert and the other edge sites are unaffected; screen-locker's suite still passes 1090 tests at 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
180 lines
5.3 KiB
Python
180 lines
5.3 KiB
Python
"""End-to-end orchestration: environment wiring and exit status."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from python_pkg.endurain_import.__main__ import _route_rejected, main
|
|
from python_pkg.endurain_import.upload import Outcome, Result
|
|
|
|
|
|
@pytest.fixture
|
|
def inbox(tmp_path: Path) -> Path:
|
|
path = tmp_path / "inbox"
|
|
path.mkdir()
|
|
return path
|
|
|
|
|
|
def _file(inbox: Path, name: str = "RunnerUp_ts_Running.tcx") -> Path:
|
|
path = inbox / name
|
|
path.write_text("<xml/>")
|
|
return path
|
|
|
|
|
|
class _StubMainClient:
|
|
"""Stands in for EndurainClient across a whole main() run."""
|
|
|
|
def __init__(self, result: Result, *, reachable: bool = True) -> None:
|
|
self._result = result
|
|
self._reachable = reachable
|
|
|
|
def about(self) -> dict[str, object]:
|
|
if not self._reachable:
|
|
message = "connection refused"
|
|
raise OSError(message)
|
|
return {"version": "test"}
|
|
|
|
def upload(self, _path: Path) -> Result:
|
|
return self._result
|
|
|
|
|
|
def _env(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path, **extra: str
|
|
) -> None:
|
|
monkeypatch.setenv("ENDURAIN_API_KEY", "k")
|
|
monkeypatch.setenv("ENDURAIN_INBOX", str(inbox))
|
|
monkeypatch.setenv("ENDURAIN_STATE", str(tmp_path / "state"))
|
|
monkeypatch.setenv("ENDURAIN_NO_ADB", "1")
|
|
for key, value in extra.items():
|
|
monkeypatch.setenv(key, value)
|
|
|
|
|
|
def _patch_client(monkeypatch: pytest.MonkeyPatch, client: _StubMainClient) -> None:
|
|
import python_pkg.endurain_import.__main__ as mod
|
|
|
|
monkeypatch.setattr(mod, "EndurainClient", lambda *_a, **_k: client)
|
|
|
|
|
|
def test_main_returns_one_when_unreachable(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
_patch_client(
|
|
monkeypatch,
|
|
_StubMainClient(Result(Outcome.OK, 1, "ok"), reachable=False),
|
|
)
|
|
assert main() == 1
|
|
|
|
|
|
def test_main_succeeds_with_empty_inbox(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
_patch_client(monkeypatch, _StubMainClient(Result(Outcome.OK, 1, "ok")))
|
|
assert main() == 0
|
|
|
|
|
|
def test_main_imports_and_returns_zero(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_file(inbox)
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
_patch_client(monkeypatch, _StubMainClient(Result(Outcome.OK, 3, "ok")))
|
|
assert main() == 0
|
|
assert (inbox / "processed" / "RunnerUp_ts_Running.tcx").exists()
|
|
|
|
|
|
def test_main_fails_closed_on_ambiguous(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_file(inbox)
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
_patch_client(
|
|
monkeypatch, _StubMainClient(Result(Outcome.AMBIGUOUS, None, "timeout"))
|
|
)
|
|
assert main() == 1
|
|
|
|
|
|
def test_main_fails_closed_on_rejection(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_file(inbox)
|
|
_env(monkeypatch, tmp_path, inbox, ENDURAIN_BULK_DIR=str(tmp_path / "bulk"))
|
|
_patch_client(monkeypatch, _StubMainClient(Result(Outcome.REJECTED, None, "400")))
|
|
assert main() == 1
|
|
|
|
|
|
def test_main_prefers_tcx_over_gpx(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
"""When both formats are pending, the richer TCX must be the one uploaded."""
|
|
stem = "RunnerUp_2026-08-22-23-51-04_Running"
|
|
(inbox / f"{stem}.gpx").write_text("<gpx/>")
|
|
(inbox / f"{stem}.tcx").write_text("<tcx/>")
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
|
|
uploaded: list[str] = []
|
|
|
|
class _Recorder(_StubMainClient):
|
|
def upload(self, path: Path) -> Result:
|
|
uploaded.append(path.name)
|
|
return Result(Outcome.OK, 1, "ok")
|
|
|
|
_patch_client(monkeypatch, _Recorder(Result(Outcome.OK, 1, "ok")))
|
|
assert main() == 0
|
|
assert uploaded == [f"{stem}.tcx"]
|
|
|
|
|
|
def test_main_runs_adb_fallback_when_enabled(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
monkeypatch.delenv("ENDURAIN_NO_ADB")
|
|
called: list[Path] = []
|
|
import python_pkg.endurain_import.__main__ as mod
|
|
|
|
def _record(path: Path) -> list[Path]:
|
|
called.append(path)
|
|
return []
|
|
|
|
monkeypatch.setattr(mod, "pull_from_phone", _record)
|
|
_patch_client(monkeypatch, _StubMainClient(Result(Outcome.OK, 1, "ok")))
|
|
assert main() == 0
|
|
assert called == [inbox]
|
|
|
|
|
|
def test_route_rejected_handles_unwritable_bulk_dir(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""A broken bulk_import must not take down the whole run."""
|
|
path = tmp_path / "f.tcx"
|
|
path.write_text("x")
|
|
|
|
import shutil
|
|
|
|
def _boom(*_a: object, **_k: object) -> None:
|
|
message = "read-only filesystem"
|
|
raise OSError(message)
|
|
|
|
monkeypatch.setattr(shutil, "copy2", _boom)
|
|
_route_rejected(path, tmp_path / "bulk")
|
|
assert path.exists()
|
|
|
|
|
|
def test_main_handles_requests_exception(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
|
|
) -> None:
|
|
import requests
|
|
|
|
_env(monkeypatch, tmp_path, inbox)
|
|
|
|
class _Failing(_StubMainClient):
|
|
def about(self) -> dict[str, object]:
|
|
message = "refused"
|
|
raise requests.ConnectionError(message)
|
|
|
|
_patch_client(monkeypatch, _Failing(Result(Outcome.OK, 1, "ok")))
|
|
assert main() == 1
|