test(files): add files-overlay e2e fixture
Extracts the shared boot/serve plumbing out of live_server into _boot_app + _serve helpers, then adds files_overlay_server: a function-scoped fixture that monkey-patches LEFT4ME_ROOT to tmp_path BEFORE create_app(), seeds a files-type Overlay owned by alice, and populates the overlay root with one editable text file, one binary file, and one nested folder. Sets up the surface area the Tier-1 files-overlay e2e tests need without duplicating the live_server boilerplate. Also exposes a top-level `login(page, base_url, ...)` helper so future test modules can share it instead of re-pasting the form-POST flow. Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d5a72b317
commit
911bbf9103
1 changed files with 105 additions and 16 deletions
|
|
@ -14,7 +14,7 @@ from werkzeug.serving import make_server
|
|||
from l4d2web.app import create_app
|
||||
from l4d2web.auth import hash_password
|
||||
from l4d2web.db import init_db, session_scope
|
||||
from l4d2web.models import Blueprint, User
|
||||
from l4d2web.models import Blueprint, Overlay, User
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
|
|
@ -25,17 +25,19 @@ def _free_port() -> int:
|
|||
return port
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def live_server(tmp_path, monkeypatch):
|
||||
def _boot_app(tmp_path, monkeypatch):
|
||||
"""Set the env vars + create_app + init_db combo shared by every e2e
|
||||
fixture. Returns the Flask app.
|
||||
|
||||
Sets DATABASE_URL → temp sqlite; SESSION_COOKIE_SECURE=0 so the
|
||||
browser keeps the session cookie over http://127.0.0.1 (app.py:57
|
||||
would otherwise mark it Secure). Caller-specific env vars (e.g.
|
||||
LEFT4ME_ROOT for files-overlay tests) must be set BEFORE calling
|
||||
this, since create_app reads them during app construction.
|
||||
"""
|
||||
db_path = tmp_path / "e2e.db"
|
||||
db_url = f"sqlite:///{db_path}"
|
||||
monkeypatch.setenv("DATABASE_URL", db_url)
|
||||
# app.py:57 sets SESSION_COOKIE_SECURE = not TESTING, which would
|
||||
# mark the session cookie Secure. The browser then drops it over
|
||||
# http://127.0.0.1 in e2e tests and the login flow silently fails
|
||||
# with a redirect back to /login. Force it off explicitly via the
|
||||
# env-var override (app.py:53-55) rather than flipping TESTING,
|
||||
# which would skip the SECRET_KEY guard and other production paths.
|
||||
monkeypatch.setenv("SESSION_COOKIE_SECURE", "0")
|
||||
app = create_app({"TESTING": False, "DATABASE_URL": db_url, "SECRET_KEY": "e2e"})
|
||||
# create_app() already calls init_db() inside an app context, which
|
||||
|
|
@ -45,6 +47,37 @@ def live_server(tmp_path, monkeypatch):
|
|||
# call creates the tables on that env-derived engine so the seed
|
||||
# inserts have somewhere to land.
|
||||
init_db()
|
||||
return app
|
||||
|
||||
|
||||
def _serve(app):
|
||||
"""Start the app on a background thread, return (base_url, shutdown).
|
||||
|
||||
Caller is responsible for calling shutdown() in a finally block.
|
||||
"""
|
||||
port = _free_port()
|
||||
server = make_server("127.0.0.1", port, app)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def shutdown():
|
||||
server.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
return f"http://127.0.0.1:{port}", shutdown
|
||||
|
||||
|
||||
def login(page, base_url: str, username: str = "alice", password: str = "secret") -> None:
|
||||
"""Form-POST login helper. Reused by every e2e test."""
|
||||
page.goto(f"{base_url}/login")
|
||||
page.fill('input[name="username"]', username)
|
||||
page.fill('input[name="password"]', password)
|
||||
page.click('button[type="submit"]')
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def live_server(tmp_path, monkeypatch):
|
||||
app = _boot_app(tmp_path, monkeypatch)
|
||||
|
||||
with session_scope() as session:
|
||||
user = User(
|
||||
|
|
@ -62,16 +95,72 @@ def live_server(tmp_path, monkeypatch):
|
|||
blueprint_id = bp.id
|
||||
user_id = user.id
|
||||
|
||||
port = _free_port()
|
||||
server = make_server("127.0.0.1", port, app)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
base_url, shutdown = _serve(app)
|
||||
try:
|
||||
yield {
|
||||
"base_url": f"http://127.0.0.1:{port}",
|
||||
"base_url": base_url,
|
||||
"user_id": user_id,
|
||||
"blueprint_id": blueprint_id,
|
||||
}
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=2)
|
||||
shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def files_overlay_server(tmp_path, monkeypatch):
|
||||
"""live_server + a files-type Overlay owned by alice, seeded with:
|
||||
|
||||
<overlay_root>/server.cfg (text, editable)
|
||||
<overlay_root>/icon.png (binary, replaceable)
|
||||
<overlay_root>/cfg/admins.txt (nested-folder fixture)
|
||||
|
||||
LEFT4ME_ROOT is monkey-patched to tmp_path BEFORE create_app() so
|
||||
overlay path resolution (l4d2host.paths.overlay_path) lands under
|
||||
the temp directory instead of /var/lib/left4me — without this every
|
||||
files-overlay route 404s on macOS (see AGENTS.md "symptom-to-cause").
|
||||
"""
|
||||
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
||||
app = _boot_app(tmp_path, monkeypatch)
|
||||
|
||||
with session_scope() as session:
|
||||
user = User(
|
||||
username="alice",
|
||||
password_digest=hash_password("secret"),
|
||||
admin=False,
|
||||
)
|
||||
session.add(user)
|
||||
session.flush()
|
||||
# Same insert-then-update-path pattern dev-server.py uses: we
|
||||
# need overlay.id to write into overlay.path, but the column is
|
||||
# NOT NULL so it gets a placeholder until we know the id.
|
||||
overlay = Overlay(
|
||||
name="cfgs",
|
||||
path="_pending",
|
||||
type="files",
|
||||
user_id=user.id,
|
||||
)
|
||||
session.add(overlay)
|
||||
session.flush()
|
||||
overlay.path = str(overlay.id)
|
||||
user_id = user.id
|
||||
overlay_id = overlay.id
|
||||
|
||||
overlay_root = tmp_path / "overlays" / str(overlay_id)
|
||||
overlay_root.mkdir(parents=True)
|
||||
(overlay_root / "server.cfg").write_text('hostname "left4me"\n')
|
||||
# 8-byte PNG signature + 60 null bytes — large enough for the
|
||||
# editor's binary-mode detection (which checks the magic header).
|
||||
(overlay_root / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 60)
|
||||
(overlay_root / "cfg").mkdir()
|
||||
(overlay_root / "cfg" / "admins.txt").write_text("STEAM_1:0:1\n")
|
||||
|
||||
base_url, shutdown = _serve(app)
|
||||
try:
|
||||
yield {
|
||||
"base_url": base_url,
|
||||
"user_id": user_id,
|
||||
"overlay_id": overlay_id,
|
||||
"overlay_root": overlay_root,
|
||||
}
|
||||
finally:
|
||||
shutdown()
|
||||
|
|
|
|||
Loading…
Reference in a new issue