253 lines
22 KiB
PHP
253 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Prüft die neuen Ticket-Funktionen über die echte Weboberfläche: Tastenkürzel-Skript und Bildvorschau auf den Seiten, verknüpfte Tickets,
|
|
* Antwort einplanen und abbrechen, Berichte per E-Mail im Bericht-Baukasten, Datenschutz-Werkzeuge im Kundenprofil (nur nach Einschalten).
|
|
*/
|
|
|
|
if (!extension_loaded('pdo_sqlite') || !function_exists('curl_init')) { echo "Ticket-Funktionen-Web-Test übersprungen: pdo_sqlite oder curl fehlt.\n"; exit(0); }
|
|
|
|
date_default_timezone_set('Europe/Berlin');
|
|
$root = dirname(__DIR__);
|
|
$work = sys_get_temp_dir() . '/deskhub-tfw-' . bin2hex(random_bytes(4));
|
|
$tmp = $work . '/inst';
|
|
mkdir($tmp, 0755, true);
|
|
$webPort = random_int(34000, 34499);
|
|
$skip = '#^(config/config\.php|database/[^/]+\.sqlite[^/]*|backups|uploads|tests|plugins/[^/]+|\.git)(/|$)#';
|
|
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST) as $f) {
|
|
$rel = str_replace(chr(92), '/', substr($f->getPathname(), strlen($root) + 1));
|
|
if (preg_match($skip, $rel) && !preg_match('#^(backups|uploads|plugins)/(\.htaccess|README\.md)$#', $rel)) continue;
|
|
if ($f->isDir()) { @mkdir($tmp . '/' . $rel, 0755, true); continue; }
|
|
@mkdir(dirname($tmp . '/' . $rel), 0755, true);
|
|
copy($f->getPathname(), $tmp . '/' . $rel);
|
|
}
|
|
@mkdir($tmp . '/uploads');
|
|
@mkdir($tmp . '/config');
|
|
file_put_contents($tmp . '/config/config.php', '<?php return ' . var_export([
|
|
'app_key' => bin2hex(random_bytes(32)),
|
|
'db' => ['driver' => 'sqlite', 'sqlite_path' => $tmp . '/database/deskhub.sqlite'],
|
|
'site' => ['name' => 'Test', 'url' => 'http://127.0.0.1', 'admin_email' => 'a@test.de', 'timezone' => 'Europe/Berlin'],
|
|
'uploads' => ['max_size_bytes' => 10485760, 'allowed_extensions' => ['png'], 'path' => $tmp . '/uploads'],
|
|
], true) . ';');
|
|
|
|
putenv('DESKHUB_CONFIG_JSON');
|
|
putenv('DESKHUB_API_ALLOW_INSECURE=1');
|
|
putenv('DESKHUB_SQLITE_PATH');
|
|
putenv('DESKHUB_TESTING');
|
|
require_once $root . '/database/sqlite_schema.php';
|
|
require_once $root . '/database/migrate.php';
|
|
$db = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite');
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
$db->exec('PRAGMA foreign_keys = ON');
|
|
$db->exec(convert_schema_to_sqlite((string) file_get_contents($root . '/database/schema.sql')));
|
|
run_migrations($db, true);
|
|
$now = date('Y-m-d H:i:s');
|
|
$dept = (int) $db->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
|
|
$prio = (int) $db->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
|
|
$db->exec('DELETE FROM staff');
|
|
$ins = $db->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)');
|
|
$ins->execute(['Chef', 'chef@test.de', password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), 'admin', $dept, $now]);
|
|
$chefId = (int) $db->lastInsertId();
|
|
$ins->execute(['Agent', 'agent@test.de', password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), 'agent', $dept, $now]);
|
|
$mk = $db->prepare('INSERT INTO tickets (ticket_number, subject, user_id, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)');
|
|
$db->exec("INSERT INTO users (name, email, password_hash, created_at) VALUES ('Erika Muster','erika@example.org','x','{$now}')");
|
|
$uid = (int) $db->lastInsertId();
|
|
$mk->execute(['W-1', 'Erstes Ticket', $uid, 'Erika Muster', 'erika@example.org', $dept, $prio, 'closed', 'web', $now, $now]);
|
|
$mk->execute(['W-2', 'Zweites Ticket', null, 'Anderer', 'anderer@example.org', $dept, $prio, 'open', 'web', $now, $now]);
|
|
$w1 = (int) $db->query("SELECT id FROM tickets WHERE ticket_number = 'W-1'")->fetchColumn();
|
|
$db->prepare("INSERT INTO ticket_threads (ticket_id, author_type, user_id, message, is_internal_note, created_at) VALUES (?,?,?,?,0,?)")->execute([$w1, 'customer', $uid, 'Hallo, ich brauche Hilfe', $now]);
|
|
$tid = (int) $db->lastInsertId();
|
|
$db->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')->execute([$tid, 'foto.png', 'nicht-da.png', 10, $now]);
|
|
$cfg = ['dimension' => 'department', 'metrics' => ['total'], 'from' => date('Y-m-d', strtotime('-30 days')), 'to' => date('Y-m-d'), 'filters' => [], 'sort' => 'label', 'desc' => false];
|
|
$db->prepare('INSERT INTO saved_reports (staff_id, name, config, is_shared, created_at, updated_at) VALUES (?,?,?,?,?,?)')->execute([$chefId, 'Mein Bericht', json_encode($cfg), 0, $now, $now]);
|
|
$db = null;
|
|
|
|
$failures = [];
|
|
$total = 0;
|
|
$check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; };
|
|
$webProc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$webPort", '-t', $tmp], [['pipe', 'r'], ['file', $work . '/web.out', 'w'], ['file', $work . '/web.out', 'a']], $wp, $tmp);
|
|
|
|
try {
|
|
for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $webPort)) break; usleep(100000); }
|
|
$pdo = function () use ($tmp): PDO {
|
|
$p = new PDO('sqlite:' . $tmp . '/database/deskhub.sqlite');
|
|
$p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$p->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
return $p;
|
|
};
|
|
$call = function (string $method, string $path, array $form, string $jar) use ($webPort): array {
|
|
$ch = curl_init("http://127.0.0.1:$webPort$path");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 60, CURLOPT_HEADER => true, CURLOPT_HTTPHEADER => ['Sec-Fetch-Mode: navigate'], CURLOPT_COOKIEJAR => $jar, CURLOPT_COOKIEFILE => $jar]);
|
|
if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($form)); }
|
|
$raw = (string) curl_exec($ch);
|
|
$s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
$hs = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
|
curl_close($ch);
|
|
return [$s, substr($raw, $hs), substr($raw, 0, $hs)];
|
|
};
|
|
$jar = tempnam(sys_get_temp_dir(), 'tfw');
|
|
[, $lp] = $call('GET', '/staff/login.php', [], $jar);
|
|
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $lp, $m);
|
|
$call('POST', '/staff/login.php', ['csrf_token' => $m[1] ?? '', 'email' => 'chef@test.de', 'password' => 'Sehr-Sicher-1234'], $jar);
|
|
$csrf = function (string $path) use ($call, $jar): string {
|
|
[, $p] = $call('GET', $path, [], $jar);
|
|
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $p, $mm);
|
|
return $mm[1] ?? '';
|
|
};
|
|
|
|
// Seiten und Skripte
|
|
[$s, $page] = $call('GET', '/staff/tickets.php', [], $jar);
|
|
$check($s === 200 && str_contains($page, '/assets/js/staff-shortcuts.js'), 'Ticketliste lädt das Tastenkürzel-Skript (HTTP ' . $s . ')');
|
|
[$s, $js] = $call('GET', '/assets/js/staff-shortcuts.js', [], $jar);
|
|
$check($s === 200 && str_contains($js, 'keydown'), 'Tastenkürzel-Skript wird ausgeliefert');
|
|
[$s, $js] = $call('GET', '/assets/js/ticket-lightbox.js', [], $jar);
|
|
$check($s === 200 && str_contains($js, 'data-lightbox'), 'Bildvorschau-Skript wird ausgeliefert');
|
|
|
|
[$s, $page] = $call('GET', '/staff/ticket_view.php?n=W-1', [], $jar);
|
|
$check($s === 200 && str_contains($page, 'data-lightbox="1"') && str_contains($page, '/assets/js/ticket-lightbox.js'), 'Ticket: Bildanhang hat die Vorschau-Markierung (HTTP ' . $s . ')');
|
|
$check(str_contains($page, 'Verknüpfte Tickets') && str_contains($page, 'Später senden') && str_contains($page, 'name="send_at"'), 'Ticket: Bereiche „Verknüpfte Tickets“ und „Später senden“ sind da');
|
|
|
|
// Verknüpfen
|
|
$t = $csrf('/staff/ticket_view.php?n=W-2');
|
|
[$s] = $call('POST', '/staff/ticket_view.php?n=W-2', ['csrf_token' => $t, 'action' => 'link_add', 'link_number' => '#W-1', 'link_type' => 'duplicate'], $jar);
|
|
$check($s === 302 && (int) $pdo()->query('SELECT COUNT(*) FROM ticket_links')->fetchColumn() === 1, 'Web: Tickets verknüpfen (HTTP ' . $s . ')');
|
|
[, $page] = $call('GET', '/staff/ticket_view.php?n=W-1', [], $jar);
|
|
$check(str_contains($page, 'Hat Duplikat') && str_contains($page, 'Zweites Ticket'), 'Web: die Gegenseite zeigt „Hat Duplikat“');
|
|
[, $page] = $call('GET', '/staff/ticket_view.php?n=W-2', [], $jar);
|
|
$check(str_contains($page, 'Doppelt zu') && str_contains($page, 'Erstes Ticket'), 'Web: das Ticket selbst zeigt „Doppelt zu“');
|
|
$lid = (int) $pdo()->query('SELECT id FROM ticket_links')->fetchColumn();
|
|
$t = $csrf('/staff/ticket_view.php?n=W-2');
|
|
[$s] = $call('POST', '/staff/ticket_view.php?n=W-2', ['csrf_token' => $t, 'action' => 'link_remove', 'link_id' => $lid], $jar);
|
|
$check((int) $pdo()->query('SELECT COUNT(*) FROM ticket_links')->fetchColumn() === 0, 'Web: Verknüpfung entfernen');
|
|
|
|
// Antwort einplanen
|
|
$t = $csrf('/staff/ticket_view.php?n=W-2');
|
|
$future = date('Y-m-d\TH:i', time() + 7200);
|
|
[$s] = $call('POST', '/staff/ticket_view.php?n=W-2', ['csrf_token' => $t, 'action' => 'schedule_reply', 'message' => '<p>Bis später</p>', 'send_at' => $future, 'set_status' => 'pending'], $jar);
|
|
$row = $pdo()->query("SELECT * FROM scheduled_replies WHERE state = 'pending'")->fetch();
|
|
$check($s === 302 && $row && str_contains($row['message'], 'Bis später') && $row['set_status'] === 'pending', 'Web: Antwort einplanen (HTTP ' . $s . ')');
|
|
[, $page] = $call('GET', '/staff/ticket_view.php?n=W-2', [], $jar);
|
|
$check(str_contains($page, 'Geplante Antworten') && str_contains($page, 'Bis später'), 'Web: geplante Antwort steht im Ticket');
|
|
$check((int) $pdo()->query("SELECT COUNT(*) FROM ticket_threads t JOIN tickets k ON k.id = t.ticket_id WHERE k.ticket_number = 'W-2'")->fetchColumn() === 0, 'Web: bis zum Zeitpunkt wird nichts im Verlauf angelegt');
|
|
$t = $csrf('/staff/ticket_view.php?n=W-2');
|
|
[$s] = $call('POST', '/staff/ticket_view.php?n=W-2', ['csrf_token' => $t, 'action' => 'schedule_reply', 'message' => '<p>x</p>', 'send_at' => date('Y-m-d\TH:i', time() - 3600)], $jar);
|
|
$check((int) $pdo()->query("SELECT COUNT(*) FROM scheduled_replies")->fetchColumn() === 1, 'Web: Zeitpunkt in der Vergangenheit legt nichts an');
|
|
$t = $csrf('/staff/ticket_view.php?n=W-2');
|
|
$call('POST', '/staff/ticket_view.php?n=W-2', ['csrf_token' => $t, 'action' => 'cancel_scheduled_reply', 'scheduled_id' => (int) $row['id']], $jar);
|
|
$check($pdo()->query('SELECT state FROM scheduled_replies')->fetchColumn() === 'cancelled', 'Web: Abbrechen');
|
|
|
|
// Berichte per E-Mail
|
|
[$s, $page] = $call('GET', '/staff/report_builder.php', [], $jar);
|
|
$check($s === 200 && str_contains($page, 'Versand per E-Mail (Zeitplan)') && str_contains($page, 'name="recipients"'), 'Bericht-Baukasten zeigt den Versand per E-Mail (HTTP ' . $s . ')');
|
|
$t = $csrf('/staff/report_builder.php');
|
|
$rid = (int) $pdo()->query('SELECT id FROM saved_reports')->fetchColumn();
|
|
[$s] = $call('POST', '/staff/report_builder.php', ['csrf_token' => $t, 'action' => 'schedule_add', 'report_id' => $rid, 'recipients' => 'leitung@example.org', 'frequency' => 'weekly', 'weekday' => 1, 'send_time' => '08:00', 'period' => 'last_week'], $jar);
|
|
$sch = $pdo()->query('SELECT * FROM report_schedules')->fetch();
|
|
$check($s === 302 && $sch && $sch['period'] === 'last_week' && $sch['recipients'] === 'leitung@example.org', 'Web: Zeitplan anlegen (HTTP ' . $s . ')');
|
|
$check((int) $sch['only_if_data'] === 1 && (int) $sch['skip_unchanged'] === 1, 'Web: ohne Angabe gelten „nur bei Daten“ und „nur bei Änderung“');
|
|
[, $page] = $call('GET', '/staff/report_builder.php', [], $jar);
|
|
$check(str_contains($page, 'Nur senden, wenn der Bericht Daten enthält') && str_contains($page, 'Nur bei Änderung'), 'Web: die Schalter stehen im Formular und in der Liste');
|
|
$t = $csrf('/staff/report_builder.php');
|
|
$call('POST', '/staff/report_builder.php', ['csrf_token' => $t, 'action' => 'schedule_flag', 'schedule_id' => (int) $sch['id'], 'flag' => 'only_if_data'], $jar);
|
|
$check((int) $pdo()->query('SELECT only_if_data FROM report_schedules')->fetchColumn() === 0, 'Web: Option „nur bei Daten“ lässt sich umschalten');
|
|
$t = $csrf('/staff/report_builder.php');
|
|
$call('POST', '/staff/report_builder.php', ['csrf_token' => $t, 'action' => 'schedule_flag', 'schedule_id' => (int) $sch['id'], 'flag' => 'schedule_id; DROP TABLE x'], $jar);
|
|
$check((int) $pdo()->query('SELECT COUNT(*) FROM report_schedules')->fetchColumn() === 1, 'Web: eine ungültige Option wird abgewiesen');
|
|
[, $page] = $call('GET', '/staff/report_builder.php', [], $jar);
|
|
$check(str_contains($page, 'leitung@example.org') && str_contains($page, 'Letzte Kalenderwoche'), 'Web: Zeitplan steht in der Liste');
|
|
$t = $csrf('/staff/report_builder.php');
|
|
$call('POST', '/staff/report_builder.php', ['csrf_token' => $t, 'action' => 'schedule_toggle', 'schedule_id' => (int) $sch['id']], $jar);
|
|
$check((int) $pdo()->query('SELECT enabled FROM report_schedules')->fetchColumn() === 0, 'Web: Zeitplan pausieren');
|
|
$t = $csrf('/staff/report_builder.php');
|
|
$call('POST', '/staff/report_builder.php', ['csrf_token' => $t, 'action' => 'schedule_delete', 'schedule_id' => (int) $sch['id']], $jar);
|
|
$check((int) $pdo()->query('SELECT COUNT(*) FROM report_schedules')->fetchColumn() === 0, 'Web: Zeitplan löschen');
|
|
|
|
// Datenschutz: standardmäßig aus
|
|
[, $page] = $call('GET', '/staff/customer_profile.php?id=' . $uid, [], $jar);
|
|
$check(!str_contains($page, 'Kundendaten löschen') && !str_contains($page, 'Alle Daten exportieren'), 'Datenschutz: ohne Einschalten keine Knöpfe');
|
|
$t = $csrf('/staff/customer_profile.php?id=' . $uid);
|
|
[$s, $body, $hdr] = $call('POST', '/staff/customer_profile.php?id=' . $uid, ['csrf_token' => $t, 'action' => 'gdpr_export'], $jar);
|
|
$check(!str_contains($hdr, 'application/json'), 'Datenschutz: Export ist ohne Einschalten gesperrt');
|
|
[, $page] = $call('GET', '/staff/settings.php?tab=datenschutz', [], $jar);
|
|
$check(str_contains($page, 'name="gdpr_tools_enabled"'), 'Einstellungen: Schalter für die Datenschutz-Werkzeuge');
|
|
$t = $csrf('/staff/settings.php?tab=datenschutz');
|
|
$call('POST', '/staff/settings.php?tab=datenschutz', ['csrf_token' => $t, 'action' => 'save_privacy', 'retention_months' => 24, 'gdpr_tools_enabled' => 1], $jar);
|
|
$check($pdo()->query("SELECT setting_value FROM settings WHERE setting_key = 'gdpr_tools_enabled'")->fetchColumn() === '1', 'Einstellungen: Schalter wird gespeichert');
|
|
[, $page] = $call('GET', '/staff/customer_profile.php?id=' . $uid, [], $jar);
|
|
$check(str_contains($page, 'Kundendaten löschen') && str_contains($page, 'Alle Daten exportieren'), 'Datenschutz: nach dem Einschalten sind die Knöpfe da');
|
|
$t = $csrf('/staff/customer_profile.php?id=' . $uid);
|
|
[$s, $body, $hdr] = $call('POST', '/staff/customer_profile.php?id=' . $uid, ['csrf_token' => $t, 'action' => 'gdpr_export'], $jar);
|
|
$json = json_decode($body, true);
|
|
$check($s === 200 && str_contains(strtolower($hdr), 'attachment') && ($json['kunde']['email'] ?? '') === 'erika@example.org' && ($json['tickets'][0]['nummer'] ?? '') === 'W-1', 'Datenschutz: Export liefert die JSON-Datei (HTTP ' . $s . ')');
|
|
$check(str_contains((string) $pdo()->query("SELECT GROUP_CONCAT(action, '|') FROM admin_audit_log")->fetchColumn(), 'Kundendaten exportiert'), 'Datenschutz: Export steht im Audit-Log');
|
|
[$s] = $call('POST', '/staff/customer_profile.php?id=' . $uid, ['csrf_token' => $t, 'action' => 'gdpr_erase', 'confirm_email' => 'falsch@example.org'], $jar);
|
|
$check((int) $pdo()->query("SELECT COUNT(*) FROM users WHERE id = $uid")->fetchColumn() === 1, 'Datenschutz: ohne richtige Bestätigung wird nichts gelöscht');
|
|
[$s] = $call('POST', '/staff/customer_profile.php?id=' . $uid, ['csrf_token' => $t, 'action' => 'gdpr_erase', 'confirm_email' => 'erika@example.org'], $jar);
|
|
$check($s === 302 && (int) $pdo()->query("SELECT COUNT(*) FROM users WHERE id = $uid")->fetchColumn() === 0 && $pdo()->query("SELECT requester_name FROM tickets WHERE ticket_number = 'W-1'")->fetchColumn() === 'Gelöscht', 'Datenschutz: Löschen mit Bestätigung (HTTP ' . $s . ')');
|
|
// Zähler „braucht Aufmerksamkeit“ über die Mitarbeiter-API (Chrome-Erweiterung und Windows-Programm)
|
|
$apiJson = function (string $method, string $path, ?array $body, string $token) use ($webPort): array {
|
|
$ch = curl_init("http://127.0.0.1:$webPort$path");
|
|
$h = ['Content-Type: application/json'] + ($token !== '' ? [1 => 'Authorization: Bearer ' . $token] : []);
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => $h]);
|
|
if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body ?? [])); }
|
|
$raw = (string) curl_exec($ch);
|
|
$st = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
curl_close($ch);
|
|
return [$st, json_decode($raw, true) ?? []];
|
|
};
|
|
[, $lr] = $apiJson('POST', '/api/staff/v1/login.php', ['email' => 'agent@test.de', 'password' => 'Sehr-Sicher-1234', 'deviceName' => 'T'], '');
|
|
$tok = (string) ($lr['token'] ?? '');
|
|
$check($tok !== '', 'API: Anmeldung des Mitarbeiters');
|
|
$n2 = date('Y-m-d H:i:s');
|
|
$pdo()->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)')
|
|
->execute(['W-ATT', 'Zähler-Test', 'Kunde', 'z@example.org', $dept, $prio, 'open', 'web', $n2, $n2]);
|
|
$attId = (int) $pdo()->query("SELECT id FROM tickets WHERE ticket_number = 'W-ATT'")->fetchColumn();
|
|
[$st, $a1] = $apiJson('GET', '/api/staff/v1/activity.php', null, $tok);
|
|
$check($st === 200 && in_array($attId, $a1['attention']['unassigned'] ?? [], true) && ($a1['attention']['unassignedCount'] ?? 0) >= 1, 'API: Aktivität liefert das neue Ticket im Zähler (HTTP ' . $st . ')');
|
|
[$st, $a2] = $apiJson('GET', '/api/staff/v1/activity.php?since=' . urlencode($n2), null, $tok);
|
|
$check(in_array($attId, $a2['attention']['unassigned'] ?? [], true), 'API: auch mit „since“ kommt der echte Stand (für ältere Clients zusätzlich newReplies/newUnassigned)');
|
|
$check(array_key_exists('newReplies', $a2) && array_key_exists('newUnassigned', $a2), 'API: die alten Felder bleiben erhalten');
|
|
$apiJson('GET', '/api/staff/v1/ticket.php?number=W-ATT', null, $tok);
|
|
[, $a3] = $apiJson('GET', '/api/staff/v1/activity.php', null, $tok);
|
|
$check(!in_array($attId, $a3['attention']['unassigned'] ?? [], true) && !in_array($attId, $a3['attention']['mine'] ?? [], true), 'API: nach dem Öffnen des Tickets ist es aus dem Zähler verschwunden');
|
|
|
|
// Meine Woche, Erinnerung (Einstellung) und Screenshot-Einfügen im Antwortfeld
|
|
[$st, $page] = $call('GET', '/staff/my_week.php', [], $jar);
|
|
$check($st === 200 && str_contains($page, 'Gelöste Tickets') && str_contains($page, 'Ø Zeit bis zur ersten Antwort') && str_contains($page, 'Gelöst pro Tag'), 'Meine Woche: Seite mit Kennzahlen (HTTP ' . $st . ')');
|
|
$check(str_contains($page, 'href="/staff/my_week.php"') && str_contains($page, 'Letzte Woche') && str_contains($page, 'Letzter Monat'), 'Meine Woche: Menüpunkt und Zeiträume');
|
|
[$st, $page] = $call('GET', '/staff/my_week.php?p=lastmonth', [], $jar);
|
|
$check($st === 200 && str_contains($page, 'Letzter Monat'), 'Meine Woche: anderer Zeitraum');
|
|
[$st] = $call('GET', '/staff/my_week.php?p=' . urlencode('x<script>'), [], $jar);
|
|
$check($st === 200, 'Meine Woche: ungültiger Zeitraum wird abgefangen');
|
|
[, $page] = $call('GET', '/staff/settings.php?tab=allgemein', [], $jar);
|
|
$check(str_contains($page, 'name="wait_reminder_hours"') && str_contains($page, 'name="wait_reminder_enabled"'), 'Einstellungen: Erinnerung an Bearbeiter');
|
|
$t = $csrf('/staff/settings.php?tab=allgemein');
|
|
$call('POST', '/staff/settings.php?tab=allgemein', ['csrf_token' => $t, 'action' => 'save_system', 'helpdesk_status' => 'online', 'default_department_id' => (string) $dept, 'wait_reminder_enabled' => 1, 'wait_reminder_hours' => 6, 'wait_reminder_email' => 1, 'staff_session_timeout' => 30], $jar);
|
|
$cfgNow = $pdo()->query("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'wait_reminder_%'")->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
$check(($cfgNow['wait_reminder_hours'] ?? '') === '6' && ($cfgNow['wait_reminder_email'] ?? '') === '1' && ($cfgNow['wait_reminder_enabled'] ?? '') === '1', 'Einstellungen: Werte werden gespeichert: ' . json_encode($cfgNow));
|
|
[, $page] = $call('GET', '/staff/ticket_view.php?n=W-1', [], $jar);
|
|
$check(preg_match('/<textarea[^>]*id="message-field"[^>]*data-image-upload-url="\/staff\/upload_signature_image\.php"/', $page) === 1, 'Ticket: Antwortfeld erlaubt Bilder aus der Zwischenablage');
|
|
|
|
// Nicht-Admin
|
|
$jarA = tempnam(sys_get_temp_dir(), 'tfa');
|
|
[, $lp] = $call('GET', '/staff/login.php', [], $jarA);
|
|
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $lp, $m);
|
|
$call('POST', '/staff/login.php', ['csrf_token' => $m[1] ?? '', 'email' => 'agent@test.de', 'password' => 'Sehr-Sicher-1234'], $jarA);
|
|
[, $page] = $call('GET', '/staff/customer_profile.php?email=' . urlencode('anderer@example.org'), [], $jarA);
|
|
$check(!str_contains($page, 'Kundendaten löschen'), 'Datenschutz: normale Mitarbeiter sehen die Knöpfe nie');
|
|
@unlink($jarA);
|
|
@unlink($jar);
|
|
} finally {
|
|
if (is_resource($webProc)) { proc_terminate($webProc); proc_close($webProc); }
|
|
$rm = function (string $d) use (&$rm): void { foreach (glob($d . '/{,.}*', GLOB_BRACE) ?: [] as $e) { $b = basename($e); if ($b === '.' || $b === '..') continue; is_dir($e) ? $rm($e) : @unlink($e); } @rmdir($d); };
|
|
$rm($work);
|
|
}
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
|
|
exit(1);
|
|
}
|
|
echo "Ticket features web checks passed ({$total} Prüfungen).\n";
|