Files
deskhub/DeskHub-Webseite/tests/csat_status_templates.php
T
2026-10-24 20:14:41 +00:00

162 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft Zufriedenheitsabfrage (CSAT), Statusseite und Ticket-Vorlagen: Datenbank, Versand, Bewertung, Auswertung,
* Störungsmeldungen, Vorlagen für Kunden und Mitarbeiter sowie die Seiten und Schnittstellen dazu.
*/
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
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;
};
$pdo = get_db();
$now = db_now();
// ── Zufriedenheitsabfrage ───────────────────────────────────────────────────
$check(!csat_enabled() && csat_delay_minutes() === 30, 'Standard: ausgeschaltet, 30 Minuten');
$mk = function (string $status, string $email, ?string $closedAt, string $source = 'web', int $spam = 0) use ($pdo, $now): int {
static $n = 5000;
$n++;
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, is_spam, closed_at, resolved_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
->execute(["CS-{$n}", "Betreff {$n}", 'Erika Muster', $email, 1, 2, $status, $source, $spam, $status === 'closed' ? $closedAt : null, $status === 'resolved' ? $closedAt : null, $now, $now]);
return (int) $pdo->lastInsertId();
};
$old = date('Y-m-d H:i:s', time() - 3600);
$t1 = $mk('closed', 'erika@kunde.example', $old);
$check(csat_send_due($pdo) === 0, 'ausgeschaltet: nichts wird gesendet');
csat_set_enabled(true, 30);
$check(csat_enabled() && get_setting('csat_since', '') !== '', 'Einschalten merkt sich den Zeitpunkt');
$check(csat_send_due($pdo) === 0, 'Ticket aus der Zeit vor dem Einschalten bekommt keine Abfrage');
set_setting('csat_since', date('Y-m-d H:i:s', time() - 86400));
$tNew = $mk('closed', 'neu@kunde.example', date('Y-m-d H:i:s', time() - 60));
$tSpam = $mk('closed', 'spam@kunde.example', $old, 'web', 1);
$tChat = $mk('closed', 'chat@kunde.example', $old, 'chat');
$tOpen = $mk('open', 'offen@kunde.example', null);
$tNoMail = $mk('closed', '', $old);
$tBad = $mk('closed', 'kein-email', $old);
$tRes = $mk('resolved', 'geloest@kunde.example', $old);
$sent = csat_send_due($pdo);
$rows = $pdo->query('SELECT ticket_id FROM ticket_csat ORDER BY ticket_id')->fetchAll(PDO::FETCH_COLUMN);
$check($sent === 2 && array_map('intval', $rows) === [$t1, $tRes], 'gesendet nur für geschlossene/gelöste Tickets mit Adresse, nach der Wartezeit, ohne Spam und Chat: ' . json_encode($rows));
$check(csat_send_due($pdo) === 0, 'pro Ticket nur eine Abfrage');
$pdo->prepare('UPDATE tickets SET updated_at = ?, closed_at = ? WHERE id = ?')->execute([$old, $old, $tNew]);
$check(csat_send_due($pdo) === 1, 'nach Ablauf der Wartezeit kommt auch das neuere Ticket dran');
$row = csat_for_ticket($pdo, $t1);
$token = (string) $row['token'];
$check(preg_match('/^[a-f0-9]{40}$/', $token) === 1 && csat_find_by_token($pdo, $token)['ticket_number'] === 'CS-5001', 'Token und Suche');
$check(csat_find_by_token($pdo, 'zzz') === null && csat_find_by_token($pdo, str_repeat('a', 40)) === null, 'ungültiger Token');
$check(str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'csat.php?t=' . $token . '&amp;r=5') || str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'r=5'), 'Mail enthält die fünf Sterne-Links');
$check(!csat_record($pdo, $token, 0, '')['ok'] && !csat_record($pdo, $token, 6, '')['ok'], 'Bewertung muss 1 bis 5 sein');
$check(csat_record($pdo, $token, 4, ' Schnell und freundlich ')['ok'], 'Bewertung wird gespeichert');
$check(!csat_record($pdo, $token, 1, '')['ok'], 'zweite Bewertung wird abgelehnt');
$row = csat_for_ticket($pdo, $t1);
$check((int) $row['rating'] === 4 && $row['comment'] === 'Schnell und freundlich' && $row['answered_at'] !== null, 'Bewertung und Kommentar stehen in der Datenbank');
$check(!csat_record($pdo, 'x', 3, '')['ok'], 'unbekannter Link');
$tok2 = (string) csat_for_ticket($pdo, $tRes)['token'];
csat_record($pdo, $tok2, 2, '');
$st = csat_stats($pdo, date('Y-m-d 00:00:00', time() - 86400), date('Y-m-d 23:59:59'), null);
$check($st['sent'] === 3 && $st['answered'] === 2 && $st['avg'] === 3.0 && $st['rate'] === 66.7 && $st['dist'][4] === 1 && $st['dist'][2] === 1 && count($st['recent']) === 2, 'Auswertung: ' . json_encode($st));
$check(csat_stats($pdo, '2000-01-01 00:00:00', '2000-12-31 23:59:59', null)['sent'] === 0 && csat_stats($pdo, $now, $now, [])['sent'] === 0, 'leere Zeiträume und Abteilungen');
$check(csat_stars(4) === '★★★★☆' && csat_stars(9) === '★★★★★', 'Sterne-Text');
csat_set_enabled(false, 30);
$check(!csat_enabled(), 'Ausschalten');
// ── Statusseite ─────────────────────────────────────────────────────────────
$check(!status_page_enabled() && status_overall($pdo)['key'] === 'operational', 'Statusseite standardmäßig aus, ohne Dienste alles in Ordnung');
$web = status_save_component($pdo, null, 'Webseite', 'Kundenportal', 10);
$mail = status_save_component($pdo, null, 'E-Mail', '', 20);
$check($web !== null && $mail !== null && status_save_component($pdo, null, ' ', '', 0) === null, 'Dienste anlegen (leerer Name abgelehnt)');
$check(!status_set_component($pdo, $web, 'kaputt') && status_set_component($pdo, $web, 'degraded') && status_overall($pdo)['key'] === 'degraded', 'Zustand setzen, ungültiger Zustand abgelehnt');
status_set_component($pdo, $mail, 'outage');
$check(status_overall($pdo)['key'] === 'outage' && status_overall($pdo)['label'] === 'Ausfall', 'schlechtester Zustand zählt');
status_set_component($pdo, $mail, 'maintenance');
status_set_component($pdo, $web, 'operational');
$check(status_overall($pdo)['key'] === 'maintenance', 'Wartung');
status_set_component($pdo, $mail, 'operational');
$r = status_create_incident($pdo, ' ', 'x', 'investigating', null, null, 1);
$check(!$r['ok'], 'Meldung ohne Titel wird abgelehnt');
$r = status_create_incident($pdo, 'E-Mail verzögert', 'Wir untersuchen das.', 'investigating', $mail, 'degraded', 1);
$check($r['ok'] && status_overall($pdo)['key'] === 'degraded', 'Meldung setzt den Dienst auf „eingeschränkt“');
$inc = (int) $r['id'];
$check(count(status_incidents($pdo, 14, true)) === 1 && status_incidents($pdo)[0]['updates'][0]['body'] === 'Wir untersuchen das.', 'offene Meldung mit erstem Update');
$other = status_create_incident($pdo, 'Zweite Störung', '', 'identified', $mail, 'outage', 1)['id'];
$check(status_update_incident($pdo, $inc, 'resolved', 'Behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'outage', 'Dienst bleibt gestört, solange eine andere Meldung offen ist');
$check(status_update_incident($pdo, $other, 'resolved', 'Auch behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'operational', 'nach der letzten Meldung wieder „In Betrieb“');
$check(!status_update_incident($pdo, 999, 'resolved', '', 1)['ok'] && !status_update_incident($pdo, $inc, 'quatsch', '', 1)['ok'], 'unbekannte Meldung / Stufe');
$check(status_incidents($pdo, 14, true) === [] && count(status_incidents($pdo, 14)) === 2 && status_incidents($pdo)[0]['resolved_at'] !== null, 'behobene Meldungen bleiben 14 Tage sichtbar');
$pdo->prepare('UPDATE status_incidents SET created_at = ? WHERE id = ?')->execute([date('Y-m-d H:i:s', time() - 30 * 86400), $inc]);
$check(count(status_incidents($pdo, 14)) === 1, 'ältere behobene Meldungen verschwinden');
$sum = status_summary($pdo);
$check($sum['enabled'] === false && count($sum['components']) === 2 && $sum['overall']['key'] === 'operational' && count($sum['componentStates']) === 4 && str_ends_with($sum['url'], '/status.php'), 'Zusammenfassung für die Apps');
status_delete_incident($pdo, $inc);
status_delete_component($pdo, $mail);
$check(count(status_components($pdo)) === 1 && (int) $pdo->query("SELECT COUNT(*) FROM status_incident_updates WHERE incident_id = {$inc}")->fetchColumn() === 0, 'Löschen räumt die Updates mit auf');
// ── Ticket-Vorlagen ─────────────────────────────────────────────────────────
$check(!ticket_template_save($pdo, null, ['name' => ' '])['ok'], 'Vorlage ohne Namen wird abgelehnt');
$a = ticket_template_save($pdo, null, ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'priority_id' => '2', 'is_public' => '1', 'sort_order' => '10']);
$b = ticket_template_save($pdo, null, ['name' => 'Interne Vorlage', 'subject' => 'Intern', 'department_id' => '9999', 'is_public' => '', 'sort_order' => '20']);
$check($a['ok'] && $b['ok'], 'Vorlagen anlegen');
$check(count(ticket_templates_list($pdo)) === 2 && count(ticket_templates_list($pdo, true)) === 1 && ticket_templates_list($pdo, true)[0]['name'] === 'Neuer Mitarbeiter', 'Kunden sehen nur öffentliche Vorlagen');
$tb = ticket_template_get($pdo, $b['id']);
$check($tb['department_id'] === null && ticket_template_get($pdo, $b['id'], true) === null && ticket_template_get($pdo, 9999) === null, 'unbekannte Abteilung wird verworfen, nicht öffentliche Vorlage bleibt Kunden verborgen');
$check(ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter 2', 'is_public' => '1', 'is_active' => '0'])['ok'] && ticket_template_get($pdo, $a['id']) === null && count(ticket_templates_list($pdo, false, true)) === 2, 'Vorlage deaktivieren');
ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'is_public' => '1', 'is_active' => '1']);
$p = ticket_template_payload(ticket_template_get($pdo, $a['id']));
$check($p['name'] === 'Neuer Mitarbeiter' && $p['departmentId'] === 1 && $p['priorityId'] === null && $p['isPublic'] === true && $p['body'] === "Name:\nStartdatum:", 'Vorlage als Feldliste');
ticket_template_delete($pdo, $b['id']);
$check(count(ticket_templates_list($pdo)) === 1, 'Vorlage löschen');
// ── Seiten ──────────────────────────────────────────────────────────────────
$_SERVER['REQUEST_METHOD'] = 'GET';
$render = function (string $file, array $get = [], array $post = [], string $method = 'GET') {
$_GET = $get;
$_POST = $post;
$_SERVER['REQUEST_METHOD'] = $method;
ob_start();
try {
include dirname(__DIR__) . '/' . $file;
} finally {
$html = (string) ob_get_clean();
}
return $html;
};
// Bewertungsseite
csat_set_enabled(true, 30);
$pdo->prepare('UPDATE ticket_csat SET rating = NULL, comment = NULL, answered_at = NULL WHERE ticket_id = ?')->execute([$t1]);
$html = $render('csat.php', ['t' => $token, 'r' => '5']);
$check(str_contains($html, 'Wie zufrieden waren Sie?') && str_contains($html, 'CS-5001') && str_contains($html, 'value="5" style="position:absolute;opacity:0;width:0;height:0" checked'), 'Bewertungsseite zeigt das Ticket mit vorgewähltem Stern');
$check((int) csat_for_ticket($pdo, $t1)['answered_at'] === 0 && csat_for_ticket($pdo, $t1)['answered_at'] === null, 'Aufrufen des Links bewertet noch nichts');
$html = $render('csat.php', [], ['t' => $token, 'rating' => '3', 'comment' => 'ok'], 'POST');
$check(str_contains($html, 'Vielen Dank') && (int) csat_for_ticket($pdo, $t1)['rating'] === 3, 'Absenden speichert die Bewertung');
$check(str_contains($render('csat.php', ['t' => $token]), 'Bereits bewertet'), 'zweiter Aufruf: bereits bewertet');
$check(str_contains($render('csat.php', ['t' => 'unsinn']), 'Link nicht gültig'), 'ungültiger Link');
// Statusseite
set_setting('status_page_enabled', '1');
status_set_component($pdo, $web, 'degraded');
status_create_incident($pdo, 'Portal langsam', 'Wir arbeiten daran.', 'identified', $web, null, 1);
$html = $render('status.php');
$check(str_contains($html, 'Systemstatus') && str_contains($html, 'Teilweise eingeschränkt') && str_contains($html, 'Webseite') && str_contains($html, 'Portal langsam') && str_contains($html, 'Wir arbeiten daran.') && str_contains($html, 'Ursache gefunden'), 'Statusseite zeigt Dienste und Meldung');
$check(str_contains($html, 'href="/status.php"'), 'Link „Status“ im Kundenbereich');
// Ticket-Formular mit Vorlage
$tpl = (int) $a['id'];
$html = $render('ticket_new.php', ['template' => (string) $tpl]);
$check(str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, "Name:\nStartdatum:") && str_contains($html, 'ticket_new.php?template=' . $tpl) && str_contains($html, 'Ohne Vorlage'), 'Kundenformular wird von der Vorlage vorbelegt');
$html = $render('ticket_new.php');
$check(!str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, 'Womit können wir helfen'), 'ohne Vorlage bleibt das Formular leer, Vorlagen werden angeboten');
$html = $render('ticket_new.php', ['template' => '99999']);
$check(!str_contains($html, 'Ohne Vorlage'), 'unbekannte Vorlage wird ignoriert');
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo 'CSAT, Status und Vorlagen checks passed.' . "\n";