"""Gemeinsame Testfixtures. Jeder Test bekommt eine frische Instanz in einem temporaeren Verzeichnis -- eigene Datenbank, eigene Schluessel, eigenes Protokollverzeichnis. Nichts beruehrt eine echte Installation. """ from __future__ import annotations import os from pathlib import Path from typing import Any, Iterator import pytest @pytest.fixture() def instance_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: base = tmp_path / "instance" monkeypatch.setenv("TESM_BASE_DIR", str(base)) monkeypatch.setenv("TESM_LICENSE_BASE_DIR", str(tmp_path / "instance-license")) # Tests laufen ueber http:// -- ein Secure-Cookie wuerde nie gesetzt. monkeypatch.setenv("TESM_COOKIE_SECURE", "0") monkeypatch.setenv("TESM_LICENSE_COOKIE_SECURE", "0") monkeypatch.setenv("TESM_DEBUG", "0") monkeypatch.setenv("TESM_LICENSE_DEBUG", "0") return base @pytest.fixture() def core_app(instance_env: Path) -> Iterator[Any]: """Minimale App auf Basis von ``tesm_core`` -- ohne Fachlogik der beiden Apps.""" from flask import render_template_string from tesm_core import load_core_config from tesm_core.app_factory import create_base_app from tesm_core.auth.models import login_required from tesm_core.nav import NavItem, NavTree from tesm_core.rbac.common import administration_area, observability_area from tesm_core.rbac.model import Action, Area, PermissionTree, Resource, RolePreset demo = Area( key="demo", label="Demo", icon="grid", resources=( Resource( key="widgets", label="Widgets", actions=(Action.VIEW, Action.CREATE, Action.EDIT, Action.DELETE, Action.SECRETS), ), ), ) tree = PermissionTree([demo, administration_area(), observability_area()]) nav = NavTree( [ NavItem(key="index", label="Start", icon="grid", endpoint="index"), NavItem( key="widgets", label="Widgets", icon="cpu", endpoint="widgets", permissions=("widgets.view",), ), NavItem( key="administration", label="Verwaltung", icon="sliders", children=( NavItem( key="users", label="Benutzer", icon="users", endpoint="admin.users", permissions=("users.view",), ), NavItem( key="groups", label="Gruppen", icon="shield-users", endpoint="admin.groups", permissions=("groups.view",), ), NavItem( key="trash", label="Papierkorb", icon="trash", endpoint="trash.overview", permissions=("trash.view",), ), NavItem( key="diagnostics", label="Diagnose", icon="info", endpoint="diagnostics.overview", permissions=("diagnostics.view",), ), ), ), ] ) presets = ( RolePreset( key="viewer", name="Benutzer", description="Nur lesen", grants=(("widgets", (Action.VIEW,)),), is_default=True, ), ) config = load_core_config( app_key="tesm", app_name="TESM Test", env_prefix="TESM_", default_base=instance_env ) app = create_base_app( config=config, permissions=tree, nav=nav, role_presets=presets, app_short="TT", app_edition="Test", enable_license=False, ) @app.get("/") def index(): # type: ignore[no-untyped-def] return render_template_string( '{% extends "tesm_core/base.html" %}{% block page_title %}Start{% endblock %}' "{% block content %}
Start
{% endblock %}" ) @app.get("/widgets") @login_required def widgets(): # type: ignore[no-untyped-def] return render_template_string( '{% extends "tesm_core/base.html" %}{% block page_title %}Widgets{% endblock %}' "{% block content %}Widgets
{% endblock %}" ) app.config.update(TESTING=True) yield app @pytest.fixture() def client(core_app: Any) -> Any: return core_app.test_client() @pytest.fixture() def make_user(core_app: Any): """Erzeugt ein Konto direkt in der Datenbank.""" def factory( username: str = "tester", password: str = "Sicher-Passwort-2026", *, is_admin: bool = False, permissions: tuple[str, ...] = (), is_locked: bool = False, ) -> dict[str, Any]: from tesm_core.db import Database from tesm_core.extension import core from tesm_core.security import passwords extension = core(core_app) with extension.database.session() as conn, extension.database.transaction(conn): cursor = conn.execute( "INSERT INTO users (username, password_hash, is_admin, is_locked, auth_source, " "created_at, updated_at) VALUES (?,?,?,?,'local',datetime('now'),datetime('now'))", (username, passwords.hash_password(password), int(is_admin), int(is_locked)), ) user_id = int(cursor.lastrowid or 0) if permissions: group = conn.execute( "INSERT INTO groups (name, description, created_at, updated_at) " "VALUES (?,'Testgruppe',datetime('now'),datetime('now'))", (f"grp-{username}",), ) group_id = int(group.lastrowid or 0) conn.executemany( "INSERT INTO group_permissions (group_id, permission) VALUES (?,?)", [(group_id, p) for p in extension.permissions.sanitize(permissions)], ) conn.execute( "INSERT INTO user_groups (user_id, group_id) VALUES (?,?)", (user_id, group_id) ) row = Database.one(conn, "SELECT * FROM users WHERE id=?", (user_id,)) assert row is not None return {**row, "password": password} return factory @pytest.fixture() def login(client: Any): def do_login(username: str, password: str) -> Any: return client.post( "/login", data={"username": username, "password": password, "csrf_token": _token(client)}, follow_redirects=True, ) return do_login def _token(client: Any) -> str: """Holt ein CSRF-Token aus der Anmeldeseite.""" page = client.get("/login") body = page.get_data(as_text=True) marker = 'name="csrf_token" value="' start = body.index(marker) + len(marker) return body[start : body.index('"', start)] @pytest.fixture() def csrf_token(client: Any): def get(path: str = "/login") -> str: page = client.get(path) body = page.get_data(as_text=True) marker = 'name="csrf_token" value="' if marker not in body: marker = 'data-csrf="' start = body.index(marker) + len(marker) return body[start : body.index('"', start)] return get