umbau 1.3
This commit is contained in:
199
tests/test_rdp_drive_cleanup.py
Normal file
199
tests/test_rdp_drive_cleanup.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""Tests zu den beiden am 3.9. gefundenen, still fehlschlagenden Fehlern:
|
||||
|
||||
1. scripts/cleanup_rdp_drives.py -- der Aufraeummechanismus fuer die pro
|
||||
Sitzung umgeleiteten RDP-Laufwerke (Befund E6). Die vorherige,
|
||||
App-interne Loesung konnte prinzipiell nicht funktionieren (Rechte) und
|
||||
hat ihren PermissionError wegen shutil.rmtree(..., ignore_errors=True)
|
||||
auch noch verschluckt.
|
||||
|
||||
2. static/js/terminal.js -- Regressionsschutz gegen die Fehlerklasse
|
||||
"Terminal-Option, die es in xterm.js gar nicht gibt". Genau daran ist
|
||||
Copy-on-Select gescheitert (copyOnSelect stammt aus VS Code, nicht aus
|
||||
xterm.js; xterm.js verwirft unbekannte Optionen kommentarlos).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _load_cleanup_module():
|
||||
path = PROJECT_ROOT / "scripts" / "cleanup_rdp_drives.py"
|
||||
spec = importlib.util.spec_from_file_location("cleanup_rdp_drives", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["cleanup_rdp_drives"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def data_dir(tmp_path: Path) -> Path:
|
||||
"""Datenverzeichnis mit DB und ein paar Laufwerksverzeichnissen:
|
||||
Sitzung 66 und 70 beendet, Sitzung 67 laeuft noch."""
|
||||
(tmp_path / "rdp-drives").mkdir()
|
||||
for name in ("4_66", "4_67", "5_70"):
|
||||
(tmp_path / "rdp-drives" / name).mkdir()
|
||||
(tmp_path / "rdp-drives" / "4_66" / "uebertragen.txt").write_text("inhalt")
|
||||
|
||||
conn = sqlite3.connect(tmp_path / "jumphost.db")
|
||||
conn.execute("CREATE TABLE sessions (id INTEGER PRIMARY KEY, ended_at TEXT)")
|
||||
conn.executemany(
|
||||
"INSERT INTO sessions (id, ended_at) VALUES (?, ?)",
|
||||
[(66, "2026-09-03T10:00:00.000Z"), (67, None), (70, "2026-09-03T09:00:00.000Z")],
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_entfernt_nur_beendete_sitzungen(data_dir: Path):
|
||||
"""Kern der Sache: eine NOCH LAUFENDE Sitzung darf ihr Laufwerk nicht
|
||||
unter den Fuessen weggeraeumt bekommen. Deshalb entscheidet der
|
||||
Datenbankzustand und keine Altersheuristik ueber mtime -- eine lange,
|
||||
aber stille Sitzung (Fernwartung ueber Nacht) waere sonst betroffen."""
|
||||
module = _load_cleanup_module()
|
||||
removed, _ = module.cleanup(data_dir)
|
||||
|
||||
drives = data_dir / "rdp-drives"
|
||||
assert removed == 2
|
||||
assert not (drives / "4_66").exists()
|
||||
assert not (drives / "5_70").exists()
|
||||
assert (drives / "4_67").is_dir(), "laufende Sitzung wurde faelschlich entfernt"
|
||||
|
||||
|
||||
def test_fremde_eintraege_bleiben_unberuehrt(data_dir: Path):
|
||||
"""Das Skript laeuft als root. Alles, was nicht exakt dem von
|
||||
rdp_drive_path() erzeugten Muster '{host_id}_{session_id}' entspricht,
|
||||
wird bewusst nicht angefasst."""
|
||||
module = _load_cleanup_module()
|
||||
drives = data_dir / "rdp-drives"
|
||||
(drives / "fremdes_verzeichnis").mkdir()
|
||||
(drives / "lose_datei").write_text("x")
|
||||
(drives / "4_66_backup").mkdir()
|
||||
|
||||
module.cleanup(data_dir)
|
||||
|
||||
assert (drives / "fremdes_verzeichnis").is_dir()
|
||||
assert (drives / "lose_datei").is_file()
|
||||
assert (drives / "4_66_backup").is_dir()
|
||||
|
||||
|
||||
def test_ohne_lesbare_datenbank_wird_nichts_geloescht(tmp_path: Path):
|
||||
"""Sicherung gegen den schlimmsten denkbaren Fall: ohne verlaessliche
|
||||
Sitzungsliste wuerde JEDE Sitzung als beendet gelten. Die DB wird
|
||||
deshalb nur-lesend geoeffnet (mode=ro) -- sqlite3.connect wuerde sonst
|
||||
stillschweigend eine leere Datenbank anlegen."""
|
||||
module = _load_cleanup_module()
|
||||
(tmp_path / "rdp-drives" / "4_66").mkdir(parents=True)
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
module.cleanup(tmp_path)
|
||||
|
||||
assert (tmp_path / "rdp-drives" / "4_66").is_dir()
|
||||
assert not (tmp_path / "jumphost.db").exists(), "DB wurde faelschlich angelegt"
|
||||
|
||||
|
||||
def test_dry_run_loescht_nichts(data_dir: Path):
|
||||
module = _load_cleanup_module()
|
||||
removed, _ = module.cleanup(data_dir, dry_run=True)
|
||||
assert removed == 2
|
||||
assert (data_dir / "rdp-drives" / "4_66").is_dir()
|
||||
|
||||
|
||||
def test_verzeichnisname_passt_zu_rdp_drive_path():
|
||||
"""Bindet das Skript an die Anwendung: aendert jemand rdp_drive_path()
|
||||
(etwa zurueck auf die verschachtelte Form host/session, die am 2.9. den
|
||||
EACCES ausgeloest hat), schlaegt dieser Test fehl statt dass der
|
||||
Aufraeumdienst stumm nichts mehr findet."""
|
||||
from app.rdp_proxy.guacd_client import rdp_drive_path
|
||||
|
||||
module = _load_cleanup_module()
|
||||
name = rdp_drive_path(4, 66).name
|
||||
match = module._DRIVE_DIR_RE.match(name)
|
||||
assert match is not None, f"rdp_drive_path erzeugt '{name}', das Skript erwartet '{{host}}_{{session}}'"
|
||||
assert match.group("host_id") == "4"
|
||||
assert match.group("session_id") == "66"
|
||||
assert rdp_drive_path(4, 66).parent.name == "rdp-drives", "eine Ebene tiefer -> guacd kann sie nicht anlegen"
|
||||
|
||||
|
||||
# --- Frontend-Regressionsschutz ---------------------------------------------
|
||||
|
||||
def _vendor_xterm() -> str:
|
||||
return (PROJECT_ROOT / "static" / "js" / "vendor" / "xterm.js").read_text(
|
||||
encoding="utf-8", errors="ignore"
|
||||
)
|
||||
|
||||
|
||||
def _terminal_js(ohne_kommentare: bool = False) -> str:
|
||||
"""terminal.js, auf Wunsch ohne Kommentare.
|
||||
|
||||
Noetig, weil die Kommentare den frueheren Fehler ABSICHTLICH beim Namen
|
||||
nennen ("KEIN copyOnSelect hier ...", "Bitte Strg+Umschalt+C verwenden"
|
||||
als Zitat des alten, falschen Hinweistextes). Eine reine Textsuche ueber
|
||||
die ganze Datei wuerde genau diese Dokumentation als Rueckfall werten.
|
||||
"""
|
||||
source = (PROJECT_ROOT / "static" / "js" / "terminal.js").read_text(encoding="utf-8")
|
||||
if not ohne_kommentare:
|
||||
return source
|
||||
source = re.sub(r"/\*.*?\*/", "", source, flags=re.DOTALL) # Blockkommentare
|
||||
source = re.sub(r"^\s*//.*$", "", source, flags=re.MULTILINE) # ganze Kommentarzeilen
|
||||
return source
|
||||
|
||||
|
||||
def test_terminal_js_setzt_keine_unbekannten_xterm_optionen():
|
||||
"""xterm.js verwirft unbekannte Konstruktor-Optionen KOMMENTARLOS. Genau
|
||||
das ist mit `copyOnSelect: true` passiert (eine VS-Code-Einstellung, in
|
||||
xterm.js nicht existent): kein Fehler, keine Warnung, keine Wirkung --
|
||||
'Markieren kopiert' war seit jeher tot. Dieser Test prueft jede im
|
||||
Terminal-Konstruktor gesetzte Option gegen die tatsaechlich mitgelieferte
|
||||
xterm.js-Datei."""
|
||||
source = _terminal_js(ohne_kommentare=True)
|
||||
vendor = _vendor_xterm()
|
||||
|
||||
block = re.search(r"new Terminal\(\{(.*?)\n \}\);", source, re.DOTALL)
|
||||
assert block, "Terminal-Konstruktoraufruf in terminal.js nicht gefunden"
|
||||
|
||||
options = set(re.findall(r"^\s{8}([A-Za-z][A-Za-z0-9]*):", block.group(1), re.MULTILINE))
|
||||
assert options, "keine Optionen erkannt -- Test muss angepasst werden"
|
||||
|
||||
unbekannt = sorted(o for o in options if f"{o}" not in vendor)
|
||||
assert not unbekannt, (
|
||||
f"terminal.js setzt Optionen, die die ausgelieferte xterm.js nicht kennt: {unbekannt}. "
|
||||
"xterm.js ignoriert sie stillschweigend -- die Funktion fehlt dann ohne jede Fehlermeldung."
|
||||
)
|
||||
|
||||
|
||||
def test_copyonselect_ist_und_bleibt_verschwunden():
|
||||
"""Punktgenauer Schutz gegen exakt diesen Rueckfall."""
|
||||
source = _terminal_js(ohne_kommentare=True)
|
||||
konstruktor = re.search(r"new Terminal\(\{(.*?)\n \}\);", source, re.DOTALL)
|
||||
assert konstruktor and "copyOnSelect" not in konstruktor.group(1)
|
||||
assert "copyOnSelect" not in _vendor_xterm(), (
|
||||
"xterm.js kennt copyOnSelect inzwischen doch -- dann darf dieser Test angepasst werden."
|
||||
)
|
||||
|
||||
|
||||
def test_kopieren_beim_markieren_ist_selbst_implementiert():
|
||||
"""Der Ersatz fuer die nicht existente Option: onSelectionChange +
|
||||
eigener Clipboard-Aufruf mit execCommand-Ruecksprung."""
|
||||
source = _terminal_js(ohne_kommentare=True)
|
||||
assert "this.term.onSelectionChange(" in source
|
||||
assert "_copyToClipboard" in source
|
||||
assert 'document.execCommand("copy")' in source
|
||||
|
||||
|
||||
def test_keine_hinweise_auf_in_chrome_belegte_tastenkombinationen():
|
||||
"""Strg+Umschalt+C/V sind in Chrome/Edge fest fuer die Entwicklertools
|
||||
reserviert und erreichen die Seite nie -- ein Fehlertext, der genau das
|
||||
empfiehlt, schickt den Benutzer in die Irre."""
|
||||
source = _terminal_js(ohne_kommentare=True)
|
||||
fehlerhafte_hinweise = re.findall(r'"[^"]*Bitte[^"]*Strg\+Umschalt\+[CV][^"]*"', source)
|
||||
assert not fehlerhafte_hinweise, fehlerhafte_hinweise
|
||||
Reference in New Issue
Block a user