|
4 | 4 | import logging |
5 | 5 | import os |
6 | 6 |
|
| 7 | +import pytest |
| 8 | + |
7 | 9 | from historian.app import build_app |
8 | 10 | from historian.cli import main |
9 | 11 | from historian.config import Settings |
10 | | -from historian.debug import QueryTranscript, configure_logging |
| 12 | +from historian.debug import QueryTranscript, _prepare_private_file, configure_logging |
11 | 13 |
|
12 | 14 | from conftest import event |
13 | 15 |
|
@@ -157,3 +159,37 @@ def test_operational_log_uses_metadata_not_event_payload(tmp_path, vesper_manife |
157 | 159 | assert "event-1" in content |
158 | 160 | assert "music.playback.started" in content |
159 | 161 | assert "do-not-store" not in content |
| 162 | + |
| 163 | + |
| 164 | +def test_default_debug_paths_are_not_in_tmp(monkeypatch) -> None: |
| 165 | + """Default debug log paths must live under the XDG data dir, not /tmp.""" |
| 166 | + monkeypatch.setenv("XDG_DATA_HOME", "/tmp/xdg-data-fixture") |
| 167 | + settings = Settings() |
| 168 | + assert not settings.debug_log_path.startswith("/tmp/historian") |
| 169 | + assert not settings.resolver_debug_log_path.startswith("/tmp/historian") |
| 170 | + assert "/historian/debug.log" in settings.debug_log_path |
| 171 | + assert "/historian/resolver.log" in settings.resolver_debug_log_path |
| 172 | + |
| 173 | + |
| 174 | +def test_prepare_private_file_refuses_symlink(tmp_path) -> None: |
| 175 | + """_prepare_private_file must not follow a pre-existing symlink (O_NOFOLLOW). |
| 176 | +
|
| 177 | + A symlink at the target path is an attack vector for overwriting an |
| 178 | + arbitrary file; opening it must fail rather than write through it. |
| 179 | + """ |
| 180 | + real_file = tmp_path / "real-target.txt" |
| 181 | + real_file.write_text("original\n", encoding="utf-8") |
| 182 | + link = tmp_path / "debug.log" |
| 183 | + os.symlink(real_file, link) |
| 184 | + with pytest.raises(OSError): |
| 185 | + _prepare_private_file(link, clear=True) |
| 186 | + # The target the symlink pointed at must be untouched. |
| 187 | + assert real_file.read_text(encoding="utf-8") == "original\n" |
| 188 | + |
| 189 | + |
| 190 | +def test_prepare_private_file_creates_new_file(tmp_path) -> None: |
| 191 | + """A normal (non-symlink) path is created with owner-only permissions.""" |
| 192 | + target = tmp_path / "debug.log" |
| 193 | + _prepare_private_file(target, clear=True) |
| 194 | + assert target.is_file() |
| 195 | + assert os.stat(target).st_mode & 0o777 == 0o600 |
0 commit comments