"""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