OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

WorkflowSubjectScopeTest.php

9 KB
<?php

declare(strict_types=1);

use OpenNIT\Core\Database;
use OpenNIT\Workflows\WorkflowManager;

/**
 * Deckt ab, für wen ein Antrag gestellt werden darf.
 *
 * Ohne Einschränkung kann jede antragsberechtigte Person jede beliebige Person
 * aus dem Verzeichnis als betroffen eintragen – auch für einen Austritt. Die
 * Vorlage lässt sich deshalb auf die eigenen Unterstellten begrenzen.
 *
 * Entscheidend ist, dass die Prüfung beim Einreichen stattfindet: Eine
 * gefilterte Auswahlliste ist nur Bequemlichkeit, denn die Kennung der
 * betroffenen Person kommt aus dem abgesendeten Formular.
 */
return static function (): void {

    /**
     * Datenbank-Attrappe mit einem kleinen Verzeichnis.
     *
     * @param array<string,string>              $benutzer  Konto-Kennung => E-Mail
     * @param list<array<string,mixed>>         $personen  Verzeichnis-Personen
     */
    $db = static function (array $benutzer, array $personen): Database {
        return new class ($benutzer, $personen) extends Database {
            public function __construct(private array $benutzer, private array $personen) {}

            public function fetchOne(string $sql, array $params = []): ?array
            {
                if (str_contains($sql, 'FROM users')) {
                    $email = $this->benutzer[(string)$params[0]] ?? null;
                    return $email === null ? null : ['email' => $email];
                }
                // Person über E-Mail
                foreach ($this->personen as $p) {
                    if (($p['email'] ?? null) === ($params[0] ?? null) && (int)($p['is_active'] ?? 1) === 1) {
                        return $p;
                    }
                }
                return null;
            }

            public function fetchAll(string $sql, array $params = []): array
            {
                // Unterstellte: manager_email = ? ODER manager_ldap_dn = ?
                [$email, $dn] = [$params[0] ?? '', $params[1] ?? ''];
                $treffer = [];
                foreach ($this->personen as $p) {
                    if ((int)($p['is_active'] ?? 1) !== 1) {
                        continue;
                    }
                    $perMail = ($p['manager_email'] ?? '') !== '' && $p['manager_email'] === $email;
                    $perDn   = ($p['manager_ldap_dn'] ?? '') !== '' && $dn !== '' && $p['manager_ldap_dn'] === $dn;
                    if ($perMail || $perDn) {
                        $treffer[] = ['id' => $p['id']];
                    }
                }
                return $treffer;
            }
        };
    };

    // Chefin, zwei Unterstellte, eine fremde Person – einmal über Entra
    // (manager_email), einmal über LDAP (manager_ldap_dn).
    $verzeichnis = [
        ['id' => 1, 'email' => 'chefin@example', 'external_id' => 'CN=Chefin,OU=X', 'is_active' => 1],
        ['id' => 2, 'email' => 'anna@example',   'manager_email' => 'chefin@example', 'is_active' => 1],
        ['id' => 3, 'email' => 'bernd@example',  'manager_ldap_dn' => 'CN=Chefin,OU=X', 'is_active' => 1],
        ['id' => 4, 'email' => 'fremd@example',  'manager_email' => 'andere@example', 'is_active' => 1],
        ['id' => 5, 'email' => 'ex@example',     'manager_email' => 'chefin@example', 'is_active' => 0],
    ];
    $konten = ['10' => 'chefin@example', '11' => 'fremd@example', '12' => 'ohne@verzeichnis'];

    // WorkflowManager erwartet weitere Abhaengigkeiten – die Aufloesung braucht
    // nur die Datenbank, daher ohne Konstruktor erzeugen und sie nachreichen.
    $auflösung = static function (int $userId) use ($db, $konten, $verzeichnis): array {
        $m = (new ReflectionClass(WorkflowManager::class))->newInstanceWithoutConstructor();
        $prop = new ReflectionProperty(WorkflowManager::class, 'db');
        $prop->setAccessible(true);
        $prop->setValue($m, $db($konten, $verzeichnis));
        return $m->selectablePersonIds($userId);
    };

    // ----------------------------------------------------------------
    // Auflösung der Unterstellten
    // ----------------------------------------------------------------

    test('Die eigene Person ist immer wählbar', function () use ($auflösung) {
        // Ein Antrag für sich selbst darf nie an der Einschränkung scheitern.
        assertTrue(in_array(1, $auflösung(10), true));
    });

    test('Beide Verzeichnis-Wege liefern die Unterstellten', function () use ($auflösung) {
        // Entra pflegt die E-Mail der Führungskraft, LDAP deren DN.
        $ids = $auflösung(10);
        assertTrue(in_array(2, $ids, true), 'Unterstellte über die E-Mail fehlt');
        assertTrue(in_array(3, $ids, true), 'Unterstellter über den DN fehlt');
    });

    test('Fremde Personen sind nicht wählbar', function () use ($auflösung) {
        assertFalse(in_array(4, $auflösung(10), true), 'Eine fremde Person ist wählbar');
    });

    test('Ausgetretene Personen sind nicht wählbar', function () use ($auflösung) {
        assertFalse(in_array(5, $auflösung(10), true), 'Eine inaktive Person ist wählbar');
    });

    test('Wer niemanden führt, darf nur für sich selbst stellen', function () use ($auflösung) {
        assertSame([4], $auflösung(11));
    });

    test('Wer nicht im Verzeichnis steht, bekommt keine Auswahl', function () use ($auflösung) {
        // Ein reines Verwaltungskonto ohne Verzeichnis-Eintrag; die Ausnahme für
        // die Vorlagen-Verwaltung greift eine Ebene höher im Controller.
        assertSame([], $auflösung(12));
    });

    // ----------------------------------------------------------------
    // Wo die Prüfung stattfindet
    // ----------------------------------------------------------------

    test('Beide Antragswege prüfen beim Einreichen, nicht nur die Liste', function () {
        // Die Kennung der betroffenen Person kommt aus dem Formular; eine nur
        // gefilterte Auswahlliste liesse sich mit einem eigenen Absenden umgehen.
        $wurzel = dirname(__DIR__) . '/src/Controllers/';
        foreach (['WorkflowController.php', 'PortalController.php'] as $datei) {
            $quelle = (string)file_get_contents($wurzel . $datei);

            $stelle = strpos($quelle, "_subject_person_id");
            assertTrue($stelle !== false, "Kein Personenbezug in {$datei}");

            $block = substr($quelle, $stelle, 900);
            assertTrue(str_contains($block, 'erlaubteBetroffene('), "Keine Prüfung beim Einreichen in {$datei}");
            assertTrue(str_contains($block, 'in_array($pid, $erlaubt, true)'), "Die Kennung wird in {$datei} nicht geprüft");
        }
    });

    test('Auch die Personensuche folgt der Einschränkung', function () {
        // Das Formular im Backend füllt die betroffene Person nicht aus einer
        // Liste, sondern über die Personensuche. Ohne Einschränkung dort liesse
        // sich jemand auswählen, den das Einreichen anschliessend abweist.
        $suche = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/DirectoryAdminController.php');
        assertTrue(str_contains($suche, "\$request->get('workflow', 0)"), 'Die Suche kennt den Vorlagen-Bezug nicht');
        assertTrue(str_contains($suche, 'erlaubteBetroffeneFuerVorlage('), 'Die Suche wendet die Einschränkung nicht an');

        $form = (string)file_get_contents(dirname(__DIR__) . '/templates/workflows/start.php');
        assertTrue(
            str_contains($form, '/api/persons/search?workflow='),
            'Das Formular gibt der Suche die Vorlage nicht mit'
        );
    });

    test('Ohne Einschränkung bleibt alles wie bisher', function () {
        // Bestehende Vorlagen sollen sich durch das Update nicht ändern.
        $sql = (string)file_get_contents(dirname(__DIR__) . '/database/migrations/218_workflow_subject_scope.sql');
        assertTrue(str_contains($sql, "NOT NULL DEFAULT 'all'"), 'Der Standard ist nicht der bisherige Stand');

        foreach (['WorkflowController.php', 'PortalController.php'] as $datei) {
            $quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/' . $datei);
            assertTrue(
                str_contains($quelle, "(\$wf['subject_scope'] ?? 'all') !== 'reports'"),
                "In {$datei} fehlt der Ausweichwert für Instanzen ohne die Spalte"
            );
        }
    });

    test('Die Einschränkung lässt sich im Backend setzen', function () {
        $seite = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/workflows/edit.php');
        assertTrue(str_contains($seite, 'name="subject_scope"'), 'Kein Schalter in der Vorlagen-Verwaltung');

        $ctrl = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/WorkflowAdminController.php');
        assertTrue(str_contains($ctrl, "'subject_scope'        => \$subjectScope"), 'Der Schalter wird nicht gespeichert');
        // Ohne Personenbezug ergibt die Einschränkung keinen Sinn und würde nur verwirren.
        assertTrue(str_contains($ctrl, "\$subjectMode === 'person' && \$request->post('subject_scope') === 'reports'"), 'Die Einschränkung ist nicht an den Personenbezug gebunden');
    });
};