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>
167 lines
6.4 KiB
Python
167 lines
6.4 KiB
Python
from sqlalchemy import select
|
|
|
|
from l4d2web.db import init_db, session_scope
|
|
from l4d2web.models import GlobalOverlaySource, Job, Overlay, User
|
|
from l4d2web.services.global_overlays import (
|
|
enqueue_refresh_global_overlays,
|
|
ensure_global_overlays,
|
|
is_creatable_overlay_type,
|
|
)
|
|
|
|
|
|
def test_ensure_global_overlays_creates_singletons_and_directories(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'global_overlays.db'}")
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
init_db()
|
|
|
|
with session_scope() as session:
|
|
created = ensure_global_overlays(session)
|
|
assert created == {"cedapug-maps", "l4d2center-maps"}
|
|
|
|
second = ensure_global_overlays(session)
|
|
assert second == set()
|
|
|
|
overlays = session.scalars(select(Overlay).order_by(Overlay.name)).all()
|
|
assert [overlay.name for overlay in overlays] == ["cedapug-maps", "l4d2center-maps"]
|
|
assert [overlay.type for overlay in overlays] == ["cedapug_maps", "l4d2center_maps"]
|
|
assert [overlay.user_id for overlay in overlays] == [None, None]
|
|
assert len({overlay.path for overlay in overlays}) == 2
|
|
for overlay in overlays:
|
|
assert (tmp_path / "overlays" / overlay.path).is_dir()
|
|
|
|
sources = session.scalars(select(GlobalOverlaySource).order_by(GlobalOverlaySource.source_key)).all()
|
|
assert [source.source_key for source in sources] == ["cedapug-maps", "l4d2center-maps"]
|
|
assert [source.source_type for source in sources] == [
|
|
"cedapug_custom_page",
|
|
"l4d2center_csv",
|
|
]
|
|
|
|
|
|
def test_ensure_global_overlays_repairs_existing_rows(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'global_overlay_repair.db'}")
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
init_db()
|
|
|
|
with session_scope() as session:
|
|
overlay = Overlay(name="cedapug-maps", path="legacy", type="external", user_id=None)
|
|
session.add(overlay)
|
|
session.flush()
|
|
session.add(
|
|
GlobalOverlaySource(
|
|
overlay_id=overlay.id,
|
|
source_key="cedapug-maps",
|
|
source_type="wrong",
|
|
source_url="https://example.invalid/wrong",
|
|
)
|
|
)
|
|
|
|
(tmp_path / "overlays" / "legacy").mkdir(parents=True)
|
|
|
|
with session_scope() as session:
|
|
created = ensure_global_overlays(session)
|
|
assert created == {"l4d2center-maps"}
|
|
|
|
repaired = session.scalar(select(Overlay).where(Overlay.name == "cedapug-maps"))
|
|
assert repaired is not None
|
|
assert repaired.type == "cedapug_maps"
|
|
assert repaired.user_id is None
|
|
assert (tmp_path / "overlays" / repaired.path).is_dir()
|
|
|
|
source = session.scalar(
|
|
select(GlobalOverlaySource).where(GlobalOverlaySource.source_key == "cedapug-maps")
|
|
)
|
|
assert source is not None
|
|
assert source.source_type == "cedapug_custom_page"
|
|
assert source.source_url == "https://cedapug.com/custom"
|
|
|
|
|
|
def test_ensure_global_overlays_does_not_hijack_private_overlay_name(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'global_overlay_private_name.db'}")
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
init_db()
|
|
|
|
with session_scope() as session:
|
|
user = User(username="alice", password_digest="digest", admin=False)
|
|
session.add(user)
|
|
session.flush()
|
|
private = Overlay(
|
|
name="l4d2center-maps",
|
|
path="private-l4d2center",
|
|
type="workshop",
|
|
user_id=user.id,
|
|
)
|
|
session.add(private)
|
|
session.flush()
|
|
private_id = private.id
|
|
private_user_id = user.id
|
|
|
|
with session_scope() as session:
|
|
created = ensure_global_overlays(session)
|
|
|
|
assert created == {"cedapug-maps", "l4d2center-maps"}
|
|
private = session.scalar(select(Overlay).where(Overlay.id == private_id))
|
|
assert private is not None
|
|
assert private.user_id == private_user_id
|
|
assert private.type == "workshop"
|
|
assert private.path == "private-l4d2center"
|
|
|
|
system = session.scalar(
|
|
select(Overlay).where(Overlay.name == "l4d2center-maps", Overlay.user_id.is_(None))
|
|
)
|
|
assert system is not None
|
|
assert system.id != private_id
|
|
assert system.type == "l4d2center_maps"
|
|
assert (tmp_path / "overlays" / system.path).is_dir()
|
|
|
|
source = session.scalar(
|
|
select(GlobalOverlaySource).where(GlobalOverlaySource.source_key == "l4d2center-maps")
|
|
)
|
|
assert source is not None
|
|
assert source.overlay_id == system.id
|
|
|
|
|
|
def test_enqueue_refresh_global_overlays_coalesces_active_jobs(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'refresh_jobs.db'}")
|
|
init_db()
|
|
|
|
for state in ("queued", "running", "cancelling"):
|
|
with session_scope() as session:
|
|
session.query(Job).delete()
|
|
existing = Job(
|
|
user_id=7,
|
|
server_id=None,
|
|
overlay_id=None,
|
|
operation="refresh_global_overlays",
|
|
state=state,
|
|
)
|
|
session.add(existing)
|
|
session.flush()
|
|
existing_id = existing.id
|
|
|
|
job = enqueue_refresh_global_overlays(session, user_id=None)
|
|
assert job.id == existing_id
|
|
assert session.query(Job).filter_by(operation="refresh_global_overlays").count() == 1
|
|
|
|
|
|
def test_enqueue_refresh_global_overlays_creates_system_job(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'refresh_system_job.db'}")
|
|
init_db()
|
|
|
|
with session_scope() as session:
|
|
job = enqueue_refresh_global_overlays(session, user_id=None)
|
|
|
|
assert job.id is not None
|
|
assert job.user_id is None
|
|
assert job.server_id is None
|
|
assert job.overlay_id is None
|
|
assert job.operation == "refresh_global_overlays"
|
|
assert job.state == "queued"
|
|
|
|
|
|
def test_is_creatable_overlay_type_policy():
|
|
assert is_creatable_overlay_type("workshop", admin=False) is True
|
|
assert is_creatable_overlay_type("external", admin=False) is False
|
|
assert is_creatable_overlay_type("external", admin=True) is True
|
|
assert is_creatable_overlay_type("workshop", admin=True) is True
|
|
assert is_creatable_overlay_type("l4d2center_maps", admin=True) is False
|
|
assert is_creatable_overlay_type("cedapug_maps", admin=True) is False
|