left4me/l4d2web/tests/test_global_overlay_refresh.py
mwiegand 92d6ebbe82
feat(l4d2-web): managed global map overlays with daily refresh
Adds two managed system overlays (l4d2center-maps, cedapug-maps) that
fetch curated map archives from upstream sources and reconcile addons
symlinks for non-Steam maps. A daily systemd timer enqueues a coalesced
refresh_global_overlays worker job; downloads, extraction, and rebuilds
run in the existing job worker and surface in the job log UI.

Schema: GlobalOverlaySource / GlobalOverlayItem / GlobalOverlayItemFile
plus nullable Job.user_id so system jobs render as "system" in the UI.
The new builder reconciles symlinks against the per-source vpk cache
and leaves foreign symlinks untouched. Initialize-time guard refuses
to mount a partial overlay if any expected vpk is missing from cache.

Refresh service uses shutil.move to handle EXDEV when /tmp and the
cache live on different filesystems.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:05:14 +02:00

69 lines
3.5 KiB
Python

from pathlib import Path
from l4d2web.db import init_db, session_scope
from l4d2web.models import GlobalOverlayItem, GlobalOverlayItemFile, GlobalOverlaySource
from l4d2web.services.global_map_sources import GlobalMapManifestItem
def test_refresh_global_overlays_updates_manifest_items_and_invokes_builders(tmp_path, monkeypatch):
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'refresh.db'}")
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
init_db()
from l4d2web.services import global_overlay_refresh
monkeypatch.setattr(
global_overlay_refresh,
"fetch_l4d2center_manifest",
lambda: ("hash-center", [GlobalMapManifestItem("carriedoff.vpk", "carriedoff.vpk", "https://example.invalid/carriedoff.7z", "carriedoff.vpk", 3, "" )]),
)
monkeypatch.setattr(
global_overlay_refresh,
"fetch_cedapug_manifest",
lambda: ("hash-ceda", [GlobalMapManifestItem("FatalFreight.zip", "Fatal Freight", "https://example.invalid/FatalFreight.zip")]),
)
def fake_download_and_extract(source_key, item, *, should_cancel):
target = tmp_path / "global_overlay_cache" / source_key / "vpks" / (item.expected_vpk_name or item.item_key.replace(".zip", ".vpk"))
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"vpk")
return [(target.name, f"{source_key}/vpks/{target.name}", 3, "")], "etag", "last-modified", 3
built: list[str] = []
monkeypatch.setattr(global_overlay_refresh, "download_and_extract_item", fake_download_and_extract)
monkeypatch.setattr(global_overlay_refresh, "build_global_overlay", lambda overlay, **kwargs: built.append(overlay.name))
out: list[str] = []
result = global_overlay_refresh.refresh_global_overlays(on_stdout=out.append, on_stderr=out.append, should_cancel=lambda: False)
assert result == ["cedapug-maps", "l4d2center-maps"]
assert set(built) == {"cedapug-maps", "l4d2center-maps"}
with session_scope() as db:
assert db.query(GlobalOverlaySource).count() == 2
assert db.query(GlobalOverlayItem).count() == 2
assert db.query(GlobalOverlayItemFile).count() == 2
def test_refresh_removes_items_absent_from_manifest(tmp_path, monkeypatch):
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'remove.db'}")
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
init_db()
from l4d2web.services.global_overlays import ensure_global_overlays
from l4d2web.services import global_overlay_refresh
with session_scope() as db:
ensure_global_overlays(db)
source = db.query(GlobalOverlaySource).filter_by(source_key="l4d2center-maps").one()
item = GlobalOverlayItem(source_id=source.id, item_key="old.vpk", display_name="old.vpk", download_url="https://example.invalid/old.7z")
db.add(item)
db.flush()
db.add(GlobalOverlayItemFile(item_id=item.id, vpk_name="old.vpk", cache_path="l4d2center-maps/vpks/old.vpk", size=3))
monkeypatch.setattr(global_overlay_refresh, "fetch_l4d2center_manifest", lambda: ("empty-center", []))
monkeypatch.setattr(global_overlay_refresh, "fetch_cedapug_manifest", lambda: ("empty-ceda", []))
monkeypatch.setattr(global_overlay_refresh, "build_global_overlay", lambda overlay, **kwargs: None)
global_overlay_refresh.refresh_global_overlays(on_stdout=lambda line: None, on_stderr=lambda line: None, should_cancel=lambda: False)
with session_scope() as db:
assert db.query(GlobalOverlayItem).filter_by(item_key="old.vpk").count() == 0