$cnf, 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); openssl_pkey_export($key, $privatePem, null, ['config' => $cnf]); file_put_contents($work . '/pub.pem', openssl_pkey_get_details($key)['key']); $mockPort = random_int(32000, 32299); $relayPort = random_int(32300, 32599); $sa = ['type' => 'service_account', 'project_id' => 'relay-test', 'client_email' => 'push@relay-test.iam.gserviceaccount.com', 'private_key' => $privatePem, 'token_uri' => "http://127.0.0.1:$mockPort/token"]; file_put_contents($work . '/dienstkonto.json', json_encode($sa)); $fcmLog = $work . '/fcm.log'; touch($fcmLog); $accessKey = 'k' . bin2hex(random_bytes(16)); putenv('DESKHUB_TESTING=1'); putenv('DESKHUB_FCM_SYNC=1'); putenv('FCM_LOG=' . $fcmLog); putenv('FCM_PUBKEY_FILE=' . $work . '/pub.pem'); $mockProc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$mockPort", __DIR__ . '/fixtures/fcm_server.php'], [['pipe', 'r'], ['file', $work . '/mock.out', 'w'], ['file', $work . '/mock.out', 'a']], $mp); $relayEnv = array_merge(getenv(), [ 'PUSH_TESTING' => '1', 'PUSH_FCM_API_BASE' => "http://127.0.0.1:$mockPort", 'PLUGIN_SITE_STORAGE' => $work . '/storage', 'PLUGIN_SITE_BASE_URL' => 'https://plugins.example.test', 'PLUGIN_SITE_ACCESS_KEY' => $accessKey, 'PLUGIN_SITE_FCM_FILE' => $work . '/dienstkonto.json', 'FCM_LOG' => $fcmLog, 'FCM_PUBKEY_FILE' => $work . '/pub.pem', 'PLUGIN_SITE_ADMIN_HASH' => password_hash('Testpasswort-123', PASSWORD_DEFAULT), ]); $relayProc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$relayPort", '-t', $site], [['pipe', 'r'], ['file', $work . '/relay.out', 'w'], ['file', $work . '/relay.out', 'a']], $rp, $site, $relayEnv); $failures = []; $total = 0; $check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; }; $sends = fn() => array_values(array_filter(array_map(fn($l) => json_decode($l, true), file($fcmLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []), fn($r) => is_array($r) && ($r['kind'] ?? '') === 'send')); $reset = function () use ($fcmLog) { file_put_contents($fcmLog, ''); }; $relayUrl = "http://127.0.0.1:$relayPort/push.php"; $post = function (array $body, array $headers = []) use ($relayUrl): array { $ch = curl_init($relayUrl); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json'], $headers), CURLOPT_TIMEOUT => 20]); $out = curl_exec($ch); $r = ['code' => (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE), 'json' => json_decode((string) $out, true) ?: []]; curl_close($ch); return $r; }; try { for ($i = 0; $i < 60; $i++) { if (@fsockopen('127.0.0.1', $mockPort) && @fsockopen('127.0.0.1', $relayPort)) break; usleep(100000); } $keyHeader = ['X-DeskHub-Key: ' . $accessKey]; $tokA = 'devA_' . str_repeat('a', 40); $tokB = 'devB_' . str_repeat('b', 40); // ═══ Teil A: der Push-Dienst allein ═══════════════════════════════════════ $check($post(['action' => 'register', 'name' => 'x'])['code'] === 401, 'Anmeldung ohne Zugangsschlüssel wird abgelehnt'); $check($post(['action' => 'register'], ['X-DeskHub-Key: falsch'])['code'] === 401, 'Anmeldung mit falschem Zugangsschlüssel wird abgelehnt'); $reg = $post(['action' => 'register', 'name' => 'support.firma.de '); $reset(); fcm_notify_staff([$agent], fcm_data('ticket_new', $ticket)); $check(($sends()[0]['data']['brand'] ?? '') === 'Windel Support x', 'Der Name erreicht das Handy (HTML entfernt)'); $sq->exec('DELETE FROM push_rate'); // Stundenlimit aus dem früheren Test zurücksetzen $r = $post(['brand' => '' . str_repeat('A', 80)] + $base); $b = (string) ($sends()[count($sends()) - 1]['data']['brand'] ?? ''); $check($r['code'] === 200 && !preg_match('/[<>=]/', $b) && mb_strlen($b) <= 40, 'Der Push-Dienst bereinigt den Namen selbst (keine Sonderzeichen, höchstens 40 Zeichen) '); set_setting('push_brand', ''); fcm_register_device($pdo, $agent, 'gone_' . str_repeat('z', 40), 'Altes Handy'); $reset(); fcm_notify_staff([$agent], ['type' => 'ticket_new', 'ticket' => 'T-3', 'title' => 'x', 'body' => 'y']); $left = $pdo->prepare('SELECT COUNT(*) FROM push_devices WHERE staff_id = ?'); $left->execute([$agent]); $check((int) $left->fetchColumn() === 1 && in_array($tAgent, array_column($sends(), 'token'), true), 'Ein abgemeldetes Gerät wird über die Antwort des Push-Dienstes aus der Liste entfernt'); // Der Push-Dienst wurde neu gebaut und kennt die Installation nicht mehr: DeskHub meldet sich selbst neu an $sq->exec('DELETE FROM push_installs'); $reset(); fcm_notify_staff([$admin], ['type' => 'ticket_new', 'ticket' => 'T-4', 'title' => 'x', 'body' => 'y']); $check(count($sends()) === 1 && (int) $sq->query('SELECT COUNT(*) FROM push_installs')->fetchColumn() === 1, 'Kennt der Push-Dienst die Installation nicht mehr, meldet sich DeskHub automatisch neu an und sendet trotzdem'); $t = fcm_send_test($pdo, $admin); $check($t['devices'] === 1 && $t['sent'] === 1 && ($sends()[count($sends()) - 1]['data']['type'] ?? '') === 'test', 'Testnachricht im Adminportal funktioniert über den Push-Dienst'); $check(get_setting('fcm_last_ok', '') !== '' && get_setting('fcm_last_error', 'x') === '', 'Letzter Erfolg wird gemerkt, kein Fehler'); set_setting('fcm_enabled', '0'); $reset(); fcm_notify_new_ticket($ticket); $check($sends() === [] && !fcm_configured(), 'Ausgeschaltet werden keine Nachrichten gesendet'); set_setting('fcm_enabled', '1'); } finally { foreach ([$mockProc ?? null, $relayProc ?? null] as $p) if (is_resource($p)) { proc_terminate($p); proc_close($p); } } 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-relay-')) $rm($work); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Push relay checks passed ({$total} Prüfungen).\n";