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

167 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft die Schnittstellen der Apps zu Zufriedenheitsabfrage, Statusseite, Ticket-Vorlagen und Cc-Empfängern.
*/
date_default_timezone_set('Europe/Berlin'); // wie in der Test-Installation
$root = dirname(__DIR__);
$work = sys_get_temp_dir() . '/deskhub-apicss-' . bin2hex(random_bytes(4));
$tmp = $work . '/inst';
mkdir($tmp, 0755, true);
$webPort = random_int(36500, 36999);
$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_SQLITE_PATH');
putenv('DESKHUB_TESTING');
putenv('DESKHUB_API_ALLOW_INSECURE=1');
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')));
$mig = 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();
$db->exec('DELETE FROM staff');
$ins = $db->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)');
$ids = [];
foreach ([['Chef', 'chef@test.de', 'admin'], ['Agent', 'agent@test.de', 'agent']] as [$n, $e, $r]) {
$ins->execute([$n, $e, password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $r, $dept, $now]);
$ids[$n] = (int) $db->lastInsertId();
}
$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;
};
$api = function (string $method, string $path, ?array $json = null, ?string $bearer = null) use ($webPort): array {
$ch = curl_init("http://127.0.0.1:$webPort$path");
$h = ['Content-Type: application/json'];
if ($bearer) $h[] = 'Authorization: Bearer ' . $bearer;
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => $h, CURLOPT_CUSTOMREQUEST => $method]);
if ($json !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
$b = (string) curl_exec($ch);
$s = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$s, json_decode($b, true) ?? $b];
};
$p = $pdo();
$nowS = date('Y-m-d H:i:s');
$p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)')
->execute(['T9001', 'API-Test', 'Kunde', 'kunde@test.de', $dept, 2, 'open', 'web', $nowS, $nowS]);
$tid = (int) $p->lastInsertId();
$login = function (string $email) use ($api): string {
[, $lg] = $api('POST', '/api/staff/v1/login.php', ['email' => $email, 'password' => 'Sehr-Sicher-1234', 'deviceName' => 'Test']);
return (string) ($lg['token'] ?? '');
};
$admin = $login('chef@test.de');
$agent = $login('agent@test.de');
$check($admin !== '' && $agent !== '', 'Anmeldung von Administrator und Mitarbeiter');
// Cc-Empfänger
[$s, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'cc@test.de', 'name' => 'Cc Person'], $agent);
$check($s === 200 && count($r['collaborators']) === 1 && $r['collaborators'][0]['email'] === 'cc@test.de' && $r['collaborators'][0]['name'] === 'Cc Person', 'Cc hinzufügen');
[$s, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'CC@test.de'], $agent);
$check(count($r['collaborators']) === 1, 'dieselbe Adresse nicht doppelt');
[$s, ] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'add', 'email' => 'kaputt'], $agent);
$check($s === 422, 'ungültige Adresse wird abgelehnt');
[$s, $r] = $api('GET', '/api/staff/v1/ticket_collaborators.php?number=T9001', null, $agent);
$cid = (int) ($r['collaborators'][0]['id'] ?? 0);
$check($s === 200 && $cid > 0, 'Cc-Liste abrufen');
[$s, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
$check(count($r['ticket']['collaborators']) === 1 && $r['ticket']['followers'] === [] && $r['ticket']['csat'] === null, 'Ticket enthält Cc, Beobachter und (noch keine) Bewertung');
$api('PATCH', '/api/staff/v1/ticket_update.php', ['number' => 'T9001', 'following' => true], $agent);
[, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
$check(count($r['ticket']['followers']) === 1 && $r['ticket']['followers'][0]['name'] === 'Agent', 'Beobachter erscheinen am Ticket');
[, $r] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'T9001', 'action' => 'remove', 'id' => $cid], $agent);
$check($r['collaborators'] === [], 'Cc entfernen');
[$s, ] = $api('POST', '/api/staff/v1/ticket_collaborators.php', ['number' => 'GIBTS-NICHT', 'action' => 'add', 'email' => 'a@b.de'], $agent);
$check($s === 404 || $s === 403, 'unbekanntes Ticket: ' . $s);
// Bewertung am Ticket und Auswertung
$p->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, department_id, rating, comment, sent_at, answered_at) VALUES (?,?,?,?,?,?,?,?)')
->execute([$tid, bin2hex(random_bytes(20)), $ids['Agent'], $dept, 5, 'Top', $nowS, $nowS]);
[, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9001', null, $agent);
$check($r['ticket']['csat']['rating'] === 5 && $r['ticket']['csat']['comment'] === 'Top', 'Bewertung am Ticket');
[$s, $r] = $api('GET', '/api/staff/v1/csat.php?days=7', null, $agent);
$check($s === 200 && $r['enabled'] === false && $r['stats']['answered'] === 1 && $r['stats']['avg'] == 5 && count($r['stats']['dist']) === 5 && $r['stats']['byStaff'][0]['name'] === 'Agent', 'Auswertung über die Schnittstelle');
// Vorlagen
[$s, $r] = $api('GET', '/api/staff/v1/ticket_templates.php', null, $agent);
$check($s === 200 && $r['templates'] === [], 'noch keine Vorlagen');
$p->prepare("INSERT INTO ticket_templates (name, subject, body, department_id, is_public, is_active, sort_order, created_at) VALUES ('Neuer Mitarbeiter','Neu: ','Name:',?,1,1,1,?)")->execute([$dept, $nowS]);
$p->prepare("INSERT INTO ticket_templates (name, subject, is_public, is_active, sort_order, created_at) VALUES ('Inaktiv','x',1,0,2,?)")->execute([$nowS]);
[, $r] = $api('GET', '/api/staff/v1/ticket_templates.php', null, $agent);
$check(count($r['templates']) === 1 && $r['templates'][0]['name'] === 'Neuer Mitarbeiter' && $r['templates'][0]['departmentId'] === $dept && $r['templates'][0]['body'] === 'Name:', 'Vorlagen für die Apps (nur aktive)');
[$s, ] = $api('GET', '/api/staff/v1/ticket_templates.php');
$check($s === 401, 'Vorlagen nur mit Anmeldung');
// Statusseite
[$s, $r] = $api('GET', '/api/staff/v1/status.php', null, $agent);
$check($s === 200 && $r['canManage'] === false && $r['status']['enabled'] === false && $r['status']['components'] === [], 'Statusseite lesen (Mitarbeiter)');
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_enabled', 'enabled' => true], $agent);
$check($s === 403, 'Mitarbeiter dürfen die Statusseite nicht ändern');
$p->prepare("INSERT INTO status_components (name, status, sort_order, created_at) VALUES ('Webseite','operational',1,?)")->execute([$nowS]);
$comp = (int) $p->lastInsertId();
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_enabled', 'enabled' => true], $admin);
$check($s === 200 && $r['status']['enabled'] === true && $r['canManage'] === true, 'Administrator schaltet die Statusseite ein');
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'create_incident', 'title' => 'Portal langsam', 'body' => 'Wir prüfen das.', 'status' => 'investigating', 'componentId' => $comp, 'componentState' => 'degraded'], $admin);
$check($s === 200 && $r['status']['overall']['key'] === 'degraded' && count($r['status']['incidents']) === 1 && $r['status']['components'][0]['status'] === 'degraded', 'Meldung veröffentlichen');
$inc = (int) ($r['status']['incidents'][0]['id'] ?? 0);
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'create_incident', 'title' => ''], $admin);
$check($s === 422, 'Meldung ohne Titel');
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_component', 'id' => $comp, 'status' => 'outage'], $admin);
$check($r['status']['overall']['key'] === 'outage', 'Dienst-Zustand ändern');
[$s, ] = $api('POST', '/api/staff/v1/status.php', ['action' => 'set_component', 'id' => $comp, 'status' => 'kaputt'], $admin);
$check($s === 422, 'ungültiger Zustand');
[$s, $r] = $api('POST', '/api/staff/v1/status.php', ['action' => 'update_incident', 'id' => $inc, 'status' => 'resolved', 'body' => 'Behoben.'], $admin);
$check($r['status']['overall']['key'] === 'operational' && $r['status']['incidents'][0]['status'] === 'resolved' && count($r['status']['incidents'][0]['updates']) === 2, 'Meldung beheben setzt den Dienst zurück');
[, $r] = $api('GET', '/api/staff/v1/status.php', null, $agent);
$check($r['status']['enabled'] === true && $r['status']['title'] === 'Systemstatus', 'Mitarbeiter sehen den Stand');
} finally {
if (isset($webProc) && is_resource($webProc)) { proc_terminate($webProc); proc_close($webProc); }
}
usleep(300000);
$rm = function (string $d) use (&$rm) {
foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") && !is_link("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); }
@rmdir($d);
};
if (str_starts_with($work, sys_get_temp_dir() . '/deskhub-apicss-')) $rm($work);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo "API CSAT, Status und Vorlagen checks passed ({$total} Prüfungen).\n";