145 lines
13 KiB
PHP
145 lines
13 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Prüft die gemeinsam genutzte Verwaltungslogik (includes/admin_models.php) für Abteilungen,
|
|
* Hilfethemen, Teams und SLA-Pläne sowie die Überwachung des Mail-Abrufs (includes/health.php).
|
|
* Läuft gegen eine frische In-Memory-SQLite-Datenbank.
|
|
*/
|
|
|
|
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
|
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
|
|
|
$failures = [];
|
|
$check = function (bool $ok, string $what) use (&$failures): void {
|
|
if (!$ok) $failures[] = $what;
|
|
};
|
|
$throwsAm = function (callable $fn, string $code) use ($check): void {
|
|
try { $fn(); $check(false, "erwartete Fehler {$code}"); }
|
|
catch (AdminModelException $e) { $check($e->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();
|
|
$prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn();
|
|
$pdo->prepare("INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES ('Agent','agent@example.test','x','agent',?,1,?)")->execute([$dept1, $now]);
|
|
$agentId = (int) $pdo->lastInsertId();
|
|
|
|
// ── Abteilungen ─────────────────────────────────────────────────────────────
|
|
$newDept = department_save($pdo, 0, ['name' => 'Vertrieb', 'email' => '', 'is_default' => false]);
|
|
$check($newDept > 0, 'Abteilung wird angelegt');
|
|
$check((int) $pdo->query("SELECT is_active FROM departments WHERE id = {$newDept}")->fetchColumn() === 1, 'neue Abteilung ist aktiv');
|
|
$throwsAm(fn() => department_save($pdo, 0, ['name' => '']), 'NAME_REQUIRED');
|
|
$throwsAm(fn() => department_save($pdo, $newDept, ['name' => 'Vertrieb', 'is_default' => true, 'is_active' => false]), 'DEFAULT_INACTIVE');
|
|
$throwsAm(fn() => department_save($pdo, $newDept, ['name' => 'Vertrieb', 'manager_id' => 99999]), 'INVALID_MANAGER');
|
|
$throwsAm(fn() => department_save($pdo, $newDept, ['name' => 'Vertrieb', 'sla_plan_id' => 99999]), 'INVALID_SLA_PLAN');
|
|
department_save($pdo, $newDept, ['name' => 'Vertrieb Nord', 'is_active' => false]);
|
|
department_save($pdo, $newDept, ['name' => 'Vertrieb Nord']);
|
|
$check((int) $pdo->query("SELECT is_active FROM departments WHERE id = {$newDept}")->fetchColumn() === 0, 'fehlendes is_active lässt den Abteilungsstatus unverändert');
|
|
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id) VALUES (?,?,?,?,?,?)')
|
|
->execute(['AM-1', 'Test', 'X', 'x@example.test', $newDept, $prio]);
|
|
$throwsAm(fn() => department_delete($pdo, $newDept), 'DEPARTMENT_IN_USE');
|
|
|
|
// ── Hilfethemen ─────────────────────────────────────────────────────────────
|
|
$tParent = help_topic_save($pdo, 0, ['name' => 'A', 'department_id' => $dept1, 'priority_id' => $prio]);
|
|
$tChild = help_topic_save($pdo, 0, ['name' => 'B', 'department_id' => $dept1, 'priority_id' => $prio, 'parent_id' => $tParent]);
|
|
$throwsAm(fn() => help_topic_save($pdo, $tParent, ['name' => 'A', 'department_id' => $dept1, 'priority_id' => $prio, 'parent_id' => $tChild]), 'INVALID_PARENT');
|
|
$throwsAm(fn() => help_topic_save($pdo, $tParent, ['name' => 'A', 'department_id' => $dept1, 'priority_id' => $prio, 'parent_id' => $tParent]), 'INVALID_PARENT');
|
|
$throwsAm(fn() => help_topic_save($pdo, 0, ['name' => 'C', 'department_id' => 99999, 'priority_id' => $prio]), 'INVALID_DEPARTMENT');
|
|
help_topic_delete($pdo, $tParent);
|
|
$check((int) $pdo->query("SELECT COALESCE(parent_id, 0) FROM help_topics WHERE id = {$tChild}")->fetchColumn() === 0, 'Unterthema rückt beim Löschen des Elternthemas nach oben');
|
|
|
|
// ── Teams ───────────────────────────────────────────────────────────────────
|
|
$teamA = team_save($pdo, 0, ['name' => 'Alpha', 'members' => [$agentId, $agentId, 0]]);
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM team_members WHERE team_id = {$teamA}")->fetchColumn() === 1, 'Teammitglieder werden ohne Duplikate gespeichert');
|
|
$throwsAm(fn() => team_save($pdo, 0, ['name' => 'alpha']), 'NAME_TAKEN');
|
|
$throwsAm(fn() => team_save($pdo, 0, ['name' => 'Beta', 'lead_id' => 99999]), 'INVALID_LEAD');
|
|
team_save($pdo, $teamA, ['name' => 'Alpha', 'is_active' => false, 'no_alerts' => true]);
|
|
team_save($pdo, $teamA, ['name' => 'Alpha 2']);
|
|
$row = $pdo->query("SELECT is_active, no_alerts FROM teams WHERE id = {$teamA}")->fetch();
|
|
$check((int) $row['is_active'] === 0 && (int) $row['no_alerts'] === 1, 'fehlende Team-Schalter bleiben unverändert');
|
|
|
|
// ── SLA-Pläne ───────────────────────────────────────────────────────────────
|
|
$throwsAm(fn() => sla_plan_save($pdo, 0, ['name' => 'Ungültig', 'response_hours' => 10, 'resolution_hours' => 5]), 'INVALID_HOURS');
|
|
$planDept = sla_plan_save($pdo, 0, ['name' => 'Abteilungsplan', 'response_hours' => 2, 'resolution_hours' => 8]);
|
|
$planTopic = sla_plan_save($pdo, 0, ['name' => 'Themenplan', 'response_hours' => 1, 'resolution_hours' => 3]);
|
|
$throwsAm(fn() => sla_plan_save($pdo, 0, ['name' => 'abteilungsplan', 'response_hours' => 1, 'resolution_hours' => 2]), 'NAME_TAKEN');
|
|
$pdo->prepare('UPDATE departments SET sla_plan_id = ? WHERE id = ?')->execute([$planDept, $dept1]);
|
|
$topicSla = help_topic_save($pdo, 0, ['name' => 'Mit Plan', 'department_id' => $dept1, 'priority_id' => $prio, 'sla_plan_id' => $planTopic]);
|
|
$prioRow = $pdo->query("SELECT * FROM priorities WHERE id = {$prio}")->fetch();
|
|
$from = '2026-01-05 08:00:00';
|
|
$none = calc_sla_dates($prioRow, $from);
|
|
$viaDept = calc_sla_dates($prioRow, $from, $dept1, null);
|
|
$viaTopic = calc_sla_dates($prioRow, $from, $dept1, $topicSla);
|
|
$check(strtotime($viaDept['resolution_due_at']) - strtotime($from) === 8 * 3600, 'Abteilungs-SLA-Plan ersetzt die Lösungszeit der Priorität');
|
|
$check(strtotime($viaTopic['resolution_due_at']) - strtotime($from) === 3 * 3600 && strtotime($viaTopic['first_response_due_at']) - strtotime($from) === 3600, 'Themen-SLA-Plan hat Vorrang vor dem der Abteilung');
|
|
$check($none['resolution_due_at'] !== $viaDept['resolution_due_at'], 'ohne Plan gelten die Zeiten der Priorität');
|
|
$pdo->prepare('UPDATE sla_plans SET is_active = 0 WHERE id = ?')->execute([$planTopic]);
|
|
$viaInactive = calc_sla_dates($prioRow, $from, $dept1, $topicSla);
|
|
$check(strtotime($viaInactive['resolution_due_at']) - strtotime($from) === 8 * 3600, 'deaktivierter Themenplan fällt auf den Abteilungsplan zurück');
|
|
sla_plan_delete($pdo, $planDept);
|
|
$check(calc_sla_dates($prioRow, $from, $dept1, null)['resolution_due_at'] === $none['resolution_due_at'], 'nach dem Löschen gelten wieder die Zeiten der Priorität');
|
|
|
|
// ── Überwachung des Mail-Abrufs ─────────────────────────────────────────────
|
|
$pdo->prepare("INSERT INTO imap_accounts (label, host, port, encryption, username, password_enc, folder, after_fetch, archive_folder, spam_folder, is_active, created_at) VALUES ('Test','imap.example.test',993,'ssl','u','x','INBOX','seen','Processed','Junk',1,?)")->execute([$now]);
|
|
$accId = (int) $pdo->lastInsertId();
|
|
$acc = fn() => $pdo->query("SELECT * FROM imap_accounts WHERE id = {$accId}")->fetch();
|
|
$hasCode = fn(string $code) => in_array($code, array_column(system_health($pdo), 'code'), true);
|
|
health_record_mail_result($pdo, $acc(), 'Verbindung abgelehnt');
|
|
$check($hasCode('mailbox_flaky') && !$hasCode('mailbox_down'), 'ein einzelner Fehlversuch ist nur ein Hinweis');
|
|
health_record_mail_result($pdo, $acc(), 'Verbindung abgelehnt');
|
|
health_record_mail_result($pdo, $acc(), 'Verbindung abgelehnt');
|
|
$check($hasCode('mailbox_down') && !empty($acc()['alert_sent_at']), 'nach drei Fehlversuchen: Störung gemeldet und Alarm vermerkt');
|
|
$alertAt = $acc()['alert_sent_at'];
|
|
health_record_mail_result($pdo, $acc(), 'Verbindung abgelehnt');
|
|
$check($acc()['alert_sent_at'] === $alertAt, 'der Alarm wird nicht bei jedem Fehlversuch wiederholt');
|
|
health_record_mail_result($pdo, $acc(), null);
|
|
$check(!$hasCode('mailbox_down') && !$hasCode('mailbox_flaky') && (int) $acc()['fail_count'] === 0 && empty($acc()['alert_sent_at']), 'nach Erfolg ist die Störung behoben');
|
|
set_setting('cron_fetch_emails_last_run', date('Y-m-d H:i:s', time() - 3600));
|
|
$check($hasCode('fetch_stale'), 'veralteter Abruf-Job wird gemeldet');
|
|
set_setting('cron_fetch_emails_last_run', db_now());
|
|
$check(!$hasCode('fetch_stale'), 'frischer Abruf-Job ist unauffällig');
|
|
|
|
// ── Bericht-Baukasten ───────────────────────────────────────────────────────
|
|
$cfg = report_clean_config($pdo, ['dimension' => "x'; DROP TABLE tickets;--", 'metrics' => ['total', 'bogus', 'x) FROM tickets--'], 'filters' => ['departmentId' => '1', 'evil_column' => '1', 'status' => 'open'], 'sort' => 'evil', 'from' => 'kein-datum', 'to' => '2026-02-01']);
|
|
$check($cfg['dimension'] === 'department', 'unbekannte Gruppierung fällt auf den Standard zurück');
|
|
$check($cfg['metrics'] === ['total'], 'unbekannte Kennzahlen werden verworfen');
|
|
$check(array_keys($cfg['filters']) === ['departmentId', 'status'], 'unbekannte Filter werden verworfen');
|
|
$check($cfg['sort'] === 'label', 'unbekannte Sortierung fällt auf "Gruppe" zurück');
|
|
$check(preg_match('/^\d{4}-\d{2}-\d{2}$/', $cfg['from']) === 1, 'ungültiges Datum wird durch einen Standard ersetzt');
|
|
$swapped = report_clean_config($pdo, ['from' => '2026-03-10', 'to' => '2026-03-01']);
|
|
$check($swapped['from'] === '2026-03-01' && $swapped['to'] === '2026-03-10', 'vertauschter Zeitraum wird korrigiert');
|
|
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)')
|
|
->execute(['RB-1', 'Bericht', 'X', 'x@example.test', $dept1, $prio, 'resolved', '2026-03-05 10:00:00', '2026-03-05 10:00:00']);
|
|
$run = report_run($pdo, ['dimension' => 'status', 'metrics' => ['total', 'resolved'], 'from' => '2026-03-01', 'to' => '2026-03-31'], null);
|
|
$check(count($run['rows']) === 1 && $run['rows'][0]['label'] === 'resolved' && $run['rows'][0]['total'] === 1 && $run['totals']['resolved'] === 1, 'Bericht zählt Tickets im Zeitraum');
|
|
$none = report_run($pdo, ['dimension' => 'status', 'from' => '2025-01-01', 'to' => '2025-01-31'], null);
|
|
$check($none['rows'] === [], 'ohne Tickets im Zeitraum bleibt der Bericht leer');
|
|
$scoped = report_run($pdo, ['dimension' => 'status', 'from' => '2026-03-01', 'to' => '2026-03-31'], [999999]);
|
|
$check($scoped['rows'] === [], 'der Abteilungsbereich des Mitarbeiters schränkt den Bericht ein');
|
|
$monthRun = report_run($pdo, ['dimension' => 'month', 'metrics' => ['total'], 'from' => '2026-03-01', 'to' => '2026-03-31'], null);
|
|
$check(($monthRun['rows'][0]['label'] ?? '') === '2026-03', 'Gruppierung nach Monat liefert "JJJJ-MM"');
|
|
$pdo->prepare('INSERT INTO departments (name, email, created_at) VALUES (?,?,?)')->execute(['=HYPERLINK("x")', '', $now]);
|
|
$csvDept = (int) $pdo->lastInsertId();
|
|
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)')
|
|
->execute(['RB-2', 'Bericht', 'X', 'x@example.test', $csvDept, $prio, 'open', '2026-03-06 10:00:00', '2026-03-06 10:00:00']);
|
|
$csv = report_to_csv(report_run($pdo, ['dimension' => 'department', 'metrics' => ['total'], 'from' => '2026-03-01', 'to' => '2026-03-31'], null));
|
|
$check(str_contains($csv, "\"'=HYPERLINK") || str_contains($csv, "'=HYPERLINK"), 'CSV schützt vor Formel-Injektion in Gruppennamen');
|
|
$check(str_starts_with($csv, "\xEF\xBB\xBF"), 'CSV beginnt mit UTF-8-BOM für Excel');
|
|
|
|
// ── Datenbank-Update (Erkennung ausstehender Migrationen) ────────────────────
|
|
require_once dirname(__DIR__) . '/database/migrate.php';
|
|
$check(count(migrations_known()) >= 30, 'bekannte Migrationen werden erkannt');
|
|
$check(migrations_pending($pdo) === [], 'frische Datenbank hat keine ausstehenden Migrationen');
|
|
$pdo->exec("DELETE FROM schema_migrations WHERE migration = 'php_0030_saved_reports'");
|
|
$check(migrations_pending($pdo) === ['php_0030_saved_reports'], 'fehlende Migration wird als ausstehend gemeldet');
|
|
$pdo->exec("INSERT INTO schema_migrations (migration) VALUES ('php_0030_saved_reports')");
|
|
$check(migrations_pending($pdo) === [], 'nach dem Einspielen ist nichts mehr ausstehend');
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
|
exit(1);
|
|
}
|
|
echo "Admin shared logic checks passed.\n";
|