left4me/l4d2host/tests/test_paths.py
mwiegand f81e839ba2
security: harden boundary inputs and production defaults
- validate instance names at the host lib and web boundary against
  [a-z0-9][a-z0-9_-]{0,63} to prevent path traversal via Server.name
- fail-closed on SECRET_KEY: load_config returns None when env unset,
  create_app raises if missing or "dev" outside TESTING
- close login timing oracle by hashing a dummy digest when the user
  is not found, equalizing response time
- set SESSION_COOKIE_SECURE outside TESTING
- delete_instance tolerates stop_service and fusermount3 failures so
  partially-initialized instances clean up without contract breaks;
  drops the is_mount() preflight that violated AGENTS.md
- document claim_next_job's single-process assumption
- clarify emit_step contract via docstring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:53:33 +02:00

98 lines
2.6 KiB
Python

from pathlib import Path
import pytest
from l4d2host.paths import (
get_left4me_root,
overlay_path,
validate_instance_name,
validate_overlay_ref,
)
def test_get_left4me_root_defaults_to_var_lib_left4me(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("LEFT4ME_ROOT", raising=False)
assert get_left4me_root() == Path("/var/lib/left4me")
def test_get_left4me_root_uses_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
assert get_left4me_root() == tmp_path
def test_get_left4me_root_rejects_relative_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LEFT4ME_ROOT", "var/lib/left4me")
with pytest.raises(ValueError):
get_left4me_root()
@pytest.mark.parametrize("raw", ["", " "])
def test_get_left4me_root_rejects_empty_env(monkeypatch: pytest.MonkeyPatch, raw: str) -> None:
monkeypatch.setenv("LEFT4ME_ROOT", raw)
with pytest.raises(ValueError):
get_left4me_root()
@pytest.mark.parametrize("ref", ["standard", "competitive/base", "users/42/custom"])
def test_validate_overlay_ref_accepts_safe_refs(ref: str) -> None:
assert validate_overlay_ref(ref) == ref
@pytest.mark.parametrize(
"ref",
["", "/tmp/bad", "../bad", "bad/../evil", "bad//evil", " bad", "bad ", ".", "bad/./evil"],
)
def test_validate_overlay_ref_rejects_unsafe_refs(ref: str) -> None:
with pytest.raises(ValueError):
validate_overlay_ref(ref)
@pytest.mark.parametrize("name", ["srv1", "alpha", "my-server", "srv_01", "a", "a" * 64])
def test_validate_instance_name_accepts_safe_names(name: str) -> None:
assert validate_instance_name(name) == name
@pytest.mark.parametrize(
"name",
[
"",
" ",
"..",
".",
"../foo",
"foo/..",
"foo/bar",
"foo\\bar",
" foo",
"foo ",
"Foo",
"foo bar",
"foo$",
"-foo",
"_foo",
"foo@bar",
"a" * 65,
],
)
def test_validate_instance_name_rejects_unsafe_names(name: str) -> None:
with pytest.raises(ValueError):
validate_instance_name(name)
def test_overlay_path_resolves_under_root(tmp_path: Path) -> None:
assert overlay_path("standard", root=tmp_path) == tmp_path / "overlays" / "standard"
def test_overlay_path_rejects_symlink_escape(tmp_path: Path) -> None:
outside = tmp_path / "outside"
outside.mkdir()
overlays = tmp_path / "overlays"
overlays.mkdir()
(overlays / "escape").symlink_to(outside)
with pytest.raises(ValueError):
overlay_path("escape", root=tmp_path)