"""Der Weg vom alten TESM in dieses: Uebersetzer und Import zusammen. Die Testdatei wird **genau so** gebaut, wie der Vorgaenger sie schreibt (PBKDF2-HMAC-SHA256, 390000 Runden, Fernet, derselbe Umschlag). Damit prueft der Test das echte Format und nicht meine Vorstellung davon. """ from __future__ import annotations import base64 import json from typing import Any import pytest pytest.importorskip("cryptography") LEGACY_PASS = "alte-passphrase-2026" NEW_PASS = "neue-passphrase-2026" @pytest.fixture() def tesm_app(instance_env: Any): from tesm import create_app return create_app(TESTING=True) def _seal_legacy(payload: dict[str, Any], passphrase: str) -> str: """Baut einen Umschlag wie ``export_data()`` im alten TESM.""" import secrets from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC salt = secrets.token_bytes(16) kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=390000) key = base64.urlsafe_b64encode(kdf.derive(passphrase.encode("utf-8"))) token = Fernet(key).encrypt(json.dumps(payload).encode("utf-8")) return json.dumps( { "app": "tesm", "version": 2, "exported_at": "2026-09-02 12:00:00", "salt": base64.urlsafe_b64encode(salt).decode("utf-8"), "payload": token.decode("utf-8"), } ) #: Ein Ausschnitt, der die Eigenheiten des alten Formats abdeckt: Switch- und #: Zugangsdatenbezug per Name, geschachtelte DHCP-Optionswerte, "name" statt #: "hostname" an der Reservierung, sowie Abschnitte, die nicht uebersetzt #: werden koennen. LEGACY_PAYLOAD: dict[str, Any] = { "credentials": [ {"name": "Switch-Admin", "username": "manager", "password": "geheim-1", "category": "switch"}, {"name": "Linux-Root", "username": "root", "password": "geheim-2", "category": "linux"}, {"name": "Windows-Dienst", "username": "svc", "password": "geheim-3", "category": "windows"}, ], "switches": [ {"hostname": "sw-keller", "ip": "192.168.80.2", "ssh_port": 22, "credential_name": "Switch-Admin"}, ], "devices": [ { "mac": "4c:52:62:25:b9:e4", "ip": "192.168.80.137", "port": "12", "name": "Autodarts", "switch_hostname": "sw-keller", "is_active": 1, "ssh_port": 22, "credential_name": "Linux-Root", }, { "mac": "aa:bb:cc:00:00:02", "ip": "192.168.80.50", "port": "3", "name": "Kasse", "switch_hostname": "sw-keller", "is_active": 1, "ssh_port": 22, "credential_name": "Windows-Dienst", }, ], "dhcp": { "settings": {"dhcp_domain": "firma.local", "dhcp_lease_default": "1200"}, "subnets": [ { "interface": "eth0", "range_start": "192.168.80.100", "range_end": "192.168.80.200", "gateway": "192.168.80.1", "dns": "192.168.80.1", "sort_order": 0, "enabled": 1, } ], "reservations": [{"mac": "aa:bb:cc:00:00:09", "ip": "192.168.80.90", "name": "drucker"}], "options": [ { "code": 225, "name": "url", "type": "string", "description": "Terminal-URL", "is_standard": 0, "values": [ {"device_mac": "4c:52:62:25:b9:e4", "value": "https://play.autodarts.com"}, {"device_mac": None, "value": "https://intranet.firma.local"}, ], }, # Ohne Wert gibt es nichts zu senden -- wird gemeldet, nicht uebernommen. {"code": 226, "name": "leer", "type": "string", "description": "", "is_standard": 0, "values": []}, ], }, # Diese Abschnitte kann der Uebersetzer nicht abbilden. "users": [{"username": "anna", "password_hash": "$2b$12$abcdefghijklmnopqrstuv"}], "known_hosts": "192.168.80.2 ssh-ed25519 AAAA...\n", } def test_legacy_envelope_needs_the_right_passphrase(): from tesm.services.legacy_transfer import LegacyTransferError, open_legacy datei = _seal_legacy({"credentials": []}, LEGACY_PASS) assert open_legacy(datei, LEGACY_PASS) == {"credentials": []} with pytest.raises(LegacyTransferError, match="Passphrase"): open_legacy(datei, "falsch") def test_legacy_envelope_of_another_app_is_refused(): from tesm.services.legacy_transfer import LegacyTransferError, open_legacy fremd = json.dumps({"app": "etwas-anderes", "salt": "AAAA", "payload": "x"}) with pytest.raises(LegacyTransferError, match="nicht zu TESM"): open_legacy(fremd, LEGACY_PASS) def test_report_names_what_it_cannot_translate(): """Nicht uebersetzbare Abschnitte muessen benannt werden, nicht verschwinden.""" from tesm.services.legacy_transfer import convert, open_legacy _, bericht = convert(open_legacy(_seal_legacy(LEGACY_PAYLOAD, LEGACY_PASS), LEGACY_PASS)) text = bericht.as_text() assert "bcrypt" in text, "der Hinweis zu lokalen Konten fehlt" assert "Host-Schluessel" in text assert "ohne Wert" in text, "die wertlose Option wurde nicht gemeldet" assert bericht.counts["credentials"] == 3 assert bericht.counts["devices"] == 2 assert bericht.counts["dhcp-Optionswerte"] == 2 def test_missing_credentials_are_reported(tesm_app): """Ein zugeordneter, aber nicht exportierter Zugang bleibt beim Import leer.""" from tesm.services.legacy_transfer import convert, open_legacy ohne = {key: value for key, value in LEGACY_PAYLOAD.items() if key != "credentials"} _, bericht = convert(open_legacy(_seal_legacy(ohne, LEGACY_PASS), LEGACY_PASS)) hinweise = " ".join(bericht.notes) assert "Switch-Admin" in hinweise and "Linux-Root" in hinweise def test_full_round_trip_into_the_database(tesm_app): """Alte Datei -> Uebersetzer -> neues Format -> vorhandener Import.""" from tesm.services import transfer from tesm.services.legacy_transfer import convert, open_legacy from tesm_core.extension import core ext = core(tesm_app) nutzlast, _ = convert(open_legacy(_seal_legacy(LEGACY_PAYLOAD, LEGACY_PASS), LEGACY_PASS)) # Ueber Siegel und Entsiegeln, damit auch der neue Umschlag geprueft ist. datei = transfer.seal(nutzlast, NEW_PASS) entsiegelt, _kopf = transfer.unseal(datei, NEW_PASS) with ext.database.session() as conn, ext.database.transaction(conn): transfer.apply_import( conn, ext.keystore, entsiegelt, categories=("credentials", "switches", "devices", "dhcp", "settings"), set_setting=lambda key, value: ext.settings.set(conn, key, value, actor="test"), ) with ext.database.session() as conn: # -- Zugangsdaten: Passwort muss wieder lesbar sein ------------------ from tesm.services import inventory zugang = {c.name: c for c in inventory.list_credentials(conn)} assert set(zugang) == {"Switch-Admin", "Linux-Root", "Windows-Dienst"} assert zugang["Linux-Root"].category == "linux" benutzer, passwort = inventory.credential_secret( conn, ext.keystore, zugang["Linux-Root"].id ) assert (benutzer, passwort) == ("root", "geheim-2") # -- Switch und Verknuepfung ---------------------------------------- switch = conn.execute("SELECT * FROM switches WHERE hostname='sw-keller'").fetchone() assert switch is not None assert switch["ip"] == "192.168.80.2" assert switch["credential_id"] == zugang["Switch-Admin"].id # -- Clients: Switch, Port, Zugang, abgeleitete Kategorie ------------ geraet = conn.execute( "SELECT * FROM devices WHERE mac='4c:52:62:25:b9:e4'" ).fetchone() assert geraet["name"] == "Autodarts" assert geraet["switch_id"] == switch["id"] assert geraet["port"] == "12" assert geraet["credential_id"] == zugang["Linux-Root"].id assert geraet["category"] == "linux", "Kategorie aus den Zugangsdaten abgeleitet" windows = conn.execute("SELECT * FROM devices WHERE mac='aa:bb:cc:00:00:02'").fetchone() assert windows["category"] == "windows" # -- DHCP: Subnetz, Reservierung, Optionswerte ---------------------- subnetz = conn.execute("SELECT * FROM dhcp_subnets").fetchone() assert (subnetz["range_start"], subnetz["range_end"]) == ( "192.168.80.100", "192.168.80.200", ) reservierung = conn.execute("SELECT * FROM dhcp_reservations").fetchone() assert reservierung["hostname"] == "drucker", "alt 'name' -> neu 'hostname'" option = conn.execute("SELECT * FROM dhcp_options WHERE code=225").fetchone() assert option is not None and option["name"] == "url" werte = { row["device_id"]: row["value"] for row in conn.execute( "SELECT device_id, value FROM dhcp_option_values WHERE option_id=?", (option["id"],), ) } # Ein globaler Wert und einer, der an genau diesem Geraet haengt. assert werte[None] == "https://intranet.firma.local" assert werte[geraet["id"]] == "https://play.autodarts.com" # -- Einstellungen: nur die DHCP-Schluessel ------------------------- assert ext.settings.get(conn, "dhcp_domain") == "firma.local" def test_option_value_of_an_unknown_device_is_not_made_global(tesm_app): """Ein Wert fuer ein unbekanntes Geraet darf nicht an alle gehen. Sonst wuerde aus einer geraetespezifischen Terminal-URL beim Import eine, die jeder Client bekommt -- die stillste Art, eine Konfiguration zu verfaelschen. """ from tesm.services import transfer from tesm_core.extension import core ext = core(tesm_app) nutzlast = { "dhcp": { "options": [ { "code": 225, "name": "url", "type": "string", "description": "", "is_standard": 0, "value": "https://nur-fuer-eines", "device_mac": "ff:ff:ff:ff:ff:ff", } ] } } with ext.database.session() as conn, ext.database.transaction(conn): transfer.apply_import( conn, ext.keystore, nutzlast, categories=("dhcp",), set_setting=lambda key, value: None, ) with ext.database.session() as conn: assert conn.execute("SELECT COUNT(*) FROM dhcp_option_values").fetchone()[0] == 0