Extract environment-backed settings into a dedicated module and switch the aiohttp app to a factory-based bootstrap. Move runtime dependencies into typed app state, tighten CORS defaults, reject sensitive inline yt-dlp options, and harden the container entrypoint validation and ownership flow. Update tests to cover settings validation, app factory behavior, CORS policy, and entrypoint safety checks.
145 lines
3.6 KiB
Python
145 lines
3.6 KiB
Python
"""Tests for the hardened container entrypoint."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
ENTRYPOINT = REPO_ROOT / "docker-entrypoint.sh"
|
|
|
|
|
|
def _write_executable(path: Path, content: str) -> None:
|
|
path.write_text(content, encoding="utf-8")
|
|
path.chmod(0o755)
|
|
|
|
|
|
def _prepare_stub_bin(tmp_path: Path) -> Path:
|
|
stub_bin = tmp_path / "bin"
|
|
stub_bin.mkdir()
|
|
|
|
_write_executable(
|
|
stub_bin / "id",
|
|
"""#!/bin/sh
|
|
case "$1" in
|
|
-u) echo "${TEST_ID_U:-1000}" ;;
|
|
-g) echo "${TEST_ID_G:-1000}" ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
""",
|
|
)
|
|
_write_executable(
|
|
stub_bin / "gosu",
|
|
"""#!/bin/sh
|
|
shift
|
|
"$@"
|
|
""",
|
|
)
|
|
_write_executable(
|
|
stub_bin / "chown",
|
|
"""#!/bin/sh
|
|
echo "$@" >> "${CHOWN_LOG}"
|
|
exit 0
|
|
""",
|
|
)
|
|
_write_executable(
|
|
stub_bin / "python3",
|
|
"""#!/bin/sh
|
|
echo "$@" >> "${PYTHON_LOG}"
|
|
exit 0
|
|
""",
|
|
)
|
|
_write_executable(
|
|
stub_bin / "bgutil-pot",
|
|
"""#!/bin/sh
|
|
echo "$@" >> "${BGUTIL_LOG}"
|
|
exit 0
|
|
""",
|
|
)
|
|
return stub_bin
|
|
|
|
|
|
def _base_env(tmp_path: Path) -> dict[str, str]:
|
|
download_dir = tmp_path / "downloads"
|
|
state_dir = tmp_path / "state"
|
|
temp_dir = tmp_path / "temp"
|
|
download_dir.mkdir(exist_ok=True)
|
|
state_dir.mkdir(exist_ok=True)
|
|
temp_dir.mkdir(exist_ok=True)
|
|
|
|
return {
|
|
"DOWNLOAD_DIR": str(download_dir),
|
|
"STATE_DIR": str(state_dir),
|
|
"TEMP_DIR": str(temp_dir),
|
|
"PUID": "1000",
|
|
"PGID": "1000",
|
|
"UMASK": "022",
|
|
"CHOWN_DIRS": "true",
|
|
}
|
|
|
|
|
|
def _run_entrypoint(tmp_path: Path, **overrides: str) -> subprocess.CompletedProcess[str]:
|
|
stub_bin = _prepare_stub_bin(tmp_path)
|
|
env = os.environ.copy()
|
|
env.update(_base_env(tmp_path))
|
|
env.update(
|
|
{
|
|
"PATH": f"{stub_bin}:{env['PATH']}",
|
|
"CHOWN_LOG": str(tmp_path / "chown.log"),
|
|
"PYTHON_LOG": str(tmp_path / "python.log"),
|
|
"BGUTIL_LOG": str(tmp_path / "bgutil.log"),
|
|
"TEST_ID_U": "0",
|
|
"TEST_ID_G": "0",
|
|
"UID": "",
|
|
"GID": "",
|
|
}
|
|
)
|
|
env.update(overrides)
|
|
return subprocess.run(
|
|
["sh", str(ENTRYPOINT)],
|
|
cwd=REPO_ROOT,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("key", "value", "expected"),
|
|
[
|
|
("PUID", "abc", "PUID must be numeric"),
|
|
("PGID", "abc", "PGID must be numeric"),
|
|
("UMASK", "89", "UMASK must be a 3 or 4 digit octal value"),
|
|
],
|
|
)
|
|
def test_entrypoint_rejects_invalid_identity_inputs(tmp_path, key, value, expected):
|
|
result = _run_entrypoint(tmp_path, **{key: value})
|
|
assert result.returncode != 0
|
|
assert expected in result.stderr
|
|
|
|
|
|
def test_entrypoint_chowns_only_data_directories(tmp_path):
|
|
result = _run_entrypoint(tmp_path)
|
|
assert result.returncode == 0
|
|
chown_log = (tmp_path / "chown.log").read_text(encoding="utf-8")
|
|
assert "/app" not in chown_log
|
|
assert str(tmp_path / "downloads") in chown_log
|
|
assert str(tmp_path / "state") in chown_log
|
|
assert str(tmp_path / "temp") in chown_log
|
|
|
|
|
|
def test_entrypoint_fails_when_directories_are_not_accessible(tmp_path):
|
|
protected_dir = tmp_path / "state"
|
|
protected_dir.mkdir(exist_ok=True)
|
|
protected_dir.chmod(0)
|
|
try:
|
|
result = _run_entrypoint(tmp_path, CHOWN_DIRS="false")
|
|
finally:
|
|
protected_dir.chmod(0o700)
|
|
assert result.returncode != 0
|
|
assert "Configured directories are not accessible" in result.stderr
|