fix: Audit-Findings — Realize-Handler, Thread-Safety, Input-Validierung
- _on_realize implementiert (war nur connected, nicht definiert) - do_unrealize für as_file() Context-Manager-Cleanup - threading.Lock für _greetd_sock Race Condition - TOML-Parsing-Fehler abfangen statt Crash - last-user Datei: Längen- und Zeichenvalidierung - detect_locale: non-alpha LANG-Werte abweisen - exec_cmd Plausibility-Check mit shutil.which - Exception-Details ins Log statt in die UI - subprocess.run Timeout für Power-Actions - Sequence[Path] statt tuple[Path, ...] in get_sessions - Mock-Server _recvall für fragmentierte Reads - [behavior]-Config-Sektion entfernt (unimplementiert) - Design Decisions in CLAUDE.md dokumentiert
This commit is contained in:
@@ -35,6 +35,14 @@ class TestLoadConfig:
|
||||
|
||||
assert config.background is None
|
||||
|
||||
def test_returns_defaults_for_corrupt_toml(self, tmp_path: Path) -> None:
|
||||
toml_file = tmp_path / "moongreet.toml"
|
||||
toml_file.write_text("this is not valid [[[ toml !!!")
|
||||
|
||||
config = load_config(toml_file)
|
||||
|
||||
assert config.background is None
|
||||
|
||||
def test_resolves_relative_path_against_config_dir(self, tmp_path: Path) -> None:
|
||||
toml_file = tmp_path / "moongreet.toml"
|
||||
toml_file.write_text(
|
||||
|
||||
+11
-3
@@ -63,6 +63,13 @@ class TestDetectLocale:
|
||||
|
||||
assert result == "en"
|
||||
|
||||
def test_rejects_non_alpha_lang(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LANG", "../../etc")
|
||||
|
||||
result = detect_locale()
|
||||
|
||||
assert result == "en"
|
||||
|
||||
|
||||
class TestLoadStrings:
|
||||
"""Tests for loading the correct string table."""
|
||||
@@ -111,8 +118,9 @@ class TestLoadStrings:
|
||||
result = strings.faillock_attempts_remaining.format(n=1)
|
||||
assert "1" in result
|
||||
|
||||
def test_connection_error_template(self) -> None:
|
||||
def test_connection_error_is_generic(self) -> None:
|
||||
strings = load_strings("en")
|
||||
|
||||
result = strings.connection_error.format(error="timeout")
|
||||
assert "timeout" in result
|
||||
# Error messages should not contain format placeholders (no info leakage)
|
||||
assert "{" not in strings.connection_error
|
||||
assert "{" not in strings.socket_error
|
||||
|
||||
@@ -40,16 +40,27 @@ class MockGreetd:
|
||||
self._thread = threading.Thread(target=self._serve, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
@staticmethod
|
||||
def _recvall(conn: socket.socket, n: int) -> bytes:
|
||||
"""Receive exactly n bytes from a socket, handling fragmented reads."""
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
chunk = conn.recv(n - len(buf))
|
||||
if not chunk:
|
||||
break
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
def _serve(self) -> None:
|
||||
conn, _ = self._server.accept()
|
||||
try:
|
||||
for response in self._responses:
|
||||
# Receive a message
|
||||
header = conn.recv(4)
|
||||
header = self._recvall(conn, 4)
|
||||
if len(header) < 4:
|
||||
break
|
||||
length = struct.unpack("!I", header)[0]
|
||||
payload = conn.recv(length)
|
||||
payload = self._recvall(conn, length)
|
||||
msg = json.loads(payload.decode("utf-8"))
|
||||
self._received.append(msg)
|
||||
|
||||
@@ -182,6 +193,10 @@ class TestLoginFlow:
|
||||
class TestFaillockWarning:
|
||||
"""Tests for the faillock warning message logic."""
|
||||
|
||||
def test_no_warning_on_zero_attempts(self) -> None:
|
||||
strings = load_strings("de")
|
||||
assert faillock_warning(0, strings) is None
|
||||
|
||||
def test_no_warning_on_first_attempt(self) -> None:
|
||||
strings = load_strings("de")
|
||||
assert faillock_warning(1, strings) is None
|
||||
@@ -231,3 +246,21 @@ class TestLastUser:
|
||||
from moongreet.greeter import GreeterWindow
|
||||
result = GreeterWindow._load_last_user()
|
||||
assert result is None
|
||||
|
||||
def test_load_last_user_rejects_oversized_username(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
cache_path = tmp_path / "last-user"
|
||||
cache_path.write_text("a" * 300)
|
||||
monkeypatch.setattr("moongreet.greeter.LAST_USER_PATH", cache_path)
|
||||
|
||||
from moongreet.greeter import GreeterWindow
|
||||
result = GreeterWindow._load_last_user()
|
||||
assert result is None
|
||||
|
||||
def test_load_last_user_rejects_invalid_characters(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
cache_path = tmp_path / "last-user"
|
||||
cache_path.write_text("../../etc/passwd")
|
||||
monkeypatch.setattr("moongreet.greeter.LAST_USER_PATH", cache_path)
|
||||
|
||||
from moongreet.greeter import GreeterWindow
|
||||
result = GreeterWindow._load_last_user()
|
||||
assert result is None
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ from unittest.mock import patch, call
|
||||
|
||||
import pytest
|
||||
|
||||
from moongreet.power import reboot, shutdown
|
||||
from moongreet.power import reboot, shutdown, POWER_TIMEOUT
|
||||
|
||||
|
||||
class TestReboot:
|
||||
@@ -17,7 +17,7 @@ class TestReboot:
|
||||
reboot()
|
||||
|
||||
mock_run.assert_called_once_with(
|
||||
["loginctl", "reboot"], check=True
|
||||
["loginctl", "reboot"], check=True, timeout=POWER_TIMEOUT
|
||||
)
|
||||
|
||||
@patch("moongreet.power.subprocess.run")
|
||||
@@ -36,7 +36,7 @@ class TestShutdown:
|
||||
shutdown()
|
||||
|
||||
mock_run.assert_called_once_with(
|
||||
["loginctl", "poweroff"], check=True
|
||||
["loginctl", "poweroff"], check=True, timeout=POWER_TIMEOUT
|
||||
)
|
||||
|
||||
@patch("moongreet.power.subprocess.run")
|
||||
|
||||
Reference in New Issue
Block a user