testsAndMisc/python_pkg/endurain_import/tests/test_remote_dedupe.py
Krzysztof kuhy Rudnicki 5ba2cbd476
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Dedupe endurain imports against postgres when the API cannot answer
Endurain API keys are upload-only by design: the scope allow-list is
frozenset({"activities:upload"}), enforced in both validate_api_key_scopes
and the create schema, and the activities read route is mounted with
validate_access_token rather than validate_access_token_or_api_key. A key
therefore fails at authentication before any scope check, so the HTTP
dedupe check added in 325d5a8b could never succeed and every pass fell
back to the ledger alone. Verified against v0.19.0; upstream documents no
plan to widen the allow-list.

Read the start times out of postgres instead. The container publishes no
host port, so this goes through docker exec -- no new dependency and no
port opened just to read four rows. The HTTP path is still tried first so
the supported route resumes working on its own if the allow-list ever
grows.

None and [] are kept strictly apart: a dead container, timeout or non-zero
exit means "could not ask" and returns to the ledger, while an empty result
set is a real answer. Conflating them would re-upload everything on any
docker hiccup, which is the bug the dedupe check exists to prevent.

Proven with an empty ledger: the known-duplicate TCX was skipped and the
activity count held at 4, and the same file WAS imported with
ENDURAIN_NO_DB=1, so the check is what blocks it. The duplicate that
negative control created was removed afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 15:23:08 +02:00

153 lines
5.0 KiB
Python

"""Skipping runs Endurain already holds.
RunnerUp's own uploader writes straight to the server and leaves no ledger
trace, so the server is the only place these runs are visible.
"""
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import pytest
from python_pkg.endurain_import.__main__ import main
from python_pkg.endurain_import.tests.conftest import (
_env,
_file,
_patch_client,
_StubMainClient,
)
from python_pkg.endurain_import.upload import Outcome, Result
# The name encodes 23:51:04 LOCAL time; the UTC instant depends on the zone,
# so these derive it the same way production does rather than hardcoding it.
_RUN_NAME = "RunnerUp_2026-08-22-23-51-04_Running.tcx"
def _run_start() -> datetime:
return datetime(
2026, 8, 22, 23, 51, 4, tzinfo=datetime.now().astimezone().tzinfo
).astimezone(timezone.utc)
def test_run_already_in_endurain_is_skipped_not_reuploaded(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""RunnerUp's own uploader leaves no ledger trace; only the server knows."""
_env(monkeypatch, tmp_path, inbox)
_file(inbox, _RUN_NAME)
client = _StubMainClient(Result(Outcome.OK, 1, "ok"), remote=[_run_start()])
_patch_client(monkeypatch, client)
assert main() == 0
assert client.uploads == 0
assert (inbox / "processed" / _RUN_NAME).exists()
def test_remote_skip_is_recorded_so_the_next_run_needs_no_query(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
_env(monkeypatch, tmp_path, inbox)
_file(inbox, _RUN_NAME)
_patch_client(
monkeypatch,
_StubMainClient(Result(Outcome.OK, 1, "ok"), remote=[_run_start()]),
)
assert main() == 0
assert _RUN_NAME in (tmp_path / "state" / "ledger.json").read_text()
def test_a_genuinely_new_run_still_uploads(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""The remote check must not become a blanket refusal to upload."""
_env(monkeypatch, tmp_path, inbox)
_file(inbox, _RUN_NAME)
other = datetime(2026, 1, 1, tzinfo=timezone.utc)
client = _StubMainClient(Result(Outcome.OK, 7, "ok"), remote=[other])
_patch_client(monkeypatch, client)
assert main() == 0
assert client.uploads == 1
def test_unavailable_query_falls_back_to_the_ledger(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""A failed lookup must not read as 'nothing exists' -- nor block imports."""
_env(monkeypatch, tmp_path, inbox)
_file(inbox, _RUN_NAME)
client = _StubMainClient(Result(Outcome.OK, 1, "ok"), remote=None)
_patch_client(monkeypatch, client)
assert main() == 0
assert client.uploads == 1
def test_unparseable_filename_falls_back_to_the_ledger(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""No timestamp in the name means the remote check cannot apply."""
_env(monkeypatch, tmp_path, inbox)
_file(inbox, "mystery.tcx")
client = _StubMainClient(Result(Outcome.OK, 1, "ok"), remote=[_run_start()])
_patch_client(monkeypatch, client)
assert main() == 0
assert client.uploads == 1
def test_user_id_is_configurable(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
_env(monkeypatch, tmp_path, inbox, ENDURAIN_USER_ID="7")
_file(inbox, _RUN_NAME)
seen: list[int] = []
class _Recording(_StubMainClient):
def recent_start_times(self, user_id: int) -> list[datetime] | None:
seen.append(user_id)
return []
_patch_client(monkeypatch, _Recording(Result(Outcome.OK, 1, "ok")))
assert main() == 0
assert seen == [7]
def _patch_database(
monkeypatch: pytest.MonkeyPatch, result: list[datetime] | None
) -> None:
"""Stand in for the postgres reader; never touches a real container."""
from python_pkg.endurain_import import db_lookup
class _StubDatabase:
def recent_start_times(self, _user_id: int) -> list[datetime] | None:
return result
monkeypatch.setattr(db_lookup, "EndurainDatabase", _StubDatabase)
def test_database_fallback_catches_what_the_api_cannot(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""API keys cannot read activities, so postgres answers instead."""
_file(inbox, _RUN_NAME)
client = _StubMainClient(Result(Outcome.OK, 1, "uploaded"), remote=None)
_env(monkeypatch, tmp_path, inbox, ENDURAIN_NO_DB="0")
_patch_client(monkeypatch, client)
_patch_database(monkeypatch, [_run_start()])
assert main() == 0
assert client.uploads == 0
def test_database_failure_falls_back_to_the_ledger(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, inbox: Path
) -> None:
"""Both backends unknown must not block the import."""
_file(inbox, _RUN_NAME)
client = _StubMainClient(Result(Outcome.OK, 1, "uploaded"), remote=None)
_env(monkeypatch, tmp_path, inbox, ENDURAIN_NO_DB="0")
_patch_client(monkeypatch, client)
_patch_database(monkeypatch, None)
assert main() == 0
assert client.uploads == 1