200 lines
8.2 KiB
Python
200 lines
8.2 KiB
Python
"""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
|