left4me/l4d2web/tests/test_blueprints.py
mwiegand c6f10e632d
test(blueprint): also assert prism.css is referenced in editor assets
The plan template (and verbatim implementation) listed five of the six
editor asset URLs in the structural test — vendor/prism.css was
omitted. If a future change drops the Prism stylesheet from the
partial, syntax tokens lose their color rules silently and the test
still passes. Add the missing assertion and update the plan to match.

Addresses Minor #1 from the Task 6 code review.

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

465 lines
16 KiB
Python

import json
from datetime import UTC, datetime
import pytest
from sqlalchemy import select
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, BlueprintOverlay, Overlay, Server, User
@pytest.fixture
def user_client(tmp_path, monkeypatch):
db_url = f"sqlite:///{tmp_path/'blueprint.db'}"
monkeypatch.setenv("DATABASE_URL", db_url)
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()
user_id = user.id
session.add_all(
[
Overlay(name="o1", path="/opt/l4d2/overlays/o1"),
Overlay(name="o2", path="/opt/l4d2/overlays/o2"),
]
)
client = app.test_client()
with client.session_transaction() as sess:
sess["user_id"] = user_id
sess["pw_changed_at"] = datetime.now(UTC).isoformat()
sess["csrf_token"] = "test-token"
return client
@pytest.fixture
def linked_blueprint(tmp_path, monkeypatch):
db_url = f"sqlite:///{tmp_path/'linked.db'}"
monkeypatch.setenv("DATABASE_URL", db_url)
app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
init_db()
with session_scope() as session:
user = User(username="bob", password_digest=hash_password("secret"), admin=False)
session.add(user)
session.flush()
blueprint = Blueprint(user_id=user.id, name="linked", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add(Server(user_id=user.id, blueprint_id=blueprint.id, name="alpha", port=27015))
blueprint_id = blueprint.id
user_id = user.id
client = app.test_client()
with client.session_transaction() as sess:
sess["user_id"] = user_id
sess["pw_changed_at"] = datetime.now(UTC).isoformat()
sess["csrf_token"] = "test-token"
return client, blueprint_id
def test_user_can_create_private_blueprint(user_client) -> None:
payload = {
"name": "comp",
"arguments": ["-tickrate 100"],
"config": ["sv_consistency 1"],
"overlay_ids": [1, 2],
}
response = user_client.post(
"/blueprints",
data=json.dumps(payload),
content_type="application/json",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 201
def _create_other_users_private_overlay() -> int:
with session_scope() as session:
other = User(username="mallory", password_digest=hash_password("secret"), admin=False)
session.add(other)
session.flush()
overlay = Overlay(
name="mallory-private",
path="mallory-private",
type="workshop",
user_id=other.id,
)
session.add(overlay)
session.flush()
return overlay.id
def test_user_cannot_create_blueprint_with_other_users_private_overlay(user_client) -> None:
foreign_overlay_id = _create_other_users_private_overlay()
payload = {
"name": "bad",
"arguments": [],
"config": [],
"overlay_ids": [foreign_overlay_id],
}
response = user_client.post(
"/blueprints",
data=json.dumps(payload),
content_type="application/json",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 403
def test_user_cannot_update_blueprint_with_other_users_private_overlay(user_client) -> None:
foreign_overlay_id = _create_other_users_private_overlay()
create = user_client.post(
"/blueprints",
data={"name": "comp", "arguments": "", "config": "", "overlay_ids": ["1"]},
headers={"X-CSRF-Token": "test-token"},
)
assert create.status_code == 302
response = user_client.post(
"/blueprints/1",
data={
"name": "edited",
"arguments": "",
"config": "",
"overlay_ids": [str(foreign_overlay_id)],
},
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 403
def test_user_can_create_blueprint_with_system_overlay(user_client) -> None:
payload = {
"name": "system-ok",
"arguments": [],
"config": [],
"overlay_ids": [1],
}
response = user_client.post(
"/blueprints",
data=json.dumps(payload),
content_type="application/json",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 201
blueprint_id = response.get_json()["id"]
with session_scope() as session:
link = session.query(BlueprintOverlay).filter_by(blueprint_id=blueprint_id, overlay_id=1).one()
assert link.position == 0
def test_delete_blueprint_blocked_when_in_use(linked_blueprint) -> None:
client, blueprint_id = linked_blueprint
response = client.delete(f"/blueprints/{blueprint_id}", headers={"X-CSRF-Token": "test-token"})
assert response.status_code == 409
def test_post_delete_blueprint_redirects_to_index(user_client) -> None:
create = user_client.post(
"/blueprints",
data={"name": "doomed", "arguments": "", "config": ""},
headers={"X-CSRF-Token": "test-token"},
)
assert create.status_code == 302
from sqlalchemy import select
from l4d2web.models import Blueprint as BlueprintModel
with session_scope() as session:
blueprint_id = session.scalars(select(BlueprintModel.id)).one()
response = user_client.post(
f"/blueprints/{blueprint_id}/delete",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 302
assert response.headers["Location"] == "/blueprints"
with session_scope() as session:
assert session.scalars(select(BlueprintModel)).all() == []
def test_post_delete_blueprint_blocked_when_in_use(linked_blueprint) -> None:
client, blueprint_id = linked_blueprint
response = client.post(
f"/blueprints/{blueprint_id}/delete",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 409
def test_post_delete_blueprint_returns_404_for_other_user(user_client, tmp_path) -> None:
with session_scope() as session:
other = User(username="mallory", password_digest=hash_password("secret"), admin=False)
session.add(other)
session.flush()
foreign = Blueprint(user_id=other.id, name="foreign", arguments="[]", config="[]")
session.add(foreign)
session.flush()
foreign_id = foreign.id
response = user_client.post(
f"/blueprints/{foreign_id}/delete",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 404
def test_form_update_preserves_ordered_overlays_and_multiline_fields(user_client) -> None:
create = user_client.post(
"/blueprints",
data={
"name": "comp",
"arguments": "-tickrate 100",
"config": "sv_consistency 1",
"overlay_ids": ["2", "1"],
"overlay_position_1": "2",
"overlay_position_2": "1",
},
headers={"X-CSRF-Token": "test-token"},
)
assert create.status_code == 302
update = user_client.post(
"/blueprints/1",
data={
"name": "edited",
"arguments": "-tickrate 100\n+sv_lan 0",
"config": "sv_consistency 1\nsv_allow_lobby_connect_only 0",
"overlay_ids": ["1", "2"],
"overlay_position_1": "1",
"overlay_position_2": "2",
},
headers={"X-CSRF-Token": "test-token"},
)
assert update.status_code == 302
assert update.headers["Location"] == "/blueprints/1"
def test_blueprint_detail_renders_editor_assets(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
# Editor opts the textarea in via a data-attribute.
assert 'data-editor-language="srccfg"' in body
# All editor assets are referenced.
assert "static/vendor/prism.js" in body
assert "static/vendor/prism.css" in body
assert "static/vendor/codejar.js" in body
assert "static/js/srccfg-grammar.js" in body
assert "static/js/editor.js" in body
assert "static/css/editor.css" in body
# Scripts are nonce'd (CSP regression guard).
assert 'nonce="' in body
def test_blueprint_config_form_post_still_round_trips(user_client) -> None:
# The editor is a visual layer; the form POST contract must be
# unaffected. This guards against accidentally renaming `config` or
# dropping it from form serialization.
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
blueprint_id = blueprint.id
update = user_client.post(
f"/blueprints/{blueprint_id}",
data={
"name": "bp",
"arguments": "",
"config": "sv_cheats 1\nmp_gamemode coop",
"overlay_ids": [],
},
headers={"X-CSRF-Token": "test-token"},
)
assert update.status_code == 302
with session_scope() as session:
bp = session.get(Blueprint, blueprint_id)
assert json.loads(bp.config) == ["sv_cheats 1", "mp_gamemode coop"]
def test_blueprint_detail_renders_picker_list_and_select(user_client) -> None:
with session_scope() as session:
session.add(Overlay(name="o3", path="/opt/l4d2/overlays/o3"))
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add(BlueprintOverlay(blueprint_id=blueprint.id, overlay_id=1, position=0))
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
assert "data-overlay-list" in body
assert "data-overlay-add" in body
assert body.count('data-overlay-id="1"') == 1
assert 'data-overlay-id="2"' not in body
assert '<option value="2"' in body
assert '<option value="3"' in body
assert '<option value="1"' not in body
def test_form_save_persists_expose_server_cfg_set(user_client) -> None:
create = user_client.post(
"/blueprints",
data={
"name": "comp",
"arguments": "",
"config": "",
"overlay_ids": ["1", "2"],
"overlay_position_1": "1",
"overlay_position_2": "2",
"expose_server_cfg_ids": ["2"],
},
headers={"X-CSRF-Token": "test-token"},
)
assert create.status_code == 302
with session_scope() as session:
rows = session.execute(
select(BlueprintOverlay.overlay_id, BlueprintOverlay.expose_server_cfg)
.order_by(BlueprintOverlay.position)
).all()
assert sorted(rows) == [(1, False), (2, True)]
update = user_client.post(
"/blueprints/1",
data={
"name": "comp",
"arguments": "",
"config": "",
"overlay_ids": ["1", "2"],
"overlay_position_1": "1",
"overlay_position_2": "2",
"expose_server_cfg_ids": ["1"],
},
headers={"X-CSRF-Token": "test-token"},
)
assert update.status_code == 302
with session_scope() as session:
rows = session.execute(
select(BlueprintOverlay.overlay_id, BlueprintOverlay.expose_server_cfg)
.order_by(BlueprintOverlay.position)
).all()
assert sorted(rows) == [(1, True), (2, False)]
def test_blueprint_detail_renders_expose_checkbox_compact_label(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add(BlueprintOverlay(
blueprint_id=blueprint.id, overlay_id=1, position=0, expose_server_cfg=True,
))
session.add(BlueprintOverlay(
blueprint_id=blueprint.id, overlay_id=2, position=1, expose_server_cfg=False,
))
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
assert 'name="expose_server_cfg_ids" value="1"' in body
assert 'name="expose_server_cfg_ids" value="2"' in body
assert "exec <code>server.cfg</code>" in body
# The technical alias must NOT appear in the row label anymore.
assert "server_overlay_1" not in body
assert "server_overlay_2" not in body
assert "checked" in body # row 1 is exposed
def test_blueprint_detail_shows_config_preview_for_exposed_overlays(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
comp = Overlay(name="comp", path="/opt/l4d2/overlays/comp", user_id=user.id)
plain = Overlay(name="plain", path="/opt/l4d2/overlays/plain", user_id=user.id)
session.add_all([comp, plain])
session.flush()
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add(BlueprintOverlay(
blueprint_id=blueprint.id, overlay_id=comp.id, position=0, expose_server_cfg=True,
))
session.add(BlueprintOverlay(
blueprint_id=blueprint.id, overlay_id=plain.id, position=1, expose_server_cfg=False,
))
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
assert 'class="config-preview"' in body
assert "exec comp.cfg" in body
assert "exec plain.cfg" not in body
def test_blueprint_detail_omits_preview_when_nothing_exposed(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add(BlueprintOverlay(
blueprint_id=blueprint.id, overlay_id=1, position=0, expose_server_cfg=False,
))
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
assert 'class="config-preview"' not in body
def test_blueprint_detail_picker_emits_hidden_overlay_ids_in_order(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
session.add_all(
[
BlueprintOverlay(blueprint_id=blueprint.id, overlay_id=2, position=0),
BlueprintOverlay(blueprint_id=blueprint.id, overlay_id=1, position=1),
]
)
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
first = body.find('name="overlay_ids" value="2"')
second = body.find('name="overlay_ids" value="1"')
assert first != -1 and second != -1
assert first < second