diff --git a/l4d2web/services/l4d2_facade.py b/l4d2web/services/l4d2_facade.py index 80e9e8d..9dff64f 100644 --- a/l4d2web/services/l4d2_facade.py +++ b/l4d2web/services/l4d2_facade.py @@ -11,6 +11,7 @@ from l4d2web.models import ( Overlay, OverlayWorkshopItem, Server, + User, WorkshopItem, ) from l4d2web.services import host_commands @@ -30,6 +31,8 @@ def build_server_spec_payload( server: Server, blueprint: Blueprint, overlay_rows: list[tuple[int, str, bool]], + *, + resolved_hostname: str = "", ) -> dict: overlays: list[dict] = [] for overlay_id, path, expose in overlay_rows: @@ -45,8 +48,10 @@ def build_server_spec_payload( if expose ] config_lines: list[str] = exec_lines + json.loads(blueprint.config) - # rcon_password is appended LAST so neither overlays nor user blueprint - # config can override it (Source's cvar semantics are last-wins). + if resolved_hostname: + config_lines.append(f'hostname "{resolved_hostname}"') + # rcon_password is appended LAST so neither overlays, blueprint config, + # nor hostname can override it (Source's cvar semantics are last-wins). if server.rcon_password: config_lines.append(f'rcon_password "{server.rcon_password}"') return { @@ -98,7 +103,17 @@ def initialize_server(server_id: int, on_stdout=None, on_stderr=None, should_can # than mount a partial overlay (would silently leave maps missing in-game). _check_workshop_overlay_caches(blueprint_id=blueprint.id) - spec_path = write_temp_spec(build_server_spec_payload(server, blueprint, overlay_rows)) + # Resolve hostname — explicit override or ephemeral fallback + if server.hostname: + resolved_hostname = server.hostname + else: + with session_scope() as db: + user = db.get(User, server.user_id) + resolved_hostname = f"{user.username} {server.name}" + + spec_path = write_temp_spec(build_server_spec_payload( + server, blueprint, overlay_rows, resolved_hostname=resolved_hostname, + )) try: host_commands.run_command( ["l4d2ctl", "initialize", server_unit_name(server.id), "-f", str(spec_path)], diff --git a/l4d2web/tests/test_l4d2_facade.py b/l4d2web/tests/test_l4d2_facade.py index c5e20c8..dd8a48b 100644 --- a/l4d2web/tests/test_l4d2_facade.py +++ b/l4d2web/tests/test_l4d2_facade.py @@ -385,3 +385,54 @@ def test_build_server_spec_payload_omits_rcon_password_when_empty() -> None: assert not line.startswith("rcon_password ") +# --------------------------------------------------------------------------- +# build_server_spec_payload — hostname injection +# --------------------------------------------------------------------------- + + +def test_build_server_spec_payload_injects_hostname() -> None: + from l4d2web.services.l4d2_facade import build_server_spec_payload + + bp = Blueprint(id=1, user_id=1, name="bp", arguments="[]", config='["sv_consistency 1"]') + srv = Server(id=1, user_id=1, blueprint_id=1, name="alpha", port=27015, rcon_password="sekret") + spec = build_server_spec_payload(srv, bp, [], resolved_hostname="My Server") + cfg = spec["config"] + assert 'hostname "My Server"' in cfg + assert cfg[-1] == 'rcon_password "sekret"' + + +def test_build_server_spec_payload_omits_hostname_when_empty() -> None: + from l4d2web.services.l4d2_facade import build_server_spec_payload + + bp = Blueprint(id=1, user_id=1, name="bp", arguments="[]", config="[]") + srv = Server(id=1, user_id=1, blueprint_id=1, name="alpha", port=27015, rcon_password="sekret") + spec = build_server_spec_payload(srv, bp, []) + for line in spec["config"]: + assert not line.startswith("hostname ") + + +def test_initialize_server_resolves_fallback_hostname( + monkeypatch: pytest.MonkeyPatch, server_with_blueprint, +) -> None: + """When server.hostname is empty, deploy emits hostname " ".""" + from l4d2web.services.l4d2_facade import initialize_server + + spec_contents: list[str] = [] + + def fake_run_command(cmd, **kwargs): + nonlocal spec_contents + spec_path = cmd[cmd.index("-f") + 1] + spec_contents.append(Path(spec_path).read_text()) + return CommandResult(returncode=0, stdout="", stderr="") + + monkeypatch.setattr("l4d2web.services.host_commands.run_command", fake_run_command) + + server_id, _ = server_with_blueprint + initialize_server(server_id) + + assert len(spec_contents) == 1 + assert "hostname" in spec_contents[0] + # The fixture creates user "alice" and server named "alpha" + assert '"alice alpha"' in spec_contents[0] + +