Quelltext des aktuellen Standes
<?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'))
);
});
};