277 lines
25 KiB
PHP
277 lines
25 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Prüft die Logik der neuen Ticket-Funktionen: verknüpfte Tickets, zeitversetzt gesendete Antworten, Berichte per E-Mail nach Zeitplan
|
|
* und die Datenschutz-Werkzeuge (Auskunft und Löschung), sowie die Syntax der neuen Skripte.
|
|
*/
|
|
|
|
putenv('DESKHUB_TESTING=1');
|
|
putenv('DESKHUB_SQLITE_PATH=:memory:');
|
|
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
|
|
|
$failures = [];
|
|
$total = 0;
|
|
$check = function (bool $ok, string $what) use (&$failures, &$total): void {
|
|
$total++;
|
|
if (!$ok) $failures[] = $what;
|
|
};
|
|
$pdo = get_db();
|
|
$now = date('Y-m-d H:i:s');
|
|
$dept = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
|
$prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
|
|
$pdo->exec('DELETE FROM staff');
|
|
$insStaff = $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)');
|
|
$insStaff->execute(['Chef', 'chef@test.de', 'x', 'admin', $dept, $now]);
|
|
$adminId = (int) $pdo->lastInsertId();
|
|
$insStaff->execute(['Agent', 'agent@test.de', 'x', 'agent', $dept, $now]);
|
|
$agentId = (int) $pdo->lastInsertId();
|
|
$admin = $pdo->query("SELECT * FROM staff WHERE id = {$adminId}")->fetch(PDO::FETCH_ASSOC);
|
|
$agent = $pdo->query("SELECT * FROM staff WHERE id = {$agentId}")->fetch(PDO::FETCH_ASSOC);
|
|
|
|
$mk = function (string $number, string $email = 'kunde@example.org', string $status = 'open', ?int $userId = null) use ($pdo, $now, $dept, $prio): array {
|
|
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, user_id, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)')
|
|
->execute([$number, 'Betreff ' . $number, $userId, 'Kunde Eins', $email, $dept, $prio, $status, 'web', $now, $now]);
|
|
$id = (int) $pdo->lastInsertId();
|
|
return $pdo->query("SELECT * FROM tickets WHERE id = {$id}")->fetch(PDO::FETCH_ASSOC);
|
|
};
|
|
|
|
// ── Verknüpfte Tickets ──────────────────────────────────────────────────────
|
|
$a = $mk('T-A'); $b = $mk('T-B'); $c = $mk('T-C', 'x@example.org', 'closed');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], 'T-B', 'related', $adminId) === null, 'Verknüpfen funktioniert');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], '#T-B', 'duplicate', $adminId) !== null, 'zweite Verknüpfung desselben Paares wird abgelehnt');
|
|
$check(ticket_link_add($pdo, (int) $b['id'], 'T-A', 'related', $adminId) !== null, 'auch in Gegenrichtung ist das Paar schon verknüpft');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], 'T-A', 'related', $adminId) !== null, 'Ticket nicht mit sich selbst');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], 'GIBTSNICHT', 'related', $adminId) !== null, 'unbekannte Nummer wird abgelehnt');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], 'T-C', 'unsinn', $adminId) !== null, 'ungültige Art wird abgelehnt');
|
|
$check(ticket_link_add($pdo, (int) $a['id'], 'T-C', 'blocks', $adminId, [$dept + 99]) !== null, 'Ticket einer fremden Abteilung darf nicht verknüpft werden');
|
|
$check(ticket_link_add($pdo, (int) $c['id'], 'T-A', 'blocks', $adminId) === null, 'blockiert-Verknüpfung (T-C blockiert T-A)');
|
|
$la = ticket_links_for($pdo, (int) $a['id']);
|
|
$byNum = array_column($la, null, 'number');
|
|
$check(count($la) === 2 && ($byNum['T-B']['label'] ?? '') === 'Verwandt mit' && ($byNum['T-C']['label'] ?? '') === 'Blockiert durch', 'Ansicht von T-A: Bezeichnungen: ' . json_encode(array_column($la, 'label')));
|
|
$lc = ticket_links_for($pdo, (int) $c['id']);
|
|
$check(count($lc) === 1 && $lc[0]['label'] === 'Blockiert' && $lc[0]['number'] === 'T-A', 'Gegenseite: T-C blockiert T-A');
|
|
$check(ticket_links_for($pdo, (int) $a['id'], [$dept + 99]) === [], 'ohne Zugriff auf die Abteilung bleibt die Liste leer');
|
|
$check(ticket_open_blockers($pdo, (int) $a['id']) === [], 'geschlossener Blocker zählt nicht als offen');
|
|
$pdo->exec("UPDATE tickets SET status = 'open' WHERE id = {$c['id']}");
|
|
$check(count(ticket_open_blockers($pdo, (int) $a['id'])) === 1, 'offener Blocker wird gemeldet');
|
|
$check(!ticket_link_remove($pdo, (int) $la[0]['id'], (int) $c['id'] + 1000), 'fremde Verknüpfung lässt sich nicht über ein anderes Ticket entfernen');
|
|
$check(ticket_link_remove($pdo, (int) $la[0]['id'], (int) $a['id']), 'Verknüpfung entfernen');
|
|
delete_ticket($pdo, $c, $adminId);
|
|
$check(ticket_links_for($pdo, (int) $a['id']) === [] || count(ticket_links_for($pdo, (int) $a['id'])) <= 1, 'Löschen eines Tickets entfernt dessen Verknüpfungen');
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM ticket_links WHERE ticket_id = {$c['id']} OR linked_ticket_id = {$c['id']}")->fetchColumn() === 0, 'keine Verknüpfungs-Reste nach dem Löschen');
|
|
|
|
// ── Zeitversetzte Antworten ─────────────────────────────────────────────────
|
|
$t = $mk('T-S1');
|
|
$check(scheduled_reply_parse_time('2030-05-01T14:30') === '2030-05-01 14:30:00', 'Zeit aus datetime-local');
|
|
$check(scheduled_reply_parse_time('morgen') === null && scheduled_reply_parse_time('') === null, 'ungültige Zeit');
|
|
$future = date('Y-m-d H:i:s', time() + 3600);
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p>Hallo</p>', date('Y-m-d H:i:s', time() - 60)) !== null, 'Zeitpunkt in der Vergangenheit wird abgelehnt');
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p>Hallo</p>', date('Y-m-d H:i:s', time() + 200 * 86400)) !== null, 'Zeitpunkt zu weit in der Zukunft wird abgelehnt');
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p> </p>', $future) !== null, 'leere Nachricht wird abgelehnt');
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p>x</p>', $future, ['set_status' => 'closed']) !== null, 'Schließen ohne Abschlussgrund wird abgelehnt');
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p>Später</p>', $future, ['set_status' => 'pending']) === null, 'Antwort einplanen');
|
|
$check(scheduled_reply_create($pdo, $t, $agentId, '<p>Heute noch</p>', date('Y-m-d H:i:s', time() + 40), ['set_status' => 'resolved', 'resolution_code' => 'fixed']) === null, 'Antwort mit Abschlussgrund einplanen');
|
|
$pend = scheduled_replies_pending($pdo, (int) $t['id']);
|
|
$check(count($pend) === 2, 'zwei ausstehende Antworten');
|
|
$check(scheduled_replies_run_due($pdo) === ['sent' => 0, 'failed' => 0], 'noch nichts fällig');
|
|
// zweite Antwort fällig machen
|
|
$pdo->exec("UPDATE scheduled_replies SET send_at = '" . date('Y-m-d H:i:s', time() - 5) . "' WHERE message LIKE '%Heute noch%'");
|
|
$r = scheduled_replies_run_due($pdo);
|
|
$check($r === ['sent' => 1, 'failed' => 0], 'fällige Antwort wird gesendet: ' . json_encode($r));
|
|
$after = $pdo->query("SELECT * FROM tickets WHERE id = {$t['id']}")->fetch(PDO::FETCH_ASSOC);
|
|
$check($after['status'] === 'resolved' && $after['resolution_code'] === 'fixed' && $after['first_responded_at'] !== null && (int) $after['assigned_staff_id'] === $agentId, 'Status, Abschlussgrund, Erstreaktion und Zuweisung gesetzt');
|
|
$thread = $pdo->query("SELECT * FROM ticket_threads WHERE ticket_id = {$t['id']}")->fetchAll(PDO::FETCH_ASSOC);
|
|
$check(count($thread) === 1 && $thread[0]['author_type'] === 'staff' && (int) $thread[0]['is_internal_note'] === 0 && str_contains($thread[0]['message'], 'Heute noch'), 'Nachricht steht im Ticket');
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM scheduled_replies WHERE state = 'sent'")->fetchColumn() === 1, 'Zeile ist als gesendet markiert');
|
|
$check(scheduled_replies_run_due($pdo)['sent'] === 0, 'zweiter Lauf sendet nicht doppelt');
|
|
$other = $pend[0]['message'] === '<p>Später</p>' ? $pend[0] : $pend[1];
|
|
$check(!scheduled_reply_cancel($pdo, (int) $other['id'], (int) $t['id'], ['id' => $agentId + 50, 'role' => 'agent']), 'Fremder Mitarbeiter darf nicht abbrechen');
|
|
$check(scheduled_reply_cancel($pdo, (int) $other['id'], (int) $t['id'], $agent), 'Verfasser bricht ab');
|
|
$check(scheduled_replies_pending($pdo, (int) $t['id']) === [], 'nichts mehr ausstehend');
|
|
// Ticket verschwindet vor dem Senden
|
|
$t2 = $mk('T-S2');
|
|
scheduled_reply_create($pdo, $t2, $adminId, '<p>Weg</p>', $future);
|
|
$pdo->exec("UPDATE scheduled_replies SET send_at = '" . date('Y-m-d H:i:s', time() - 5) . "' WHERE state = 'pending'");
|
|
$pdo->exec("DELETE FROM tickets WHERE id = {$t2['id']}");
|
|
$r = scheduled_replies_run_due($pdo);
|
|
$row = $pdo->query("SELECT state, error FROM scheduled_replies WHERE message LIKE '%Weg%'")->fetch(PDO::FETCH_ASSOC);
|
|
$check($r['failed'] === 1 && $row['state'] === 'failed' && $row['error'] !== '', 'Ticket gelöscht: Antwort schlägt sauber fehl');
|
|
// Verfasser deaktiviert
|
|
$t3 = $mk('T-S3');
|
|
scheduled_reply_create($pdo, $t3, $agentId, '<p>Inaktiv</p>', $future);
|
|
$pdo->exec("UPDATE staff SET is_active = 0 WHERE id = {$agentId}");
|
|
$pdo->exec("UPDATE scheduled_replies SET send_at = '" . date('Y-m-d H:i:s', time() - 5) . "' WHERE state = 'pending'");
|
|
$check(scheduled_replies_run_due($pdo)['failed'] === 1, 'deaktivierter Verfasser: Antwort wird nicht gesendet');
|
|
$pdo->exec("UPDATE staff SET is_active = 1 WHERE id = {$agentId}");
|
|
|
|
// ── Berichte per E-Mail ─────────────────────────────────────────────────────
|
|
$ref = strtotime('2026-11-04 12:00:00'); // Mittwoch
|
|
$check(report_period_range('yesterday', $ref) === ['2026-11-03', '2026-11-03'], 'Zeitraum gestern');
|
|
$check(report_period_range('last_7', $ref) === ['2026-10-28', '2026-11-03'], 'Zeitraum letzte 7 Tage');
|
|
$check(report_period_range('last_week', $ref) === ['2026-10-26', '2026-11-01'], 'Zeitraum letzte Kalenderwoche: ' . json_encode(report_period_range('last_week', $ref)));
|
|
$check(report_period_range('last_month', $ref) === ['2026-10-01', '2026-10-31'], 'Zeitraum letzter Monat');
|
|
$check(report_period_range('last_30', $ref) === ['2026-10-05', '2026-11-03'], 'Zeitraum letzte 30 Tage');
|
|
$s = ['frequency' => 'weekly', 'weekday' => 1, 'send_time' => '08:00', 'monthday' => 1];
|
|
$check(report_schedule_next($s, $ref) === '2026-11-09 08:00:00', 'nächster Montag: ' . report_schedule_next($s, $ref));
|
|
$check(report_schedule_next(['frequency' => 'weekly', 'weekday' => 3, 'send_time' => '18:00'] , $ref) === '2026-11-04 18:00:00', 'heute später am Tag');
|
|
$check(report_schedule_next(['frequency' => 'weekly', 'weekday' => 3, 'send_time' => '08:00'], $ref) === '2026-11-11 08:00:00', 'heute schon vorbei: nächste Woche');
|
|
$check(report_schedule_next(['frequency' => 'daily', 'send_time' => '07:30'], $ref) === '2026-11-05 07:30:00', 'täglich');
|
|
$check(report_schedule_next(['frequency' => 'monthly', 'monthday' => 1, 'send_time' => '06:00'], $ref) === '2026-12-01 06:00:00', 'monatlich');
|
|
$fri = strtotime('2026-11-06 12:00:00'); // Freitag
|
|
$check(report_schedule_next(['frequency' => 'daily', 'send_time' => '08:00', 'weekdays_only' => 1], $fri) === '2026-11-09 08:00:00', 'täglich nur Werktage: nach Freitag kommt Montag');
|
|
$check(report_schedule_next(['frequency' => 'daily', 'send_time' => '08:00', 'weekdays_only' => 0], $fri) === '2026-11-07 08:00:00', 'täglich ohne Einschränkung: auch am Samstag');
|
|
$check(report_schedule_next(['frequency' => 'weekly', 'weekday' => 1, 'send_time' => 'quatsch'], $ref) === '2026-11-09 08:00:00', 'ungültige Uhrzeit fällt auf 08:00 zurück');
|
|
|
|
[$list, $err] = report_schedule_recipients($pdo, "chef@test.de; AGENT@test.de\nchef@test.de", $agent);
|
|
$check($err === null && $list === ['chef@test.de', 'agent@test.de'], 'Empfänger: doppelte entfernt, Kleinschreibung');
|
|
[, $err] = report_schedule_recipients($pdo, 'extern@example.org', $agent);
|
|
$check($err !== null, 'Mitarbeiter ohne Adminrechte: externer Empfänger abgelehnt');
|
|
[$list, $err] = report_schedule_recipients($pdo, 'extern@example.org', $admin);
|
|
$check($err === null && $list === ['extern@example.org'], 'Administrator darf externe Empfänger eintragen');
|
|
[, $err] = report_schedule_recipients($pdo, 'keine-adresse', $admin);
|
|
$check($err !== null, 'ungültige Adresse abgelehnt');
|
|
[, $err] = report_schedule_recipients($pdo, implode(',', array_map(fn($i) => "u{$i}@example.org", range(1, 11))), $admin);
|
|
$check($err !== null, 'mehr als 10 Empfänger abgelehnt');
|
|
|
|
$cfg = report_clean_config($pdo, ['dimension' => 'department', 'metrics' => ['total', 'open']]);
|
|
$pdo->prepare('INSERT INTO saved_reports (staff_id, name, config, is_shared, created_at, updated_at) VALUES (?,?,?,?,?,?)')
|
|
->execute([$adminId, 'Wochenbericht <b>', json_encode($cfg), 0, $now, $now]);
|
|
$reportId = (int) $pdo->lastInsertId();
|
|
$check(report_schedule_create($pdo, $agent, ['report_id' => $reportId, 'recipients' => 'agent@test.de']) !== null, 'fremden, nicht geteilten Bericht darf der Mitarbeiter nicht einplanen');
|
|
$check(report_schedule_create($pdo, $admin, ['report_id' => $reportId, 'recipients' => 'chef@test.de', 'frequency' => 'daily', 'send_time' => '07:00', 'period' => 'yesterday']) === null, 'Zeitplan anlegen');
|
|
$sch = $pdo->query('SELECT * FROM report_schedules')->fetch(PDO::FETCH_ASSOC);
|
|
$check($sch && $sch['frequency'] === 'daily' && $sch['period'] === 'yesterday' && $sch['next_run_at'] > $now, 'Zeitplan gespeichert, nächster Termin liegt in der Zukunft');
|
|
$check((int) $sch['only_if_data'] === 1 && (int) $sch['skip_unchanged'] === 1 && (int) $sch['weekdays_only'] === 0, 'Standard: nur bei Daten, nur bei Änderung (Werktage ohne Angabe aus)');
|
|
$rendered = report_schedule_render($pdo, $sch);
|
|
$check($rendered !== null && str_contains($rendered[1], 'Wochenbericht <b>') && !str_contains($rendered[1], 'Wochenbericht <b>') && str_contains($rendered[0], 'DeskHub-Bericht'), 'Bericht wird gerendert, Name ist maskiert');
|
|
$check(str_contains($rendered[1], '<table') && str_contains($rendered[1], 'Gesamt') || str_contains($rendered[1], 'Keine Tickets'), 'Tabelle im Text');
|
|
$check(report_schedules_run_due($pdo) === ['sent' => 0, 'failed' => 0], 'noch nicht fällig');
|
|
$pdo->exec("UPDATE report_schedules SET next_run_at = '" . date('Y-m-d H:i:s', time() - 10) . "'");
|
|
$r = report_schedules_run_due($pdo);
|
|
$sch2 = $pdo->query('SELECT * FROM report_schedules')->fetch(PDO::FETCH_ASSOC);
|
|
$check($sch2['next_run_at'] > $now, 'nach dem Lauf ist der nächste Termin gesetzt (kein Doppelversand)');
|
|
$check($r === ['sent' => 0, 'failed' => 0] && str_contains((string) $sch2['last_note'], 'keine Daten'), 'leerer Bericht wird nicht gesendet, die Notiz steht im Zeitplan: ' . json_encode([$r, $sch2['last_note']]));
|
|
$check(report_schedule_send($pdo, $sch2)['skipped'] === null, '„Test senden“ ignoriert die Schalter');
|
|
// Mit Daten: wird versucht (ohne eingerichteten Mailweg scheitert der Versand, das ist hier erwartet)
|
|
$mk('R-1');
|
|
$pdo->exec("UPDATE tickets SET created_at = '" . date('Y-m-d H:i:s', strtotime('-1 day')) . "' WHERE ticket_number = 'R-1'");
|
|
$pdo->exec("UPDATE report_schedules SET next_run_at = '" . date('Y-m-d H:i:s', time() - 10) . "'");
|
|
$r = report_schedules_run_due($pdo);
|
|
$check($r['sent'] + $r['failed'] === 1, 'Bericht mit Daten wird bearbeitet: ' . json_encode($r));
|
|
// Gleiche Zahlen wie beim letzten Versand: übersprungen
|
|
$hash = report_schedule_render($pdo, $pdo->query('SELECT * FROM report_schedules')->fetch(PDO::FETCH_ASSOC))[3];
|
|
$pdo->exec("UPDATE report_schedules SET last_hash = '{$hash}', next_run_at = '" . date('Y-m-d H:i:s', time() - 10) . "'");
|
|
$r = report_schedules_run_due($pdo);
|
|
$sch3 = $pdo->query('SELECT * FROM report_schedules')->fetch(PDO::FETCH_ASSOC);
|
|
$check($r === ['sent' => 0, 'failed' => 0] && str_contains((string) $sch3['last_note'], 'keine Änderung'), 'unveränderter Bericht wird nicht erneut gesendet: ' . json_encode([$r, $sch3['last_note']]));
|
|
$pdo->exec("UPDATE report_schedules SET skip_unchanged = 0, next_run_at = '" . date('Y-m-d H:i:s', time() - 10) . "'");
|
|
$r = report_schedules_run_due($pdo);
|
|
$check($r['sent'] + $r['failed'] === 1, 'ohne „nur bei Änderung“ wird wieder gesendet');
|
|
$check(report_schedules_run_due($pdo) === ['sent' => 0, 'failed' => 0], 'zweiter Lauf: nichts mehr fällig');
|
|
$pdo->exec("DELETE FROM saved_reports");
|
|
$check(report_schedule_render($pdo, $sch2) === null, 'gelöschter Bericht: kein Versand');
|
|
|
|
// ── Datenschutz ─────────────────────────────────────────────────────────────
|
|
$check(!gdpr_tools_enabled(), 'Werkzeuge sind standardmäßig ausgeschaltet');
|
|
set_setting('gdpr_tools_enabled', '1');
|
|
$check(gdpr_tools_enabled(), 'Werkzeuge einschaltbar');
|
|
$pdo->exec("INSERT INTO users (name, email, password_hash, phone, created_at) VALUES ('Erika Muster','erika@example.org','x','0123','{$now}')");
|
|
$uid = (int) $pdo->lastInsertId();
|
|
$g1 = $mk('G-1', 'erika@example.org', 'closed', $uid);
|
|
$g2 = $mk('G-2', 'erika@example.org', 'open', $uid);
|
|
$other = $mk('G-OTHER', 'andere@example.org', 'closed');
|
|
$pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, user_id, message, is_internal_note, created_at) VALUES (?,?,?,?,0,?)")->execute([$g1['id'], 'customer', $uid, '<p>Meine Adresse ist Musterweg 1</p>', $now]);
|
|
$cThread = (int) $pdo->lastInsertId();
|
|
$pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,1,?)")->execute([$g1['id'], 'staff', $adminId, 'Interne Notiz', $now]);
|
|
$pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,0,?)")->execute([$other['id'], 'customer', 'Fremde Nachricht', $now]);
|
|
$uploadDir = realpath((string) config()['uploads']['path']) ?: sys_get_temp_dir();
|
|
$stored = 'gdpr-test-' . bin2hex(random_bytes(4)) . '.txt';
|
|
file_put_contents($uploadDir . DIRECTORY_SEPARATOR . $stored, 'geheim');
|
|
$pdo->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')->execute([$cThread, 'ausweis.txt', $stored, 6, $now]);
|
|
$pdo->prepare('INSERT INTO customer_notes (user_id, staff_id, note, created_at) VALUES (?,?,?,?)')->execute([$uid, $adminId, 'Ruft oft an', $now]);
|
|
$pdo->prepare('INSERT INTO ticket_collaborators (ticket_id, email, name, created_at) VALUES (?,?,?,?)')->execute([$g1['id'], 'erika@example.org', 'Erika', $now]);
|
|
|
|
$exp = gdpr_customer_export($pdo, ['user_id' => $uid]);
|
|
$check($exp !== null && $exp['kunde']['email'] === 'erika@example.org' && $exp['kunde']['telefon'] === '0123' && $exp['kunde']['hat_konto'] === true, 'Export: Stammdaten');
|
|
$check(count($exp['tickets']) === 2 && !in_array('G-OTHER', array_column($exp['tickets'], 'nummer'), true), 'Export: nur Tickets dieses Kunden');
|
|
$msgs = $exp['tickets'][0]['nachrichten'];
|
|
$check(count($msgs) === 2 && $msgs[0]['text'] === 'Meine Adresse ist Musterweg 1' && $msgs[1]['intern'] === true, 'Export: Nachrichten als Klartext, interne gekennzeichnet');
|
|
$check($exp['tickets'][0]['anhaenge'][0]['name'] === 'ausweis.txt' && $exp['notizen_zum_kunden'][0]['text'] === 'Ruft oft an', 'Export: Anhangsnamen und Notizen');
|
|
$check(json_encode($exp, JSON_UNESCAPED_UNICODE) !== false, 'Export lässt sich als JSON kodieren');
|
|
$guest = gdpr_customer_export($pdo, ['email' => 'andere@example.org']);
|
|
$check($guest !== null && $guest['kunde']['hat_konto'] === false && count($guest['tickets']) === 1, 'Export für Gast über E-Mail');
|
|
$check(gdpr_customer_export($pdo, ['email' => 'niemand@example.org']) === null && gdpr_customer_export($pdo, ['user_id' => 99999]) === null, 'unbekannter Kunde: kein Export');
|
|
|
|
$res = gdpr_customer_erase($pdo, ['user_id' => $uid]);
|
|
$check($res['error'] !== null && (int) $pdo->query("SELECT COUNT(*) FROM users WHERE id = {$uid}")->fetchColumn() === 1, 'offenes Ticket verhindert die Löschung');
|
|
$res = gdpr_customer_erase($pdo, ['user_id' => $uid], true);
|
|
$check($res['error'] === null && $res['tickets'] === 2 && $res['messages'] === 1 && $res['files'] === 1, 'Löschung erzwungen: ' . json_encode($res));
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM users WHERE id = {$uid}")->fetchColumn() === 0, 'Konto entfernt');
|
|
$tk = $pdo->query("SELECT requester_name, requester_email, requester_ip, user_id FROM tickets WHERE id = {$g1['id']}")->fetch(PDO::FETCH_ASSOC);
|
|
$check($tk['requester_name'] === 'Gelöscht' && str_ends_with($tk['requester_email'], '@deleted.local') && $tk['user_id'] === null, 'Ticket anonymisiert');
|
|
$check($pdo->query("SELECT message FROM ticket_threads WHERE id = {$cThread}")->fetchColumn() === '[Auf Anfrage des Betroffenen gelöscht]', 'Kundennachricht ersetzt');
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM ticket_threads WHERE ticket_id = {$g1['id']} AND is_internal_note = 1")->fetchColumn() === 1, 'Mitarbeiter-Notiz bleibt');
|
|
$check((int) $pdo->query('SELECT COUNT(*) FROM ticket_attachments')->fetchColumn() === 0 && !file_exists($uploadDir . DIRECTORY_SEPARATOR . $stored), 'Anhang: Zeile und Datei entfernt');
|
|
$check((int) $pdo->query('SELECT COUNT(*) FROM customer_notes')->fetchColumn() === 0 && (int) $pdo->query('SELECT COUNT(*) FROM ticket_collaborators')->fetchColumn() === 0, 'Notizen und Mitleser entfernt');
|
|
$check($pdo->query("SELECT requester_email FROM tickets WHERE id = {$other['id']}")->fetchColumn() === 'andere@example.org', 'Daten anderer Kunden bleiben unberührt');
|
|
$check(gdpr_customer_erase($pdo, ['email' => 'niemand@example.org'])['error'] !== null, 'unbekannter Kunde: Fehler');
|
|
$res = gdpr_customer_erase($pdo, ['email' => 'andere@example.org']);
|
|
$check($res['error'] === null && $res['tickets'] === 1, 'Gast löschen (geschlossenes Ticket, ohne Erzwingen)');
|
|
@unlink($uploadDir . DIRECTORY_SEPARATOR . $stored);
|
|
|
|
// ── Zähler „braucht Aufmerksamkeit“ ──────────────────────────────────────────
|
|
$pdo->exec('DELETE FROM ticket_threads'); $pdo->exec('DELETE FROM tickets'); $pdo->exec('DELETE FROM ticket_acks');
|
|
$mkA = function (string $n, string $status, ?int $assigned, array $extra = []) use ($pdo, $now, $dept, $prio): int {
|
|
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, assigned_staff_id, is_spam, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)')
|
|
->execute([$n, 'S ' . $n, 'K', 'k@example.org', $dept, $prio, $status, $extra['source'] ?? 'web', $assigned, $extra['spam'] ?? 0, $extra['created'] ?? $now, $now]);
|
|
return (int) $pdo->lastInsertId();
|
|
};
|
|
$att = fn() => staff_attention_tickets($pdo, $agent);
|
|
$tNew = $mkA('A-NEW', 'open', null);
|
|
$tMine = $mkA('A-MINE', 'open', $agentId);
|
|
$tOther = $mkA('A-OTHER', 'open', $adminId);
|
|
$tAnswered = $mkA('A-ANS', 'answered', $agentId);
|
|
$tSpam = $mkA('A-SPAM', 'open', null, ['spam' => 1]);
|
|
$tStaff = $mkA('A-STAFF', 'open', null, ['source' => 'staff']);
|
|
$r = $att();
|
|
$check($r['unassigned'] === [$tNew] && $r['mine'] === [$tMine], 'Zähler: nur offene, nicht zugewiesene und eigene Tickets: ' . json_encode($r));
|
|
ticket_ack($pdo, $tNew, $agentId);
|
|
$check($att()['unassigned'] === [], 'Zähler: geöffnetes (gesehenes) Ticket verschwindet');
|
|
ticket_ack($pdo, $tNew, $agentId); // zweites Mal: kein Fehler, keine Doppelzeile
|
|
$check((int) $pdo->query("SELECT COUNT(*) FROM ticket_acks WHERE ticket_id = {$tNew}")->fetchColumn() === 1, 'Zähler: Merker wird nicht doppelt angelegt');
|
|
$pdo->exec("UPDATE ticket_acks SET acked_at = '2000-01-01 00:00:00' WHERE ticket_id = {$tNew}");
|
|
$check($att()['unassigned'] === [$tNew], 'Zähler: eine ältere Merker-Zeit zählt nicht als gesehen');
|
|
ticket_ack($pdo, $tNew, $agentId);
|
|
$pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,0,?)")->execute([$tNew, 'customer', 'Neue Frage', date('Y-m-d H:i:s', time() + 60)]);
|
|
$check($att()['unassigned'] === [$tNew], 'Zähler: neue Kundennachricht nach dem Öffnen zählt wieder');
|
|
$pdo->exec("UPDATE tickets SET status = 'answered' WHERE id = {$tMine}");
|
|
$check($att()['mine'] === [], 'Zähler: beantwortetes Ticket verschwindet von selbst');
|
|
$pdo->exec("UPDATE tickets SET assigned_staff_id = {$adminId} WHERE id = {$tNew}");
|
|
$check($att()['unassigned'] === [], 'Zähler: jemand anderes hat das Ticket übernommen: weg');
|
|
$pdo->exec("UPDATE tickets SET assigned_staff_id = NULL, snoozed_until = '" . date('Y-m-d H:i:s', time() + 3600) . "' WHERE id = {$tNew}");
|
|
$check($att()['unassigned'] === [], 'Zähler: zurückgestelltes Ticket zählt nicht');
|
|
$pdo->exec("UPDATE tickets SET snoozed_until = NULL WHERE id = {$tNew}");
|
|
$check($att()['unassigned'] === [$tNew], 'Zähler: nach der Wiedervorlage zählt es wieder');
|
|
$check(staff_attention_tickets($pdo, $admin)['mine'] === [$tOther], 'Zähler: jeder Mitarbeiter sieht seinen eigenen Stand');
|
|
|
|
// ── Skripte ─────────────────────────────────────────────────────────────────
|
|
$node = trim((string) @shell_exec(PHP_OS_FAMILY === 'Windows' ? 'where node 2>NUL' : 'command -v node 2>/dev/null'));
|
|
if ($node !== '') {
|
|
foreach (['staff-shortcuts.js', 'ticket-lightbox.js'] as $js) {
|
|
$out = [];
|
|
exec('node --check ' . escapeshellarg(dirname(__DIR__) . '/assets/js/' . $js) . ' 2>&1', $out, $code);
|
|
$check($code === 0, $js . ' hat gültige Syntax: ' . implode(' ', $out));
|
|
}
|
|
}
|
|
$check(str_contains((string) file_get_contents(dirname(__DIR__) . '/staff/_footer.php'), 'staff-shortcuts.js'), 'Tastenkürzel sind im Fuß der Mitarbeiterseiten eingebunden');
|
|
foreach (['send_scheduled_replies', 'send_report_schedules'] as $job) {
|
|
$check(isset(pseudo_cron_job_definitions()[$job]), 'Hintergrundjob ' . $job . ' ist im eingebauten Cron eingetragen');
|
|
}
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
|
exit(1);
|
|
}
|
|
echo "Ticket features checks passed ({$total} Prüfungen).\n";
|