109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
from __future__ import annotations
|
|||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pshelpers import fake_visual, make_settings, needs_pg
|
||
|
|
|
||
|
|
from packetstation import cli, doctor, guard
|
||
|
|
from packetstation.settings import Settings
|
||
|
|
|
||
|
|
ALL = ",".join(doctor.SKIPPABLE)
|
||
|
|
|
||
|
|
|
||
|
|
def test_all_skipped_json_shape(dev_visual: None, capsys: pytest.CaptureFixture[str]) -> None:
|
||
|
|
rc = cli.main(["doctor", "--json", "--skip", ALL])
|
||
|
|
out = json.loads(capsys.readouterr().out)
|
||
|
|
assert rc == 0
|
||
|
|
assert isinstance(out, list) and out
|
||
|
|
for item in out:
|
||
|
|
assert set(item) == {"name", "status", "detail", "hint"}
|
||
|
|
assert item["status"] in ("PASS", "WARN", "FAIL", "SKIP")
|
||
|
|
by = {c["name"]: c for c in out}
|
||
|
|
assert by["config"]["status"] == "PASS"
|
||
|
|
assert by["config: stack guard"]["status"] == "PASS"
|
||
|
|
assert by["tz"]["status"] == "PASS"
|
||
|
|
for key in doctor.SKIPPABLE:
|
||
|
|
assert by[key]["status"] == "SKIP"
|
||
|
|
|
||
|
|
|
||
|
|
def test_text_report(dev_visual: None, capsys: pytest.CaptureFixture[str]) -> None:
|
||
|
|
assert cli.main(["doctor", "--skip", ALL]) == 0
|
||
|
|
assert "[PASS] config" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_guard_failure_fails_doctor(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
|
||
|
|
monkeypatch.setattr(guard, "load_visual_settings", lambda: fake_visual(True, explicit=True))
|
||
|
|
assert cli.main(["doctor", "--json", "--skip", ALL]) == 1
|
||
|
|
by = {c["name"]: c for c in json.loads(capsys.readouterr().out)}
|
||
|
|
assert by["config: stack guard"]["status"] == "FAIL"
|
||
|
|
|
||
|
|
|
||
|
|
def test_visual_refuses_production_off_prod(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||
|
|
monkeypatch.setattr(guard, "load_visual_settings", lambda: fake_visual(True, explicit=True))
|
||
|
|
[c] = doctor.check_visual(make_settings(tmp_path, stack="test"))
|
||
|
|
assert c.status == "FAIL" and "refusing PRODUCTION" in c.detail
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_stack_fails_config(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
|
||
|
|
monkeypatch.delenv("STACK")
|
||
|
|
assert cli.main(["doctor", "--json", "--skip", ALL]) == 1
|
||
|
|
by = {c["name"]: c for c in json.loads(capsys.readouterr().out)}
|
||
|
|
assert by["config"]["status"] == "FAIL" and "STACK" in by["config"]["detail"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_ldap_insecure_is_a_warning(
|
||
|
|
dev_visual: None, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||
|
|
) -> None:
|
||
|
|
monkeypatch.setenv("LDAP_ALLOW_INSECURE", "true")
|
||
|
|
assert cli.main(["doctor", "--json", "--skip", ALL]) == 0
|
||
|
|
out = json.loads(capsys.readouterr().out)
|
||
|
|
assert any(c["status"] == "WARN" and "LDAP_ALLOW_INSECURE" in c["detail"] for c in out)
|
||
|
|
|
||
|
|
|
||
|
|
def test_unknown_skip_value() -> None:
|
||
|
|
with pytest.raises(SystemExit):
|
||
|
|
doctor.parse_skip("visual,bogus")
|
||
|
|
|
||
|
|
|
||
|
|
def test_volume_checks(tmp_path: Path) -> None:
|
||
|
|
(tmp_path / "assets").mkdir()
|
||
|
|
checks = doctor.check_volume(make_settings(tmp_path))
|
||
|
|
assert checks[0].status in ("PASS", "WARN") and checks[1].status == "SKIP"
|
||
|
|
(tmp_path / "bk").mkdir()
|
||
|
|
checks = doctor.check_volume(make_settings(tmp_path, backup_dir=tmp_path / "bk"))
|
||
|
|
assert checks[1].status == "PASS"
|
||
|
|
checks = doctor.check_volume(make_settings(tmp_path, assets_dir=tmp_path / "missing"))
|
||
|
|
assert checks[0].status == "FAIL"
|
||
|
|
|
||
|
|
|
||
|
|
def test_tz() -> None:
|
||
|
|
assert doctor.check_tz("America/Denver").status == "PASS"
|
||
|
|
assert doctor.check_tz("Mars/Olympus").status == "FAIL"
|
||
|
|
|
||
|
|
|
||
|
|
def test_ldap_plain_url_refused(tmp_path: Path) -> None:
|
||
|
|
(tmp_path / "ldap").write_text("x\n")
|
||
|
|
s = make_settings(
|
||
|
|
tmp_path,
|
||
|
|
ldap_url="ldap://dc1.example.test",
|
||
|
|
ldap_bind_dn="CN=svc,DC=example,DC=test",
|
||
|
|
ldap_password_file=tmp_path / "ldap",
|
||
|
|
)
|
||
|
|
[c] = doctor.check_ldap(s)
|
||
|
|
assert c.status == "FAIL" and "without STARTTLS" in c.detail
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_report() -> None:
|
||
|
|
text = "[PASS] Settings dev / x\n[FAIL] Connect timeout\n -> check the VPN\n\n1 failed, 0 warnings"
|
||
|
|
checks = doctor.parse_report("visual", text)
|
||
|
|
assert [(c.name, c.status) for c in checks] == [("visual: Settings", "PASS"), ("visual: Connect", "FAIL")]
|
||
|
|
assert checks[1].hint == "check the VPN"
|
||
|
|
|
||
|
|
|
||
|
|
@needs_pg
|
||
|
|
def test_pg_doctor_db_check(pg_settings: Settings) -> None:
|
||
|
|
checks = doctor.check_db(pg_settings)
|
||
|
|
assert [c.status for c in checks] == ["PASS"] * 4, [(c.name, c.detail) for c in checks]
|