- 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>
98 lines
2.6 KiB
Python
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)
|