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', ' 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'); // E-Mail-Abruf von Hand: ohne Postfach eine verständliche Meldung (kein Absturz) [$s, $r] = $api('POST', '/api/staff/v1/mail_fetch.php', [], $agent); $check($s === 200 && $r['ok'] === false && $r['available'] === false && str_contains((string) $r['message'], 'kein aktives Postfach'), 'Abruf ohne Postfach: Meldung: ' . json_encode($r)); [$s, ] = $api('POST', '/api/staff/v1/mail_fetch.php', []); $check($s === 401, 'Abruf ohne Anmeldung: ' . $s); [$s, ] = $api('GET', '/api/staff/v1/mail_fetch.php', null, $agent); $check($s === 405, 'Abruf nur per POST: ' . $s); // Nachladen: nur für E-Mail-Tickets [$s, $r] = $api('POST', '/api/staff/v1/ticket_mail_reload.php', ['number' => 'T9001'], $agent); $check($s === 422 && ($r['error']['code'] ?? '') === 'EMAIL_TICKET_REQUIRED', 'Web-Ticket hat keine Original-E-Mail: ' . json_encode($r)); [$s, $r] = $api('POST', '/api/staff/v1/ticket_mail_reload.php', [], $agent); $check($s === 422 && ($r['error']['code'] ?? '') === 'TICKET_NUMBER_REQUIRED', 'Ticketnummer ist Pflicht'); $p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)') ->execute(['T9002', 'Mail-Test', 'Kunde', 'kunde@test.de', $dept, 2, 'open', 'email', $nowS, $nowS]); $mailTid = (int) $p->lastInsertId(); $p->exec("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES ({$mailTid},'customer','Hallo',0,'{$nowS}')"); $thId = (int) $p->lastInsertId(); [$s, $r] = $api('POST', '/api/staff/v1/ticket_mail_reload.php', ['number' => 'T9002'], $agent); $check($s === 422 && ($r['error']['code'] ?? '') === 'MAIL_RELOAD_FAILED' && str_contains((string) $r['error']['message'], 'keine Original-E-Mail'), 'E-Mail-Ticket ohne gespeicherte Mail: verständliche Meldung: ' . json_encode($r)); // Eingebettete Bilder stehen nicht in der Anhangliste der Ticket-Schnittstelle $p->exec("INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at, content_id, is_inline) VALUES ({$thId},'Bericht.docx','a.docx',100,'{$nowS}',NULL,0)"); $p->exec("INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at, content_id, is_inline) VALUES ({$thId},'logo.png','b.png',100,'{$nowS}','logo@x',1)"); [$s, $r] = $api('GET', '/api/staff/v1/ticket.php?number=T9002', null, $agent); $names = []; foreach ($r['ticket']['messages'] ?? $r['ticket']['threads'] ?? [] as $m) foreach ($m['attachments'] ?? [] as $a) $names[] = $a['name'] ?? $a['originalName'] ?? ''; $check($s === 200 && $names === ['Bericht.docx'], 'nur der echte Anhang steht in der Liste, nicht das eingebettete Bild: ' . json_encode($names) . ' ' . substr(json_encode($r), 0, 300)); } 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 Mail actions checks passed ({$total} Prüfungen).\n";