OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

WorkflowDecisionTest.php

7 KB
<?php

declare(strict_types=1);

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

/**
 * Deckt die Schranken einer Genehmigungsentscheidung ab.
 *
 * Eine Entscheidung verändert den Lauf eines Antrags und ist nicht
 * zurücknehmbar. Sie darf deshalb nur fallen, wenn die Station tatsächlich an
 * der Reihe ist, keine Rückfrage offen steht und die entscheidende Person für
 * genau diese Station eingetragen ist – eine ausgedachte Stations-ID genügt
 * nicht.
 */
return static function (): void {

    /**
     * Datenbank-Attrappe mit einer Station, dem Antragsstatus und den
     * eingetragenen Genehmigern.
     *
     * @param array<string,mixed>|null $step     Zeile aus request_step_states
     * @param string                   $status   Status des Antrags
     * @param array<string,mixed>|null $approver Offene Genehmiger-Zeile
     */
    $db = static function (?array $step, string $status = 'running', ?array $approver = null): Database {
        return new class($step, $status, $approver) extends Database {
            public function __construct(
                private ?array $step,
                private string $status,
                private ?array $approver
            ) {
                parent::__construct([]);
            }

            public function transaction(callable $callback): mixed
            {
                return $callback($this);
            }

            public function fetchOne(string $sql, array $params = []): ?array
            {
                if (str_contains($sql, 'FROM request_step_states')) {
                    return $this->step;
                }
                if (str_contains($sql, 'FROM request_step_approvers')) {
                    return $this->approver;
                }
                return null;
            }

            public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed
            {
                if (str_contains($sql, 'FROM workflow_requests')) {
                    return $this->status;
                }
                return false;
            }

            public function fetchAll(string $sql, array $params = []): array
            {
                return [];
            }
        };
    };

    /** Erzeugt den Manager ohne Konstruktor. */
    $manager = static function (Database $datenbank): WorkflowManager {
        $klasse = new ReflectionClass(WorkflowManager::class);
        $mgr    = $klasse->newInstanceWithoutConstructor();
        $feld   = $klasse->getProperty('db');
        $feld->setAccessible(true);
        $feld->setValue($mgr, $datenbank);
        return $mgr;
    };

    /** Führt die Entscheidung aus und liefert die Fehlermeldung (oder ''). */
    $fehler = static function (callable $aktion): string {
        try {
            $aktion();
            return '';
        } catch (\Throwable $e) {
            return $e->getMessage();
        }
    };

    /** Eine aktive Station ohne Notizpflicht. */
    $aktiveStation = ['id' => 10, 'request_id' => 3, 'status' => 'active', 'require_note' => 0];

    // ----------------------------------------------------------------
    // Form der Entscheidung
    // ----------------------------------------------------------------

    test('Nur Zustimmung und Ablehnung sind zulässig', function () use ($db, $manager, $fehler, $aktiveStation) {
        $mgr = $manager($db($aktiveStation));

        foreach (['vielleicht', '', 'APPROVE', 'delete'] as $eingabe) {
            assertSame(
                'Ungültige Entscheidung',
                $fehler(fn() => $mgr->decideAsUser(10, 5, $eingabe)),
                'Angenommen: ' . $eingabe
            );
        }
    });

    test('Eine Ablehnung verlangt eine Begründung', function () use ($db, $manager, $fehler, $aktiveStation) {
        $mgr = $manager($db($aktiveStation));

        assertSame('Begründung erforderlich', $fehler(fn() => $mgr->decideAsUser(10, 5, 'reject')));
        assertSame('Begründung erforderlich', $fehler(fn() => $mgr->decideAsUser(10, 5, 'reject', '   ')));
    });

    // ----------------------------------------------------------------
    // Zustand der Station
    // ----------------------------------------------------------------

    test('Eine nicht vorhandene Station lässt sich nicht entscheiden', function () use ($db, $manager, $fehler) {
        // Die Stations-ID kommt aus der Adresse und ist ratbar.
        $mgr = $manager($db(null));

        assertTrue(str_contains($fehler(fn() => $mgr->decideAsUser(999, 5, 'approve')), 'nicht (mehr) aktiv'));
    });

    test('Eine bereits entschiedene Station bleibt entschieden', function () use ($db, $manager, $fehler) {
        foreach (['approved', 'rejected', 'skipped', 'pending'] as $status) {
            $mgr = $manager($db(['id' => 10, 'request_id' => 3, 'status' => $status, 'require_note' => 0]));
            assertTrue(
                str_contains($fehler(fn() => $mgr->decideAsUser(10, 5, 'approve')), 'nicht (mehr) aktiv'),
                'Erneut entschieden bei Status ' . $status
            );
        }
    });

    test('Eine offene Rückfrage hält die Entscheidung auf', function () use ($db, $manager, $fehler, $aktiveStation) {
        $mgr = $manager($db($aktiveStation, 'waiting_requester'));

        assertTrue(str_contains($fehler(fn() => $mgr->decideAsUser(10, 5, 'approve')), 'Rückfrage'));
    });

    test('Eine Station mit Notizpflicht verlangt die Notiz auch bei Zustimmung', function () use ($db, $manager, $fehler) {
        $mgr = $manager($db(['id' => 10, 'request_id' => 3, 'status' => 'active', 'require_note' => 1]));

        assertTrue(str_contains($fehler(fn() => $mgr->decideAsUser(10, 5, 'approve')), 'Notiz'));
    });

    // ----------------------------------------------------------------
    // Berechtigung
    // ----------------------------------------------------------------

    test('Ohne Eintrag als Genehmiger fällt keine Entscheidung', function () use ($db, $manager, $fehler, $aktiveStation) {
        // Kern der Schranke: Eine aktive Station allein reicht nicht – die
        // entscheidende Person muss für sie eingetragen und noch offen sein.
        $mgr = $manager($db($aktiveStation, 'running', null));

        assertSame('Keine Berechtigung für diese Station', $fehler(fn() => $mgr->decideAsUser(10, 5, 'approve')));
    });

    test('Die Prüfung der Berechtigung kommt vor der Verbuchung', function () use ($db, $manager, $fehler, $aktiveStation) {
        // Ohne Genehmiger-Zeile darf applyDecision gar nicht erst laufen; die
        // Attrappe kennt dessen Abfragen nicht und würde sonst anders scheitern.
        $mgr = $manager($db($aktiveStation, 'running', null));

        assertSame(
            'Keine Berechtigung für diese Station',
            $fehler(fn() => $mgr->decideAsUser(10, 5, 'reject', 'Begründung liegt vor'))
        );
    });
};