feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde. Datenverlust: - veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" — Ergebnis nach error/, Original geloescht (Default delete). run_verapdf() trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das Original wird nicht entsorgt. - Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel daneben, mit Warnung; ProcessResult.output traegt den echten Pfad. Robustheit: - Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback. - RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen. - Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr. - Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt still unter /opt zu landen. - Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail. - Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet. - Logging explizit nach stdout (die Doku versprach das schon). Struktur: - Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung — die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte venv als gesund durchgewunken (nachgewiesen). - install.sh warnt in Containern, wenn systemd-journald nicht laeuft. Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify), Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS, AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte, Exit-Code-Tabelle. 254 Tests gruen (vorher 152). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""Punkt 5: Relative Pfade in der Config sind ein Fehler, keine stille Annahme.
|
||||
|
||||
`incoming = "in"` legte das Verzeichnis unter dem WorkingDirectory des
|
||||
Dienstes an (/opt/pdf-ocr-hotfolder/in) statt dort, wo der Scanner ablegt —
|
||||
ohne Warnung. Der Dienst schaute dann dauerhaft ins Leere.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pdf_ocr_hotfolder.config import ConfigError, load_config
|
||||
|
||||
_ABS = {
|
||||
"incoming": "/var/lib/pdf-ocr-hotfolder/incoming",
|
||||
"outgoing": "/var/lib/pdf-ocr-hotfolder/outgoing",
|
||||
"working": "/var/lib/pdf-ocr-hotfolder/working",
|
||||
"error": "/var/lib/pdf-ocr-hotfolder/error",
|
||||
}
|
||||
|
||||
|
||||
def _write(tmp_path: Path, paths: dict[str, str], extra: str = "") -> Path:
|
||||
cfg = tmp_path / "config.toml"
|
||||
zeilen = "\n".join(f'{k} = "{v}"' for k, v in paths.items())
|
||||
cfg.write_text(f"[paths]\n{zeilen}\n{extra}")
|
||||
return cfg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", list(_ABS))
|
||||
def test_relative_path_key_is_rejected(tmp_path: Path, key: str) -> None:
|
||||
paths = dict(_ABS)
|
||||
paths[key] = "in"
|
||||
with pytest.raises(ConfigError) as exc:
|
||||
load_config(_write(tmp_path, paths))
|
||||
msg = str(exc.value)
|
||||
assert key in msg
|
||||
assert "absolut" in msg.lower()
|
||||
assert "WorkingDirectory" in msg
|
||||
|
||||
|
||||
def test_dot_relative_path_is_rejected(tmp_path: Path) -> None:
|
||||
"""Auch './scans' und '../scans' sind relativ."""
|
||||
paths = dict(_ABS, incoming="./scans")
|
||||
with pytest.raises(ConfigError, match="incoming"):
|
||||
load_config(_write(tmp_path, paths))
|
||||
|
||||
|
||||
def test_absolute_paths_load(tmp_path: Path) -> None:
|
||||
cfg = load_config(_write(tmp_path, _ABS))
|
||||
assert cfg.paths.incoming == Path(_ABS["incoming"])
|
||||
|
||||
|
||||
def test_relative_archive_dir_is_rejected(tmp_path: Path) -> None:
|
||||
cfg = _write(tmp_path, _ABS,
|
||||
'\n[output]\noriginal_on_success = "archive"\n'
|
||||
'archive_dir = "archiv"\n')
|
||||
with pytest.raises(ConfigError) as exc:
|
||||
load_config(cfg)
|
||||
assert "archive_dir" in str(exc.value)
|
||||
|
||||
|
||||
def test_relative_upload_target_is_rejected(tmp_path: Path) -> None:
|
||||
cfg = _write(tmp_path, _ABS,
|
||||
'\n[upload.folder]\nenabled = true\ntarget = "fertig"\n')
|
||||
with pytest.raises(ConfigError) as exc:
|
||||
load_config(cfg)
|
||||
assert "target" in str(exc.value)
|
||||
|
||||
|
||||
def test_empty_archive_dir_and_target_are_fine(tmp_path: Path) -> None:
|
||||
"""Leer heißt 'nicht gesetzt' und bleibt erlaubt (das ist der Default)."""
|
||||
cfg = load_config(_write(tmp_path, _ABS,
|
||||
'\n[output]\narchive_dir = ""\n'
|
||||
'\n[upload.folder]\ntarget = ""\n'))
|
||||
assert cfg.output.archive_dir == ""
|
||||
assert cfg.folder.target == ""
|
||||
|
||||
|
||||
def test_absolute_archive_dir_and_target_load(tmp_path: Path) -> None:
|
||||
cfg = load_config(_write(tmp_path, _ABS,
|
||||
'\n[output]\narchive_dir = "/srv/archiv"\n'
|
||||
'\n[upload.folder]\ntarget = "/srv/fertig"\n'))
|
||||
assert cfg.output.archive_dir == "/srv/archiv"
|
||||
assert cfg.folder.target == "/srv/fertig"
|
||||
|
||||
|
||||
# ---------------- CLI ----------------
|
||||
|
||||
def test_main_returns_2_on_relative_path(tmp_path: Path, monkeypatch, capsys) -> None:
|
||||
cfg = _write(tmp_path, dict(_ABS, incoming="in"))
|
||||
monkeypatch.setattr(sys, "argv",
|
||||
["pdf-ocr-hotfolder", "--config", str(cfg), "--once"])
|
||||
from pdf_ocr_hotfolder.__main__ import main
|
||||
assert main() == 2
|
||||
err = capsys.readouterr().err
|
||||
assert "FEHLER" in err
|
||||
assert "incoming" in err
|
||||
|
||||
|
||||
def test_check_config_returns_2_on_relative_path(tmp_path: Path, monkeypatch,
|
||||
capsys) -> None:
|
||||
from pdf_ocr_hotfolder.__main__ import CHECK_ERROR, main
|
||||
|
||||
cfg = _write(tmp_path, dict(_ABS, outgoing="raus"))
|
||||
monkeypatch.setattr(sys, "argv",
|
||||
["pdf-ocr-hotfolder", "--config", str(cfg),
|
||||
"--check-config"])
|
||||
assert main() == CHECK_ERROR
|
||||
assert "outgoing" in capsys.readouterr().err
|
||||
Reference in New Issue
Block a user