"""Rauchtest des Grundgeruests: Start, Anmeldung, Rechte, CSRF, Header.""" from __future__ import annotations import re def test_app_starts_and_health_ok(client): response = client.get("/gesundheit") assert response.status_code == 200 assert response.get_json()["status"] == "ok" def test_login_page_renders_and_sets_csrf(client): response = client.get("/login") assert response.status_code == 200 body = response.get_data(as_text=True) assert "Anmelden" in body assert 'name="csrf_token"' in body def test_security_headers_present(client): response = client.get("/login") csp = response.headers["Content-Security-Policy"] assert "default-src 'self'" in csp assert "'unsafe-inline'" not in csp.split("script-src")[1].split(";")[0] assert re.search(r"nonce-[A-Za-z0-9_-]{10,}", csp) assert response.headers["X-Frame-Options"] == "DENY" assert response.headers["X-Content-Type-Options"] == "nosniff" assert "camera=()" in response.headers["Permissions-Policy"] def test_login_and_logout(client, make_user, login): make_user("anna", "Ein-gutes-Passwort-1") response = login("anna", "Ein-gutes-Passwort-1") assert response.status_code == 200 assert "Willkommen" in response.get_data(as_text=True) page = client.get("/konto/") assert page.status_code == 200 assert "anna" in page.get_data(as_text=True) def test_wrong_password_is_generic(client, make_user, login): make_user("anna", "Ein-gutes-Passwort-1") response = login("anna", "falsch") body = response.get_data(as_text=True) assert "Benutzername oder Passwort ist falsch" in body # Kein Hinweis darauf, dass das Konto existiert. assert "gesperrt" not in body def test_unknown_user_same_message(client, login): body = login("gibtsnicht", "irgendwas").get_data(as_text=True) assert "Benutzername oder Passwort ist falsch" in body def test_locked_account_rejected(client, make_user, login): make_user("gesperrt", "Ein-gutes-Passwort-1", is_locked=True) body = login("gesperrt", "Ein-gutes-Passwort-1").get_data(as_text=True) assert "gesperrt" in body def test_rate_limit_blocks_after_many_attempts(client, make_user, login): make_user("anna", "Ein-gutes-Passwort-1") messages = [login("anna", "falsch").get_data(as_text=True) for _ in range(9)] assert any("voruebergehend gesperrt" in body for body in messages) def test_protected_page_redirects_to_login(client): response = client.get("/widgets") assert response.status_code == 302 assert "/login" in response.headers["Location"] def test_permission_required_blocks_without_grant(client, make_user, login): make_user("anna", "Ein-gutes-Passwort-1") login("anna", "Ein-gutes-Passwort-1") assert client.get("/verwaltung/benutzer").status_code == 403 def test_permission_granted_allows_access(client, make_user, login): make_user("bob", "Ein-gutes-Passwort-1", permissions=("users.view",)) login("bob", "Ein-gutes-Passwort-1") response = client.get("/verwaltung/benutzer") assert response.status_code == 200 assert "Benutzer" in response.get_data(as_text=True) def test_admin_sees_everything(client, make_user, login): make_user("root", "Ein-gutes-Passwort-1", is_admin=True) login("root", "Ein-gutes-Passwort-1") for path in ("/verwaltung/benutzer", "/verwaltung/gruppen", "/papierkorb/", "/diagnose/"): assert client.get(path).status_code == 200, path def test_csrf_missing_token_is_rejected(client, make_user, login): make_user("root", "Ein-gutes-Passwort-1", is_admin=True) login("root", "Ein-gutes-Passwort-1") response = client.post("/verwaltung/benutzer/neu", data={"username": "boese"}) assert response.status_code == 403 assert "Sicherheitstoken" in response.get_data(as_text=True) def test_csrf_wrong_origin_is_rejected(client, make_user, login, csrf_token): make_user("root", "Ein-gutes-Passwort-1", is_admin=True) login("root", "Ein-gutes-Passwort-1") token = csrf_token("/verwaltung/benutzer") response = client.post( "/verwaltung/benutzer/neu", data={"username": "boese", "csrf_token": token}, headers={"Origin": "https://angreifer.example"}, ) assert response.status_code == 403 def test_nav_hides_pages_without_permission(client, make_user, login): make_user("anna", "Ein-gutes-Passwort-1") login("anna", "Ein-gutes-Passwort-1") body = client.get("/").get_data(as_text=True) assert "Diagnose" not in body assert "Widgets" not in body def test_nav_shows_pages_with_permission(client, make_user, login): make_user("bob", "Ein-gutes-Passwort-1", permissions=("widgets.view",)) login("bob", "Ein-gutes-Passwort-1") body = client.get("/").get_data(as_text=True) assert "Widgets" in body def test_default_group_created_from_preset(core_app): from tesm_core.extension import core from tesm_core.rbac import service as rbac extension = core(core_app) with extension.database.session() as conn: groups = rbac.list_groups(conn) names = {group.name for group in groups} assert "Benutzer" in names default = next(group for group in groups if group.name == "Benutzer") assert default.is_default is True assert "widgets.view" in default.permissions assert "demo.view" in default.permissions # Bereichsschalter wird mitgesetzt def test_error_page_shows_request_id(client): response = client.get("/gibt-es-nicht") assert response.status_code == 404 body = response.get_data(as_text=True) assert "404" in body assert "Vorgangsnummer" in body # --------------------------------------------------------------------------- # # Verzeichnisgruppen: suchen statt abtippen # --------------------------------------------------------------------------- # def test_group_search_is_open_to_fileshare_editors(): """Wer Freigaben zuordnet, muss die Gruppen finden koennen. Sonst braeuchte er zusaetzlich das Recht, den Verzeichnisdienst zu konfigurieren -- eine Rechteausweitung fuer eine reine Lesesuche. """ import inspect from tesm_core.views import hostsettings source = inspect.getsource(hostsettings.directory_group_search) assert "fileshare.edit" in source assert 'mode="any"' in source or "mode='any'" in source def test_the_picker_never_forces_typing_a_dn(): """Ein abgetippter DN ist ein Tippfehler, der erst bei der Anmeldung auffaellt.""" from pathlib import Path root = Path(__file__).resolve().parent.parent picker = ( root / "packages" / "tesm-core" / "src" / "tesm_core" / "templates" / "tesm_core" / "_group_picker.html" ).read_text(encoding="utf-8") assert "directory_group_search" in picker assert "data-group-query" in picker # Tippen bleibt trotzdem moeglich: ohne Dienstkonto gibt es keine Suche. assert 'name="{{ dn_field }}"' in picker for template in ( root / "packages" / "tesm-core" / "src" / "tesm_core" / "templates" / "tesm_core" / "settings_directory.html", root / "apps" / "tesm" / "src" / "tesm" / "templates" / "tesm" / "fileshare_mappings.html", ): text = template.read_text(encoding="utf-8") assert "group_picker(" in text, template.name def test_transport_choice_carries_its_port(): """Umgestellter Transport mit altem Port ergibt einen Fehler ohne Aussage.""" from pathlib import Path root = Path(__file__).resolve().parent.parent page = ( root / "packages" / "tesm-core" / "src" / "tesm_core" / "templates" / "tesm_core" / "settings_directory.html" ).read_text(encoding="utf-8") assert 'data-behavior="transport-port"' in page assert 'data-port-target="#ldap-port"' in page script = ( root / "packages" / "tesm-core" / "src" / "tesm_core" / "static" / "tesm_core" / "js" / "app.js" ).read_text(encoding="utf-8") assert "transport-port" in script assert 'ldaps: "636"' in script assert 'starttls: "389"' in script # Ein absichtlich abweichender Port darf nicht ueberschrieben werden. assert "known.includes(port.value)" in script def test_permission_matrix_has_no_nameless_checkbox(client, make_user, login): """In der Rechtematrix darf kein Kaestchen ohne ``name`` stehen. Fuer nicht vorgesehene Aktionen stand dort ein deaktiviertes Kaestchen. Das JS hat beim Einschalten des Bereichs *alle* Kaestchen der Zeile freigegeben -- auch dieses. Es liess sich anhaken, uebertrug wegen des fehlenden ``name`` aber nichts: es sah erteilt aus und war nach dem Speichern verschwunden. Ohne Eingabefeld kann das nicht wieder passieren. """ import re make_user("root", "Ein-gutes-Passwort-1", is_admin=True) login("root", "Ein-gutes-Passwort-1") body = client.get("/verwaltung/gruppen").get_data(as_text=True) if "permission-matrix" not in body: gruppe = re.search(r'href="(/verwaltung/gruppen/\d+)"', body) assert gruppe, "keine Gruppe zum Pruefen gefunden" body = client.get(gruppe.group(1)).get_data(as_text=True) assert "permission-matrix" in body, "Rechtematrix nicht gefunden" kaestchen = re.findall(r"]*type=\"checkbox\"[^>]*>", body) assert kaestchen, "keine Kaestchen in der Matrix" ohne_namen = [k for k in kaestchen if "name=" not in k] assert not ohne_namen, f"Kaestchen ohne name: {ohne_namen[:3]}" # Der Platzhalter ist jetzt reine Darstellung. assert "matrix__na" in body