errorCode() === $code, "Fehlercode {$code}, erhalten {$e->errorCode()}"); } }; $pdo = get_db(); $now = db_now(); $dept1 = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $pdo->prepare('INSERT INTO departments (name, email, is_default, created_at) VALUES (?,?,0,?)')->execute(['Zweite Abteilung', '', $now]); $dept2 = (int) $pdo->lastInsertId(); $prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn(); // ── E-Mail-Adressen ───────────────────────────────────────────────────────── $id = email_address_save($pdo, 0, ['email' => 'Support@Example.Test', 'label' => 'Support', 'department_id' => $dept1, 'priority_id' => $prio, 'is_default' => true]); $row = $pdo->query("SELECT * FROM email_addresses WHERE id = {$id}")->fetch(); $check($row['email'] === 'support@example.test', 'E-Mail wird kleingeschrieben gespeichert'); $check((int) $row['is_active'] === 1, 'neue Adresse ist aktiv'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'kaputt', 'department_id' => $dept1]), 'INVALID_EMAIL'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'support@example.test', 'department_id' => $dept1]), 'DUPLICATE_EMAIL'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'x@example.test', 'department_id' => 9999]), 'INVALID_DEPARTMENT'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'imap@example.test', 'department_id' => $dept1, 'imap_enabled' => true, 'imap_host' => '']), 'IMAP_HOST_REQUIRED'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'imap@example.test', 'department_id' => $dept1, 'imap_enabled' => true, 'imap_host' => 'imap.example.test']), 'IMAP_PASSWORD_REQUIRED'); $throws(fn() => email_address_save($pdo, 0, ['email' => 'smtp@example.test', 'department_id' => $dept1, 'smtp_enabled' => true, 'smtp_host' => '']), 'SMTP_HOST_REQUIRED'); // IMAP anlegen, Passwort bleibt bei leerem Feld erhalten, Deaktivieren pausiert den Abruf email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept1, 'imap_enabled' => true, 'imap_host' => 'imap.example.test', 'imap_password' => 'geheim']); $imap = email_address_imap($pdo, $id); $check($imap !== null && (int) $imap['is_active'] === 1 && (int) $imap['department_id'] === $dept1, 'IMAP-Postfach angelegt und verknüpft'); $check(decrypt_secret($imap['password_enc']) === 'geheim', 'IMAP-Passwort verschlüsselt gespeichert'); email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept2, 'imap_enabled' => true, 'imap_host' => 'imap2.example.test', 'imap_password' => '']); $imap = email_address_imap($pdo, $id); $check(decrypt_secret($imap['password_enc']) === 'geheim', 'leeres Passwortfeld lässt das Passwort unverändert'); $check((int) $imap['department_id'] === $dept2 && $imap['host'] === 'imap2.example.test', 'Abteilung/Host werden am Postfach nachgezogen'); email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept2, 'is_active' => false, 'imap_enabled' => true, 'imap_host' => 'imap2.example.test']); $check((int) email_address_imap($pdo, $id)['is_active'] === 0, 'deaktivierte Adresse pausiert den IMAP-Abruf'); email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept2, 'imap_enabled' => true, 'imap_host' => 'imap2.example.test']); $check((int) $pdo->query("SELECT is_active FROM email_addresses WHERE id = {$id}")->fetchColumn() === 0, 'fehlendes is_active lässt den Status unverändert'); // SMTP pro Adresse: nur aktive Adresse mit aktiviertem SMTP wird verwendet; Daten bleiben beim Abschalten erhalten email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept2, 'is_active' => true, 'smtp_enabled' => true, 'smtp_host' => 'smtp.example.test', 'smtp_port' => 465, 'smtp_encryption' => 'ssl', 'smtp_username' => 'u', 'smtp_password' => 'pw']); $smtp = email_address_smtp_for($pdo, 'Support '); $check($smtp !== null && $smtp['smtp_host'] === 'smtp.example.test' && (int) $smtp['smtp_port'] === 465, 'eigener SMTP wird für die Absenderadresse gefunden'); $check($smtp !== null && decrypt_secret($smtp['smtp_password_enc']) === 'pw', 'SMTP-Passwort verschlüsselt gespeichert'); $check(email_address_smtp_for($pdo, 'andere@example.test') === null, 'fremde Absenderadresse nutzt keinen eigenen SMTP'); email_address_save($pdo, $id, ['email' => 'support@example.test', 'department_id' => $dept2, 'smtp_enabled' => false]); $check(email_address_smtp_for($pdo, 'support@example.test') === null, 'ausgeschalteter SMTP wird nicht verwendet'); $kept = $pdo->query("SELECT smtp_host, smtp_password_enc FROM email_addresses WHERE id = {$id}")->fetch(); $check($kept['smtp_host'] === 'smtp.example.test' && $kept['smtp_password_enc'] !== null, 'SMTP-Zugangsdaten bleiben beim Ausschalten erhalten'); // Absenderauswahl ignoriert deaktivierte Adressen $pdo->prepare('UPDATE email_addresses SET is_active = 0 WHERE id = ?')->execute([$id]); $check(resolve_from_address($dept2) === null, 'deaktivierte Adresse wird nicht als Absender gewählt'); $pdo->prepare('UPDATE email_addresses SET is_active = 1 WHERE id = ?')->execute([$id]); $check(resolve_from_address($dept2) !== null, 'aktive Adresse wird als Absender gewählt'); // Löschen entfernt das Postfach mit $deleted = email_address_delete($pdo, $id); $check($deleted === 'support@example.test', 'Löschen gibt die E-Mail zurück'); $check(email_address_imap($pdo, $id) === null, 'Postfach wird mit der Adresse gelöscht'); // ── Abteilungen: Status ───────────────────────────────────────────────────── $pdo->prepare('UPDATE departments SET is_active = 0 WHERE id = ?')->execute([$dept2]); $select = array_map('intval', array_column(departments_for_select($pdo), 'id')); $check(!in_array($dept2, $select, true), 'deaktivierte Abteilung fehlt in Auswahllisten'); $select = array_map('intval', array_column(departments_for_select($pdo, [$dept2]), 'id')); $check(in_array($dept2, $select, true), 'bereits gewählte deaktivierte Abteilung bleibt auswählbar'); $check(active_department_id($pdo, $dept2) === $dept1, 'deaktivierte Abteilung weicht auf eine aktive aus'); $check(active_department_id($pdo, $dept1) === $dept1, 'aktive Abteilung bleibt'); $pdo->prepare('UPDATE departments SET is_active = 1 WHERE id = ?')->execute([$dept2]); // ── Hilfethemen: Hierarchie, Typ, Abteilungsstatus ────────────────────────── $ins = $pdo->prepare('INSERT INTO help_topics (name, department_id, priority_id, is_active, is_public, parent_id, sort_order) VALUES (?,?,?,?,?,?,?)'); $ins->execute(['Eltern', $dept1, $prio, 1, 1, null, 1]); $parent = (int) $pdo->lastInsertId(); $ins->execute(['Kind', $dept1, $prio, 1, 1, $parent, 2]); $child = (int) $pdo->lastInsertId(); $ins->execute(['Intern', $dept1, $prio, 1, 0, null, 3]); $ins->execute(['In zweiter Abteilung', $dept2, $prio, 1, 1, null, 4]); $paths = array_column(help_topic_options($pdo, true), 'path'); $check(in_array('Eltern / Kind', $paths, true), 'Kind-Thema hat den Pfad "Eltern / Kind"'); $check(!in_array('Intern', $paths, true), 'interne Themen sind für Kunden nicht sichtbar'); $check(in_array('Intern', array_column(help_topic_options($pdo, false), 'path'), true), 'interne Themen sind für Mitarbeiter sichtbar'); $check(array_search('Eltern', $paths, true) < array_search('Eltern / Kind', $paths, true), 'Eltern stehen vor ihren Kindern'); $pdo->prepare('UPDATE departments SET is_active = 0 WHERE id = ?')->execute([$dept2]); $check(!in_array('In zweiter Abteilung', array_column(help_topic_options($pdo, true), 'path'), true), 'Themen deaktivierter Abteilungen sind im Kundenportal verborgen'); $pdo->prepare('UPDATE help_topics SET parent_id = ? WHERE id = ?')->execute([$child, $parent]); // Zyklus $check(count(help_topic_options($pdo, false)) >= 1, 'zyklische Hierarchie lässt die Auflistung nicht hängen'); $pdo->prepare('UPDATE help_topics SET parent_id = NULL WHERE id = ?')->execute([$parent]); $pdo->prepare('UPDATE departments SET is_active = 1 WHERE id = ?')->execute([$dept2]); // ── Rollen pro Abteilung ──────────────────────────────────────────────────── $pdo->prepare('INSERT INTO roles (name, permissions, is_active, created_at) VALUES (?,?,1,?)') ->execute(['Nur lesen', json_encode(['reply_ticket' => false, 'delete_ticket' => false, 'close_ticket' => false]), $now]); $readOnly = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO roles (name, permissions, is_active, created_at) VALUES (?,?,1,?)') ->execute(['Löscher', json_encode(['reply_ticket' => true, 'delete_ticket' => true]), $now]); $deleter = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, role_id, department_id, is_active, created_at) VALUES ('Agent','agent@example.test','x','agent',?,?,1,?)") ->execute([$readOnly, $dept1, $now]); $agentId = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO staff_department (staff_id, department_id, notify, role_id) VALUES (?,?,1,?)')->execute([$agentId, $dept2, $deleter]); $agent = $pdo->query("SELECT * FROM staff WHERE id = {$agentId}")->fetch(); $check(!has_permission($agent, 'reply_ticket'), 'Hauptrolle gilt ohne Abteilungsangabe'); $check(!has_permission($agent, 'reply_ticket', $dept1), 'Hauptrolle gilt in der Hauptabteilung'); $check(has_permission($agent, 'reply_ticket', $dept2) && has_permission($agent, 'delete_ticket', $dept2), 'eigene Rolle gilt in der erweiterten Abteilung'); $pdo->prepare('UPDATE staff_department SET role_id = NULL WHERE staff_id = ? AND department_id = ?')->execute([$agentId, $dept2]); // Cache der Funktion ist pro Prozess: neuen Mitarbeiter mit gleicher Konstellation prüfen $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, role_id, department_id, is_active, created_at) VALUES ('Agent2','agent2@example.test','x','agent',?,?,1,?)") ->execute([$readOnly, $dept1, $now]); $agent2Id = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO staff_department (staff_id, department_id, notify, role_id) VALUES (?,?,1,NULL)')->execute([$agent2Id, $dept2]); $agent2 = $pdo->query("SELECT * FROM staff WHERE id = {$agent2Id}")->fetch(); $check(!has_permission($agent2, 'reply_ticket', $dept2), 'ohne eigene Rolle gilt in der erweiterten Abteilung die Hauptrolle'); $pdo->prepare('UPDATE roles SET is_active = 0 WHERE id = ?')->execute([$readOnly]); $agent3 = $agent2; $agent3['id'] = 987654; // frischer Cache-Schlüssel, Rollen-Cache bleibt: neue Rolle prüfen $pdo->prepare("INSERT INTO roles (name, permissions, is_active, created_at) VALUES ('Aus', ?, 0, ?)")->execute([json_encode(['reply_ticket' => false]), $now]); $offId = (int) $pdo->lastInsertId(); $agent3['role_id'] = $offId; $agent3['department_id'] = $dept1; $check(has_permission($agent3, 'reply_ticket') === (bool) default_permissions('agent')['reply_ticket'], 'deaktivierte Rolle fällt auf Standardrechte zurück'); $admin = ['id' => 1, 'role' => 'admin', 'role_id' => null, 'department_id' => $dept1]; $check(has_permission($admin, 'delete_ticket', $dept2), 'Administrator darf immer'); // ── Team-Benachrichtigung ─────────────────────────────────────────────────── $pdo->prepare('INSERT INTO teams (name, is_active, no_alerts, created_at) VALUES (?,?,?,?)')->execute(['Team A', 1, 1, $now]); $teamQuiet = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO teams (name, is_active, no_alerts, created_at) VALUES (?,?,?,?)')->execute(['Team B', 0, 0, $now]); $teamOff = (int) $pdo->lastInsertId(); $ticket = ['ticket_number' => 'T-1', 'subject' => 'Test']; $check(notify_team_assignment($pdo, $ticket, $teamQuiet) === 0, 'Team mit deaktivierter Benachrichtigung sendet nichts'); $check(notify_team_assignment($pdo, $ticket, $teamOff) === 0, 'deaktiviertes Team sendet nichts'); $check(notify_team_assignment($pdo, $ticket, 99999) === 0, 'unbekanntes Team sendet nichts'); // ── Abteilung löschen: nie ein Absturz, immer eine verständliche Meldung ────────── $deptCode = function (callable $fn): string { try { $fn(); return 'OK'; } catch (AdminModelException $e) { return $e->errorCode() . '|' . $e->getMessage(); } catch (Throwable $e) { return 'ABSTURZ ' . get_class($e) . ': ' . $e->getMessage(); } }; $pdo->prepare('INSERT INTO departments (name, email, is_default, created_at) VALUES (?,?,0,?)')->execute(['Zum Löschen', '', $now]); $delDept = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO help_topics (name, department_id, priority_id, is_active, created_at) VALUES (?,?,?,1,?)')->execute(['Thema der Abteilung', $delDept, $prio, $now]); $topicOfDept = (int) $pdo->lastInsertId(); $res = $deptCode(fn() => department_delete($pdo, $delDept)); $check(str_starts_with($res, 'DEPARTMENT_IN_USE|') && str_contains($res, '1 Hilfethemen'), 'Abteilung mit Hilfethema: verständliche Meldung statt Absturz: ' . $res); $check((int) $pdo->query("SELECT COUNT(*) FROM departments WHERE id = {$delDept}")->fetchColumn() === 1, 'die Abteilung bleibt dabei erhalten'); $pdo->exec("DELETE FROM help_topics WHERE id = {$topicOfDept}"); $pdo->prepare('INSERT INTO email_addresses (email, label, department_id, is_active, created_at) VALUES (?,?,?,1,?)')->execute(['zum-loeschen@example.test', 'X', $delDept, $now]); $mailId = (int) $pdo->lastInsertId(); $res = $deptCode(fn() => department_delete($pdo, $delDept)); $check(str_starts_with($res, 'DEPARTMENT_IN_USE|') && str_contains($res, 'E-Mail-Adressen'), 'Abteilung mit E-Mail-Adresse wird nicht gelöscht (die Adresse ginge sonst verloren): ' . $res); $pdo->exec("DELETE FROM email_addresses WHERE id = {$mailId}"); $pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)')->execute(['DEL-1', 'x', 'k', 'k@example.test', $delDept, $prio, 'open', $now, $now]); $ticketOfDept = (int) $pdo->lastInsertId(); $res = $deptCode(fn() => department_delete($pdo, $delDept)); $check(str_starts_with($res, 'DEPARTMENT_IN_USE|') && str_contains($res, '1 Tickets'), 'Abteilung mit Tickets wird nicht gelöscht: ' . $res); $pdo->exec("DELETE FROM tickets WHERE id = {$ticketOfDept}"); $check(str_starts_with($deptCode(fn() => department_delete($pdo, $dept1)), 'DEPARTMENT_IS_DEFAULT|') || str_starts_with($deptCode(fn() => department_delete($pdo, $dept1)), 'DEPARTMENT_IN_USE|'), 'Standardabteilung kann nicht gelöscht werden'); $check(str_starts_with($deptCode(fn() => department_delete($pdo, 999999)), 'NOT_FOUND|'), 'unbekannte Abteilung liefert eine Meldung'); // zusätzliche Mitgliedschaft und Textbaustein: werden beim Löschen aufgeräumt $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Mitglied','mitglied@example.test','x','agent',?,1,?)")->execute([$dept1, $now]); $memberId = (int) $pdo->lastInsertId(); $pdo->prepare('INSERT INTO staff_department (staff_id, department_id) VALUES (?,?)')->execute([$memberId, $delDept]); $pdo->prepare('INSERT INTO canned_responses (title, body, department_id, created_at) VALUES (?,?,?,?)')->execute(['Baustein', 'Text', $delDept, $now]); $canId = (int) $pdo->lastInsertId(); $res = $deptCode(fn() => department_delete($pdo, $delDept)); $check($res === 'OK', 'leere Abteilung lässt sich löschen: ' . $res); $check((int) $pdo->query("SELECT COUNT(*) FROM departments WHERE id = {$delDept}")->fetchColumn() === 0, 'Abteilung ist weg'); $check((int) $pdo->query("SELECT COUNT(*) FROM staff_department WHERE department_id = {$delDept}")->fetchColumn() === 0, 'zusätzliche Mitgliedschaften wurden mit aufgeräumt'); $check($pdo->query("SELECT department_id FROM canned_responses WHERE id = {$canId}")->fetchColumn() === null, 'Textbausteine der Abteilung gelten danach für alle'); // Mitarbeiter mit dieser Hauptabteilung blockieren das Löschen $pdo->prepare('INSERT INTO departments (name, email, is_default, created_at) VALUES (?,?,0,?)')->execute(['Mit Mitarbeiter', '', $now]); $withStaff = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Haupt','haupt@example.test','x','agent',?,1,?)")->execute([$withStaff, $now]); $res = $deptCode(fn() => department_delete($pdo, $withStaff)); $check(str_starts_with($res, 'DEPARTMENT_IN_USE|') && str_contains($res, 'Hauptabteilung'), 'Mitarbeiter mit dieser Hauptabteilung blockieren das Löschen: ' . $res); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Admin models integration checks passed.\n";