Files
deskhub/DeskHub-Webseite/tests/vacation_substitute_access.php
T
2026-10-30 21:50:21 +00:00

146 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft den Zugriff der Vertretung: Wer einen Abwesenden vertritt, sieht und bearbeitet dessen Abteilungen mit, solange die Abwesenheit
* dauert, auch ohne eigenen Zugang zur Abteilung. Ketten, abwesende Administratoren, Ende der Abwesenheit, inaktive Mitarbeiter.
*/
$db = sys_get_temp_dir() . '/deskhub-vs-' . bin2hex(random_bytes(4)) . '.sqlite';
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_SQLITE_PATH=' . $db);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; };
$pdo = get_db();
$now = db_now();
$dept = function (string $name) use ($pdo): int {
$pdo->prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute([$name, strtolower($name) . '@firma.example']);
return (int) $pdo->lastInsertId();
};
$person = function (string $name, string $role, ?int $dept) use ($pdo, $now): int {
$pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)')
->execute([$name, strtolower($name) . '@firma.example', 'x', $role, $dept, $now]);
return (int) $pdo->lastInsertId();
};
$staffRow = fn(int $id) => $pdo->query('SELECT * FROM staff WHERE id = ' . $id)->fetch();
$ids = function (int $id) use ($staffRow): ?array { $r = staff_department_ids($staffRow($id)); if ($r !== null) sort($r); return $r; };
$away = fn(int $id, ?int $sub) => $pdo->prepare('UPDATE staff SET is_on_vacation = ?, vacation_substitute_id = ? WHERE id = ?')->execute([$sub === null ? 0 : 1, $sub, $id]);
$dA = $dept('Support'); $dB = $dept('Technik'); $dC = $dept('Buchhaltung');
$anna = $person('Anna', 'agent', $dA); // Vertretung, nur Support
$ben = $person('Ben', 'agent', $dB); // im Urlaub, Technik
$cora = $person('Cora', 'agent', $dC); // Kette
$chef = $person('Chef', 'admin', null);
$pdo->prepare('INSERT INTO staff_department (staff_id, department_id) VALUES (?,?)')->execute([$ben, $dC]); // Ben hat zusätzlich Buchhaltung
// 1) Ohne Abwesenheit: nur die eigene Abteilung
$check($ids($anna) === [$dA], 'Ohne Abwesenheit hat Anna nur ihre Abteilung');
$check($ids($ben) === [$dB, $dC], 'Ben hat Technik und Buchhaltung');
// 2) Ben im Urlaub, Anna ist die Vertretung
$away($ben, $anna);
$check($ids($anna) === [$dA, $dB, $dC], 'Die Vertretung bekommt Haupt- und zusätzliche Abteilungen des Abwesenden');
$check($ids($ben) === [$dB, $dC], 'Der Abwesende selbst behält seinen Zugriff');
$check($ids($cora) === [$dC], 'Unbeteiligte bekommen nichts dazu');
[$sql, $params] = dept_filter_sql($ids($anna), 't.department_id');
$check(str_contains($sql, 'IN') && count($params) === 3, 'Die Ticketliste filtert mit den erweiterten Abteilungen');
$check(staff_can_access_department($staffRow($anna), $dB) === true, 'Die Vertretung darf Tickets der Abteilung öffnen');
// 3) Kette: Anna ist selbst im Urlaub, Cora vertritt Anna
$away($anna, $cora);
$check($ids($cora) === [$dA, $dB, $dC], 'Kette: die Vertretung der Vertretung bekommt beide Abteilungen');
$away($anna, null);
// 4) Ende der Abwesenheit
$away($ben, null);
$check($ids($anna) === [$dA], 'Nach dem Urlaub ist der zusätzliche Zugriff wieder weg');
// 5) Inaktiver Abwesender zählt nicht
$away($ben, $anna);
$pdo->exec('UPDATE staff SET is_active = 0 WHERE id = ' . $ben);
$check($ids($anna) === [$dA], 'Ein inaktiver Abwesender gibt keinen Zugriff');
$pdo->exec('UPDATE staff SET is_active = 1 WHERE id = ' . $ben);
$away($ben, null);
// 6) Abwesender Administrator: nur Abteilungen der zugewiesenen offenen Tickets
$away($chef, $anna);
$check($ids($anna) === [$dA], 'Abwesender Admin ohne Tickets: kein zusätzlicher Zugriff');
$pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)")
->execute(['T-1', 'Test', 'open', $dB, $anna, 'Kunde', 'kunde@x.example', $now, $now]);
$check($ids($anna) === [$dA, $dB], 'Abwesender Admin: die Abteilung des zugewiesenen Tickets kommt dazu');
$pdo->exec("UPDATE tickets SET status = 'closed'");
$check($ids($anna) === [$dA], 'Geschlossene Tickets geben keinen Zugriff mehr');
$away($chef, null);
// 7) Admin bleibt unbeschränkt, Selbstvertretung wird ignoriert
$check($ids($chef) === null, 'Administratoren sehen weiter alle Abteilungen');
$away($anna, $anna);
$check($ids($anna) === [$dA], 'Selbst als Vertretung eingetragen: kein Zugriffsgewinn');
$away($anna, null);
// 8) Weitergabe und Rückgabe der Tickets (Profil-Abwesenheit)
$newTicket = function (string $nr, int $deptId, ?int $assignee, string $status = 'open') use ($pdo, $now): int {
$pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)")
->execute([$nr, 'Test ' . $nr, $status, $deptId, $assignee, 'Kunde', 'kunde@x.example', $now, $now]);
return (int) $pdo->lastInsertId();
};
$owner = fn(int $t) => (int) $pdo->query('SELECT assigned_staff_id FROM tickets WHERE id = ' . $t)->fetchColumn();
$t1 = $newTicket('V-1', $dB, $ben);
$t2 = $newTicket('V-2', $dB, $ben);
$t3 = $newTicket('V-3', $dB, $ben, 'closed');
$t4 = $newTicket('V-4', $dB, $chef);
$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]);
[$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], false);
$check($h === 0 && $owner($t1) === $ben, 'Ohne Haken wird nichts weitergegeben');
[$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true);
$check($h === 2 && $owner($t1) === $anna && $owner($t2) === $anna, 'Offene Tickets gehen an die Vertretung');
$check($owner($t3) === $ben && $owner($t4) === $chef, 'Geschlossene und fremde Tickets bleiben');
$check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 2, 'Die Weitergabe wird gemerkt');
$check((int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE action LIKE 'An die Vertretung%'")->fetchColumn() === 2, 'Das Ticket-Protokoll nennt die Weitergabe');
[$h] = vacation_apply_change($pdo, $ben, [1, $anna], [1, $anna], true);
$check($h === 0, 'Erneutes Speichern gibt nichts doppelt weiter');
// Anna ändert eines der Tickets selbst auf erledigt, ein anderes hängt jemand um
$pdo->exec("UPDATE tickets SET status = 'resolved' WHERE id = " . $t1);
$pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$cora, $t2]);
// Ende des Urlaubs
$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]);
[$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true);
$check($b === 0 && $owner($t1) === $anna && $owner($t2) === $cora, 'Erledigte und umgehängte Tickets kommen nicht zurück');
$check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 0, 'Alle Weitergaben sind abgeschlossen');
// Zurückgeben, wenn alles offen bei der Vertretung liegt
$t5 = $newTicket('V-5', $dB, $ben);
$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]);
vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true);
$check($owner($t5) === $anna, 'Weitergabe (zweiter Urlaub)');
$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]);
[$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true);
$check($b === 1 && $owner($t5) === $ben, 'Offene Tickets kommen nach dem Urlaub zurück');
// Neue Vertretung: erst zurück, dann an die neue
$t6 = $newTicket('V-6', $dB, $ben);
$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]);
vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true);
$pdo->prepare('UPDATE staff SET vacation_substitute_id = ? WHERE id = ?')->execute([$cora, $ben]);
vacation_apply_change($pdo, $ben, [1, $anna], [1, $cora], true);
$check($owner($t6) === $cora, 'Bei neuer Vertretung geht das Ticket an die neue Person');
// Ohne Vertretung / Vertretung selbst im Urlaub ohne Ersatz: nichts
$pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]);
vacation_apply_change($pdo, $ben, [1, $cora], [0, null], true);
$check($owner($t6) === $ben, 'Zurück bei Ben');
$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]);
[$h] = vacation_apply_change($pdo, $ben, [0, null], [1, null], true);
$check($h === 0 && $owner($t6) === $ben, 'Ohne Vertretung bleibt alles beim Abwesenden');
// Das Plugin „Urlaub und Vertretung“ hat Vorrang
$pdo->exec('CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (id INTEGER PRIMARY KEY AUTOINCREMENT, staff_id INTEGER, substitute_id INTEGER, start_date TEXT, end_date TEXT, note TEXT, status TEXT, created_by INTEGER, created_at TEXT)');
$pdo->prepare("INSERT INTO plugin_vertretung_absences (staff_id, substitute_id, start_date, end_date, note, status, created_at) VALUES (?,?,?,?,?,?,?)")->execute([$ben, $anna, '2026-01-01', '2026-12-31', '', 'active', $now]);
$pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]);
[$h] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true);
$check($h === 0 && $owner($t6) === $ben, 'Ist eine Abwesenheit im Plugin aktiv, gibt der Server nichts selbst weiter');
@unlink($db);
if ($failures) {
fwrite(STDERR, "Vertretungs-Zugriff fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n");
exit(1);
}
echo "Vertretungs-Zugriff bestanden.\n";