Server-renders the file editor as a real page. With HX-Modal:1 returns a layoutless fragment for modal embedding; without it returns the full standalone page. Mirrors overlay_file_content's path/editability checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
5.8 KiB
Python
138 lines
5.8 KiB
Python
from datetime import UTC, datetime
|
|
|
|
from flask import render_template_string
|
|
|
|
from l4d2web.app import create_app
|
|
from l4d2web.auth import hash_password
|
|
from l4d2web.db import init_db, session_scope
|
|
from l4d2web.models import Overlay, User
|
|
|
|
|
|
def _make_app(tmp_path, monkeypatch, db_name: str):
|
|
db_url = f"sqlite:///{tmp_path/db_name}"
|
|
monkeypatch.setenv("DATABASE_URL", db_url)
|
|
return create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
|
|
|
|
|
|
def test_base_layout_is_modal_partial_when_hx_modal_header_set(tmp_path, monkeypatch):
|
|
app = _make_app(tmp_path, monkeypatch, "layout-modal.db")
|
|
with app.test_request_context("/", headers={"HX-Modal": "1"}):
|
|
assert render_template_string("{{ base_layout }}") == "_modal_partial.html"
|
|
|
|
|
|
def test_base_layout_is_base_html_for_normal_request(tmp_path, monkeypatch):
|
|
app = _make_app(tmp_path, monkeypatch, "layout-default.db")
|
|
with app.test_request_context("/"):
|
|
assert render_template_string("{{ base_layout }}") == "base.html"
|
|
|
|
|
|
def test_base_layout_does_not_react_to_plain_hx_request_header(tmp_path, monkeypatch):
|
|
# HTMX sets HX-Request on every request including the build-status poll;
|
|
# only HX-Modal should switch the layout.
|
|
app = _make_app(tmp_path, monkeypatch, "layout-hxreq.db")
|
|
with app.test_request_context("/", headers={"HX-Request": "true"}):
|
|
assert render_template_string("{{ base_layout }}") == "base.html"
|
|
|
|
|
|
def _auth_client_with_files_overlay(tmp_path, monkeypatch, db_name: str):
|
|
db_url = f"sqlite:///{tmp_path/db_name}"
|
|
monkeypatch.setenv("DATABASE_URL", db_url)
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
|
|
init_db()
|
|
|
|
with session_scope() as session:
|
|
user = User(username="alice", password_digest=hash_password("secret"), admin=False)
|
|
session.add(user)
|
|
session.flush()
|
|
overlay = Overlay(name="cfgs", path="", type="files", user_id=user.id)
|
|
session.add(overlay)
|
|
session.flush()
|
|
overlay.path = str(overlay.id)
|
|
overlay_root = tmp_path / "overlays" / str(overlay.id)
|
|
overlay_root.mkdir(parents=True)
|
|
(overlay_root / "server.cfg").write_text("hostname \"left4me\"\nrcon_password \"hunter2\"\n", encoding="utf-8")
|
|
user_id = user.id
|
|
overlay_id = overlay.id
|
|
|
|
client = app.test_client()
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = user_id
|
|
sess["pw_changed_at"] = datetime.now(UTC).isoformat()
|
|
return client, overlay_id
|
|
|
|
|
|
def test_edit_route_renders_full_page_without_modal_header(tmp_path, monkeypatch):
|
|
client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-full.db")
|
|
response = client.get(f"/overlays/{overlay_id}/files/edit?path=server.cfg")
|
|
text = response.get_data(as_text=True)
|
|
|
|
assert response.status_code == 200
|
|
assert "<!doctype html>" in text.lower() # full base.html rendered
|
|
assert 'href="/dashboard"' in text # nav present
|
|
assert 'class="files-editor-content"' in text
|
|
assert 'rcon_password' in text # content pre-filled
|
|
|
|
|
|
def test_edit_route_renders_fragment_with_modal_header(tmp_path, monkeypatch):
|
|
client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-fragment.db")
|
|
response = client.get(
|
|
f"/overlays/{overlay_id}/files/edit?path=server.cfg",
|
|
headers={"HX-Modal": "1"},
|
|
)
|
|
text = response.get_data(as_text=True)
|
|
|
|
assert response.status_code == 200
|
|
assert "<html" not in text # layoutless
|
|
assert 'class="primary-nav"' not in text
|
|
assert 'class="files-editor-content"' in text
|
|
assert "hostname" in text # content pre-filled
|
|
|
|
|
|
def test_edit_route_404s_for_missing_file(tmp_path, monkeypatch):
|
|
client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-404.db")
|
|
response = client.get(f"/overlays/{overlay_id}/files/edit?path=nonexistent.cfg")
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_edit_route_415s_for_non_editable_file(tmp_path, monkeypatch):
|
|
client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-415.db")
|
|
# Forge a non-editable file by writing binary garbage.
|
|
from pathlib import Path
|
|
overlay_root = tmp_path / "overlays" / str(overlay_id)
|
|
Path(overlay_root).joinpath("blob.bin").write_bytes(b"\x00\x01\x02\x03" * 1024)
|
|
|
|
response = client.get(f"/overlays/{overlay_id}/files/edit?path=blob.bin")
|
|
assert response.status_code == 415
|
|
|
|
|
|
def test_edit_route_400s_for_path_traversal(tmp_path, monkeypatch):
|
|
client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-400.db")
|
|
response = client.get(f"/overlays/{overlay_id}/files/edit?path=../../etc/passwd")
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_edit_route_404s_for_non_files_overlay(tmp_path, monkeypatch):
|
|
db_url = f"sqlite:///{tmp_path/'edit-script-overlay.db'}"
|
|
monkeypatch.setenv("DATABASE_URL", db_url)
|
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
|
app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
|
|
init_db()
|
|
with session_scope() as s:
|
|
user = User(username="alice", password_digest=hash_password("secret"), admin=False)
|
|
s.add(user)
|
|
s.flush()
|
|
overlay = Overlay(name="scripted", path="", type="script", user_id=user.id)
|
|
s.add(overlay)
|
|
s.flush()
|
|
overlay.path = str(overlay.id)
|
|
(tmp_path / "overlays" / str(overlay.id)).mkdir(parents=True)
|
|
user_id = user.id
|
|
overlay_id = overlay.id
|
|
client = app.test_client()
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = user_id
|
|
sess["pw_changed_at"] = datetime.now(UTC).isoformat()
|
|
|
|
response = client.get(f"/overlays/{overlay_id}/files/edit?path=anything.cfg")
|
|
assert response.status_code == 404
|