$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";