query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $pdo->prepare("INSERT INTO departments (name, email, is_active, created_at) VALUES ('Technik', 'technik@team.example', 1, ?)")->execute([db_now()]); $d2 = (int) $pdo->lastInsertId(); $prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn(); $mkStaff = function (string $name, string $role, ?int $dept) use ($pdo): int { $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)')->execute([$name, strtolower($name) . '@team.example', 'x', $role, $dept, db_now()]); return (int) $pdo->lastInsertId(); }; $anna = $mkStaff('Anna', 'agent', $d1); $chef = $mkStaff('Chef', 'admin', null); $n = 0; $mk = function (string $subject, int $dept, array $o = []) use ($pdo, $prio, &$n): int { $n++; $now = db_now(); $pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, is_spam, merged_into, assigned_staff_id, resolution_due_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)') ->execute(['LT-' . $n, $subject, $o['name'] ?? 'Kunde', 'k@x.example', $dept, $prio, $o['status'] ?? 'open', $o['source'] ?? 'web', $o['spam'] ?? 0, $o['merged'] ?? null, $o['assigned'] ?? null, $o['due'] ?? null, $now, $now]); return (int) $pdo->lastInsertId(); }; $call = function (int $staffId, int $since) { $_SESSION['staff_id'] = $staffId; $_GET = ['since' => (string) $since]; // current_staff() merkt sich den Mitarbeiter je Prozess; für den Test über Reflection zurücksetzen $rf = new ReflectionFunction('current_staff'); foreach ($rf->getStaticVariables() as $k => $v) { /* nur lesen */ } ob_start(); include dirname(__DIR__) . '/staff/live_tickets.php'; return json_decode((string) ob_get_clean(), true); }; // current_staff() cached den ersten Mitarbeiter im Prozess: deshalb jeden Fall in einem eigenen Prozess prüfen $php = escapeshellarg(PHP_BINARY); $quiet = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null'; $script = sys_get_temp_dir() . '/lt-call-' . bin2hex(random_bytes(3)) . '.php'; file_put_contents($script, " 0) \$_SESSION['staff_id'] = (int) \$argv[1];\n\$_GET = ['since' => \$argv[2] ?? '0'];\ninclude " . var_export(dirname(__DIR__) . '/staff/live_tickets.php', true) . ";\n"); $run = function (int $staffId, int $since) use ($php, $script, $quiet): ?array { $out = []; exec("{$php} " . escapeshellarg($script) . " {$staffId} {$since} " . $quiet, $out); return json_decode(implode("\n", $out), true); }; // Ausgangsstand $base = $mk('Altes Ticket', $d1); $r0 = $run($chef, 0); $check(is_array($r0) && $r0['latest'] === $base && $r0['tickets'] === [] && $r0['more'] === 0, 'Ausgangsstand liefert die letzte ID und keine Hinweise: ' . json_encode($r0)); $check(is_array($r0) && $r0['counts']['open'] === 1 && $r0['counts']['unassigned'] === 1 && $r0['counts']['mine'] === 0 && $r0['counts']['unanswered'] === 1 && $r0['counts']['overdue'] === 0, 'Kennzahlen stimmen'); // Neue Tickets $t1 = $mk('Login geht nicht', $d1, ['name' => 'Erika Muster']); $t2 = $mk('Technik-Frage', $d2); $mk('Spam', $d1, ['spam' => 1]); $mk('Live-Chat', $d1, ['source' => 'chat']); $mk('Zusammengeführt', $d1, ['merged' => $t1]); $r1 = $run($chef, $base); $ids = array_column((array) ($r1['tickets'] ?? []), 'id'); $check($ids === [$t1, $t2], 'Administrator sieht die beiden echten neuen Tickets (kein Spam, kein Chat, kein zusammengeführtes): ' . json_encode($ids)); $check(($r1['tickets'][0]['subject'] ?? '') === 'Login geht nicht' && ($r1['tickets'][0]['name'] ?? '') === 'Erika Muster' && ($r1['tickets'][0]['number'] ?? '') === 'LT-2' && ($r1['tickets'][1]['department'] ?? '') === 'Technik', 'Hinweis enthält Nummer, Betreff, Name und Abteilung'); $check(($r1['latest'] ?? 0) === $t2, 'latest ist die ID des neuesten sichtbaren Tickets (Spam, Chat und zusammengeführte zählen nicht)'); // Abteilungsrechte $r2 = $run($anna, $base); $check(array_column((array) ($r2['tickets'] ?? []), 'id') === [$t1], 'Mitarbeiter sehen nur Tickets ihrer Abteilung'); $check(($r2['counts']['open'] ?? -1) === 5 && ($r1['counts']['open'] ?? -1) === 6, 'Zahlen werden je Abteilung gezählt wie auf der Übersicht (Mitarbeiter 5, Administrator 6): ' . ($r2['counts']['open'] ?? '?') . '/' . ($r1['counts']['open'] ?? '?')); $ra = $run($anna, 0); $check(($ra['counts']['open'] ?? -1) === $r2['counts']['open'] && ($ra['counts']['open'] ?? -1) < ($run($chef, 0)['counts']['open'] ?? 0), 'Mitarbeiter zählen weniger als Administratoren'); // Nach dem Abruf nichts Neues $r3 = $run($chef, (int) $r1['latest']); $check(($r3['tickets'] ?? null) === [] && ($r3['more'] ?? -1) === 0, 'kein Hinweis, wenn nichts Neues kam'); // Mir zugewiesen und überfällig $mk('Zugewiesen', $d1, ['assigned' => $anna, 'due' => date('Y-m-d H:i:s', time() - 3600)]); $r4 = $run($anna, 0); $check(($r4['counts']['mine'] ?? 0) === 1 && ($r4['counts']['overdue'] ?? 0) === 1, 'zugewiesene und überfällige Tickets werden gezählt'); // Begrenzung auf 6 Hinweise $before = (int) $run($chef, 0)['latest']; for ($i = 0; $i < 9; $i++) $mk('Massenticket ' . $i, $d1); $r5 = $run($chef, $before); $check(count($r5['tickets'] ?? []) === 6 && ($r5['more'] ?? 0) === 3, 'höchstens 6 Hinweise, der Rest wird als „weitere“ gezählt: ' . count($r5['tickets'] ?? []) . ' / ' . ($r5['more'] ?? '?')); $check(($r5['tickets'][0]['subject'] ?? '') === 'Massenticket 0', 'älteste zuerst'); // Ohne Anmeldung $out = []; exec("{$php} " . escapeshellarg($script) . " 0 0 " . $quiet, $out); $check(json_decode(implode('', $out), true) === ['error' => 'login'], 'ohne Anmeldung: Fehlermeldung statt Daten'); // Einbindung $footer = (string) file_get_contents(dirname(__DIR__) . '/staff/_footer.php'); $check(str_contains($footer, '/assets/js/live-tickets.js'), 'Skript ist im Layout eingebunden'); $reports = (string) file_get_contents(dirname(__DIR__) . '/staff/reports.php'); $check(substr_count($reports, 'data-live-count=') === 5, 'fünf Kennzahlen der Übersicht aktualisieren sich live'); $js = (string) file_get_contents(dirname(__DIR__) . '/assets/js/live-tickets.js'); $check(str_contains($js, '/staff/live_tickets.php') && str_contains($js, 'textContent') && !str_contains($js, 'innerHTML'), 'Skript fragt den Abruf ab und setzt Inhalte nur als Text (kein innerHTML)'); $check(preg_match('/setTimeout\(poll, document\.hidden \? 30000 : 10000\)/', $js) === 1, 'Abruf alle 10 Sekunden (im Hintergrund-Tab alle 30)'); @unlink($script); @unlink($db); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo 'Live tickets checks passed.' . "\n";