Files
deskhub/DeskHub-Webseite/tests/ticket_done_web.php
T
2026-11-05 21:22:03 +00:00

158 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Prüft über die echte Weboberfläche und die Mitarbeiter-API, dass gelöste und geschlossene Tickets als erledigt gelten:
* keine Fälligkeit und kein „überfällig“ in Liste, Ticketansicht und App-Schnittstelle; das Schließen räumt den Merker auf.
*/
if (!extension_loaded('pdo_sqlite') || !function_exists('curl_init')) { echo "Abmelde-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-done-' . bin2hex(random_bytes(4));
$tmp = $work . '/inst';
mkdir($tmp, 0755, true);
$webPort = random_int(35500, 35999);
$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')));
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,?)');
foreach ([['Chef', 'chef@test.de', 'admin'], ['Eva', 'eva@test.de', 'agent'], ['Max', 'max@test.de', 'agent']] as [$n, $e, $r]) {
$ins->execute([$n, $e, password_hash('Sehr-Sicher-1234', PASSWORD_DEFAULT), $r, $dept, $now]);
}
$evaId = (int) $db->query("SELECT id FROM staff WHERE email = 'eva@test.de'")->fetchColumn();
$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, ?string $bearer = null, ?array $json = null) use ($webPort): array {
$ch = curl_init("http://127.0.0.1:$webPort$path");
$h = ['Sec-Fetch-Mode: navigate'];
if ($json !== null) $h[] = 'Content-Type: application/json';
if ($bearer) $h[] = 'Authorization: Bearer ' . $bearer;
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 60, CURLOPT_HEADER => true, CURLOPT_HTTPHEADER => $h]);
if ($jar !== '') { curl_setopt($ch, CURLOPT_COOKIEJAR, $jar); curl_setopt($ch, CURLOPT_COOKIEFILE, $jar); }
if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $json !== null ? json_encode($json) : 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)];
};
$login = function (string $email) use ($call): string {
$jar = tempnam(sys_get_temp_dir(), 'sow');
[, $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' => $email, 'password' => 'Sehr-Sicher-1234'], $jar);
return $jar;
};
$csrf = function (string $path, string $jar) use ($call): string {
[, $p] = $call('GET', $path, [], $jar);
preg_match('/name="csrf_token" value="([a-f0-9]+)"/', $p, $mm);
return $mm[1] ?? '';
};
$loggedIn = function (string $jar) use ($call): bool {
[$s, $b] = $call('GET', '/staff/profile.php?tab=security', [], $jar);
return $s === 200 && str_contains($b, 'Von allen Geräten abmelden');
};
$apiToken = function (string $email) use ($call): string {
[, $b] = $call('POST', '/api/staff/v1/login.php', [], '', null, ['email' => $email, 'password' => 'Sehr-Sicher-1234', 'deviceName' => 'Test-Gerät']);
return (string) (json_decode($b, true)['token'] ?? '');
};
$apiOk = fn(string $tok) => $call('GET', '/api/staff/v1/me.php', [], '', $tok)[0];
$mk = function (string $n, string $status, array $o = []) use ($pdo, $now, $dept): int {
$p = $pdo();
$prio = (int) $p->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
$p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, resolution_due_at, first_response_due_at, is_overdue, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
->execute([$n, 'Betreff ' . $n, 'K', 'k@example.org', $dept, $prio, $status, 'web', date('Y-m-d H:i:s', time() - 9 * 3600), date('Y-m-d H:i:s', time() - 20 * 3600), $o['overdue'] ?? 0, date('Y-m-d H:i:s', time() - 80 * 3600), $now]);
return (int) $p->lastInsertId();
};
$mk('Z-OPEN', 'open', ['overdue' => 1]);
$mk('Z-CLOSED', 'closed', ['overdue' => 1]); // alter Zustand: überfällig-Merker noch gesetzt
$mk('Z-RESOLVED', 'resolved');
$jar = $login('chef@test.de');
$rowOf = function (string $html, string $num): string {
return preg_match('#<tr[^>]*>(?:(?!</tr>).)*' . preg_quote($num, '#') . '(?:(?!</tr>).)*</tr>#s', $html, $m) ? $m[0] : '';
};
[, $list] = $call('GET', '/staff/tickets.php?tab=all', [], $jar);
$open = $rowOf($list, 'Z-OPEN'); $closed = $rowOf($list, 'Z-CLOSED'); $resolved = $rowOf($list, 'Z-RESOLVED');
$check($open !== '' && $closed !== '' && $resolved !== '', 'Alle drei Tickets stehen in der Liste');
$check(str_contains($open, 'überfällig'), 'Liste: das offene Ticket zeigt „überfällig“');
$check(!str_contains($closed, 'überfällig') && !str_contains($closed, ' Std') && str_contains($closed, '–'), 'Liste: geschlossenes Ticket zeigt keine Fälligkeit und kein „überfällig“');
$check(!str_contains($resolved, 'überfällig') && !str_contains($resolved, ' Std') && str_contains($resolved, '–'), 'Liste: gelöstes Ticket genauso');
foreach (['Z-CLOSED' => 'geschlossen', 'Z-RESOLVED' => 'gelöst'] as $num => $label) {
[, $view] = $call('GET', '/staff/ticket_view.php?n=' . $num, [], $jar);
$check(!str_contains($view, 'überfällig') && str_contains($view, '✓ erledigt'), "Ticketansicht ($label): kein „überfällig“, die Lösung steht als erledigt");
}
[, $view] = $call('GET', '/staff/ticket_view.php?n=Z-OPEN', [], $jar);
$check(str_contains($view, 'überfällig'), 'Ticketansicht (offen): „überfällig“ bleibt');
$tok = $apiToken('chef@test.de');
[, $b] = $call('GET', '/api/staff/v1/tickets.php?status=open,closed,resolved&perPage=50', [], '', $tok);
$items = json_decode($b, true)['tickets'] ?? json_decode($b, true)['items'] ?? [];
$by = [];
foreach ($items as $it) $by[$it['number'] ?? $it['ticketNumber'] ?? ''] = $it;
$check(isset($by['Z-OPEN'], $by['Z-CLOSED'], $by['Z-RESOLVED']), 'App-Schnittstelle liefert alle drei Tickets');
$check(!empty($by['Z-OPEN']['isOverdue']) && str_contains((string) $by['Z-OPEN']['dueLabel'], 'überfällig'), 'App-Schnittstelle: offenes Ticket überfällig');
$check(empty($by['Z-CLOSED']['isOverdue']) && array_key_exists('dueLabel', $by['Z-CLOSED']) && $by['Z-CLOSED']['dueLabel'] === null, 'App-Schnittstelle: geschlossenes Ticket ohne Fälligkeit und ohne „überfällig“');
$check(empty($by['Z-RESOLVED']['isOverdue']) && array_key_exists('dueLabel', $by['Z-RESOLVED']) && $by['Z-RESOLVED']['dueLabel'] === null, 'App-Schnittstelle: gelöstes Ticket genauso');
// Echtes Schließen über die Weboberfläche räumt auf
$t = $csrf('/staff/ticket_view.php?n=Z-OPEN', $jar);
[, $v0] = $call('GET', '/staff/ticket_view.php?n=Z-OPEN', [], $jar);
$call('POST', '/staff/ticket_view.php?n=Z-OPEN', ['csrf_token' => $t, 'action' => 'update_props', 'status' => 'closed', 'resolution_code' => 'fixed', 'resolution_note' => 'ok', 'priority_id' => (string) $pdo()->query("SELECT priority_id FROM tickets WHERE ticket_number = 'Z-OPEN'")->fetchColumn(), 'department_id' => (string) $dept, 'assigned_staff_id' => ''], $jar);
$st = $pdo()->query("SELECT status, is_overdue FROM tickets WHERE ticket_number = 'Z-OPEN'")->fetch();
$check($st['status'] === 'closed' && (int) $st['is_overdue'] === 0, 'Nach dem Schließen in der Oberfläche ist das Ticket geschlossen und nicht mehr überfällig');
@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 done web checks passed ({$total} Prüfungen).\n";