myYouTube/tests/test_sync_trigger.py

160 lines
5.2 KiB
Python
Raw Normal View History

import json
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from app.services import sync_trigger
def _now():
return datetime.now(timezone.utc)
def test_is_videos_sync_due_without_finished_at():
assert sync_trigger.is_videos_sync_due(None, _now(), 2) is True
def test_is_videos_sync_due_after_idle():
finished = (_now() - timedelta(hours=3)).isoformat()
assert sync_trigger.is_videos_sync_due(finished, _now(), 2) is True
def test_is_videos_sync_due_at_exact_threshold():
finished = (_now() - timedelta(hours=2)).isoformat()
assert sync_trigger.is_videos_sync_due(finished, _now(), 2) is True
def test_is_videos_sync_due_before_idle():
finished = (_now() - timedelta(hours=1)).isoformat()
assert sync_trigger.is_videos_sync_due(finished, _now(), 2) is False
def test_is_videos_sync_due_unparseable_finished_at():
assert sync_trigger.is_videos_sync_due("not-a-date", _now(), 2) is True
assert sync_trigger.is_videos_sync_due(12345, _now(), 2) is True # non-str garbage
def test_is_videos_sync_due_naive_finished_at():
naive = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=3)
assert sync_trigger.is_videos_sync_due(naive.isoformat(), _now(), 2) is True
def test_extract_finished_at():
assert sync_trigger._extract_finished_at(None) is None
assert sync_trigger._extract_finished_at("not json") is None
assert sync_trigger._extract_finished_at(json.dumps(["a"])) is None
assert sync_trigger._extract_finished_at(json.dumps({"status": "running", "finished_at": None})) is None
assert (
sync_trigger._extract_finished_at(json.dumps({"status": "completed", "finished_at": "2026-09-10T12:00:00+00:00"}))
== "2026-09-10T12:00:00+00:00"
)
class _FakeDB:
def __init__(self):
self.closed = False
def close(self):
self.closed = True
def _patch_threads(monkeypatch, started):
monkeypatch.setattr(
sync_trigger,
"threading",
SimpleNamespace(Thread=lambda target=None, daemon=None: started.append({"target": target, "daemon": daemon})),
)
def test_maybe_trigger_skips_when_sync_running(monkeypatch):
started = []
monkeypatch.setattr(sync_trigger.sync, "is_videos_sync_running", lambda: True)
monkeypatch.setattr(
sync_trigger,
"SessionLocal",
lambda: (_ for _ in ()).throw(AssertionError("DB must not be touched while running")),
)
_patch_threads(monkeypatch, started)
sync_trigger.maybe_trigger_videos_sync()
assert started == []
def test_maybe_trigger_skips_when_not_due(monkeypatch):
started = []
db = _FakeDB()
raw = json.dumps({"status": "completed", "finished_at": (_now() - timedelta(hours=1)).isoformat()})
monkeypatch.setattr(sync_trigger.sync, "is_videos_sync_running", lambda: False)
monkeypatch.setattr(sync_trigger, "SessionLocal", lambda: db)
monkeypatch.setattr(sync_trigger, "get_setting", lambda session, key: raw)
_patch_threads(monkeypatch, started)
sync_trigger.maybe_trigger_videos_sync()
assert started == []
assert db.closed is True
def test_maybe_trigger_starts_thread_when_due_never_synced(monkeypatch):
started = []
db = _FakeDB()
monkeypatch.setattr(sync_trigger.sync, "is_videos_sync_running", lambda: False)
monkeypatch.setattr(sync_trigger, "SessionLocal", lambda: db)
monkeypatch.setattr(sync_trigger, "get_setting", lambda session, key: None)
_patch_threads(monkeypatch, started)
sync_trigger.maybe_trigger_videos_sync()
assert len(started) == 1
assert started[0]["target"] == sync_trigger._run_videos_sync
assert started[0]["daemon"] is True
assert db.closed is True
def test_maybe_trigger_starts_thread_when_due_after_idle(monkeypatch):
started = []
db = _FakeDB()
raw = json.dumps({"status": "completed", "finished_at": (_now() - timedelta(hours=5)).isoformat()})
monkeypatch.setattr(sync_trigger.sync, "is_videos_sync_running", lambda: False)
monkeypatch.setattr(sync_trigger, "SessionLocal", lambda: db)
monkeypatch.setattr(sync_trigger, "get_setting", lambda session, key: raw)
_patch_threads(monkeypatch, started)
sync_trigger.maybe_trigger_videos_sync()
assert len(started) == 1
assert started[0]["daemon"] is True
def test_maybe_trigger_never_raises_when_db_unavailable(monkeypatch):
started = []
monkeypatch.setattr(sync_trigger.sync, "is_videos_sync_running", lambda: False)
monkeypatch.setattr(
sync_trigger,
"SessionLocal",
lambda: (_ for _ in ()).throw(RuntimeError("db down")),
)
_patch_threads(monkeypatch, started)
sync_trigger.maybe_trigger_videos_sync() # must not raise
assert started == []
def test_run_videos_sync_swallows_sync_in_progress(monkeypatch):
closed = []
monkeypatch.setattr(
sync_trigger,
"SessionLocal",
lambda: SimpleNamespace(close=lambda: closed.append(True)),
)
monkeypatch.setattr(
sync_trigger.sync,
"sync_videos",
lambda db: (_ for _ in ()).throw(sync_trigger.sync.SyncInProgress("busy")),
)
sync_trigger._run_videos_sync() # must not raise
assert closed == [True]