156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from tht.cli import app
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def _config(path: Path, *, absolute_sessions: Path | None = None) -> Path:
|
|
sessions = absolute_sessions or Path("sessions")
|
|
path.write_text(
|
|
"dwh:\n"
|
|
" type: postgres_direct\n"
|
|
" connection:\n"
|
|
" database: patient_db\n"
|
|
" schema: private_schema\n"
|
|
" user: pii_user\n"
|
|
" password: super-secret\n"
|
|
"roots:\n"
|
|
" artifacts: artifacts\n"
|
|
" indexes: indexes\n"
|
|
f" sessions: {sessions}\n"
|
|
)
|
|
return path
|
|
|
|
|
|
def test_doctor_json_reports_portable_path_statuses_without_secrets(monkeypatch, tmp_path):
|
|
cfg = _config(tmp_path / "demo.yaml")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.stdout)
|
|
assert payload == {
|
|
"ok": True,
|
|
"components": {
|
|
"config": {"status": "ok"},
|
|
"data_root": {"status": "ok"},
|
|
"workspace_paths": {"status": "ok", "legacy_absolute": []},
|
|
},
|
|
}
|
|
assert "super-secret" not in result.stdout
|
|
assert "patient_db" not in result.stdout
|
|
assert "pii_user" not in result.stdout
|
|
assert result.stderr == ""
|
|
|
|
|
|
def test_doctor_json_flags_absolute_legacy_paths(monkeypatch, tmp_path):
|
|
cfg = _config(tmp_path / "demo.yaml", absolute_sessions=tmp_path / "old-sessions")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 0
|
|
payload = json.loads(result.stdout)
|
|
assert payload["components"]["workspace_paths"] == {
|
|
"status": "warning",
|
|
"legacy_absolute": ["sessions"],
|
|
}
|
|
assert str(tmp_path) not in result.stdout
|
|
|
|
|
|
def test_doctor_json_returns_structured_config_error(monkeypatch, tmp_path):
|
|
cfg = _config(tmp_path / "demo.yaml")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
cfg.write_text(cfg.read_text().replace("sessions: sessions", "sessions: ../../private"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 1
|
|
payload = json.loads(result.stdout)
|
|
assert payload["ok"] is False
|
|
assert payload["components"]["workspace_paths"]["status"] == "error"
|
|
assert "outside workspace root" in payload["components"]["workspace_paths"]["message"]
|
|
assert result.stderr == ""
|
|
|
|
|
|
def test_doctor_json_does_not_echo_invalid_config_values(monkeypatch, tmp_path):
|
|
cfg = _config(tmp_path / "demo.yaml")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
cfg.write_text(cfg.read_text().replace("database: patient_db", ""))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 1
|
|
payload = json.loads(result.stdout)
|
|
assert payload["components"]["config"] == {
|
|
"status": "error",
|
|
"message": "configuration is invalid or unreadable",
|
|
}
|
|
assert "super-secret" not in result.stdout
|
|
assert "pii_user" not in result.stdout
|
|
|
|
|
|
def test_doctor_json_normalizes_malformed_yaml(monkeypatch, tmp_path):
|
|
cfg = tmp_path / "demo.yaml"
|
|
cfg.write_text("password: super-secret\nroots: [unterminated")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 1
|
|
assert json.loads(result.stdout)["components"]["config"] == {
|
|
"status": "error",
|
|
"message": "configuration is invalid or unreadable",
|
|
}
|
|
assert result.stderr == ""
|
|
assert "super-secret" not in result.stdout
|
|
assert "Traceback" not in result.stdout
|
|
|
|
|
|
def test_doctor_json_normalizes_unreadable_config(monkeypatch, tmp_path):
|
|
cfg = tmp_path / "demo.yaml"
|
|
cfg.mkdir()
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 1
|
|
assert json.loads(result.stdout)["components"]["config"] == {
|
|
"status": "error",
|
|
"message": "configuration is invalid or unreadable",
|
|
}
|
|
assert result.stderr == ""
|
|
|
|
|
|
def test_doctor_human_output_is_actionable_and_redacted(monkeypatch, tmp_path):
|
|
cfg = _config(tmp_path / "demo.yaml", absolute_sessions=tmp_path / "patient-private")
|
|
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
|
|
|
result = runner.invoke(app, ["doctor", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "data_root: warning - set THT_DATA_ROOT to enable portable storage" in result.stdout
|
|
assert "workspace_paths: warning - absolute legacy roots: sessions" in result.stdout
|
|
assert str(tmp_path) not in result.stdout
|
|
assert "patient_db" not in result.stdout
|
|
assert "super-secret" not in result.stdout
|
|
|
|
|
|
def test_doctor_human_config_error_is_actionable_and_redacted(monkeypatch, tmp_path):
|
|
cfg = tmp_path / "patient-private.yaml"
|
|
cfg.write_text("password: super-secret\nroots: [unterminated")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
result = runner.invoke(app, ["doctor", "--config", str(cfg)])
|
|
|
|
assert result.exit_code == 1
|
|
assert "config: error - configuration is invalid or unreadable" in result.stdout
|
|
assert str(tmp_path) not in result.stdout
|
|
assert "super-secret" not in result.stdout
|
|
assert result.stderr == ""
|