Files
deskhub/wordpress-plugin/tests/run.php
T
2026-11-01 11:17:41 +00:00

519 lines
44 KiB
PHP

<?php
declare(strict_types=1);
/**
* Tests des WordPress-Plugins "DeskHub Tickets" mit einem Nachbau der WordPress-Funktionen (wp-stub.php).
* php wordpress-plugin/tests/run.php
* Gegen einen echten DeskHub-Server (Testinstallation) zusätzlich:
* DESKHUB_URL=http://127.0.0.1:8097 DESKHUB_KEY=dh_… php wordpress-plugin/tests/run.php
* Ohne DESKHUB_URL werden die Prüfungen gegen den echten Server übersprungen.
*/
require __DIR__ . '/wp-stub.php';
require dirname(__DIR__) . '/deskhub-tickets/deskhub-tickets.php';
do_action('plugins_loaded'); // wie WordPress: Plugin startet
$failures = [];
$total = 0;
$check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; };
function reset_wp(array $opts = []): void
{
$GLOBALS['wp_options'] = ['date_format' => 'd.m.Y', 'time_format' => 'H:i'];
$GLOBALS['wp_transients'] = [];
$GLOBALS['wp_http_log'] = [];
$GLOBALS['wp_http_handler'] = null;
$GLOBALS['wp_user'] = ['id' => 0, 'caps' => [], 'name' => '', 'email' => ''];
$GLOBALS['wp_settings_errors'] = [];
$GLOBALS['wp_enqueued'] = [];
$_POST = []; $_GET = []; $_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REMOTE_ADDR'] = '203.0.113.5';
if ($opts !== null) {
$GLOBALS['wp_options']['deskhub_tickets_options'] = array_merge([
'server_url' => 'https://support.example.de', 'api_key' => 'dh_geheimer_schluessel_123', 'min_seconds' => 0, 'rate_limit_per_hour' => 5,
], $opts);
}
}
function run_form_post(array $post): ?string
{
$_POST = $post;
$_SERVER['REQUEST_METHOD'] = 'POST';
try { Deskhub_Tickets_Form::handle_submit(); } catch (WpRedirect $r) { return $r->location; }
return null;
}
/** Holt den Zustand (Fehler/Erfolg), den handle_submit hinterlassen hat, aus der Weiterleitungsadresse. */
function state_from(?string $location): ?array
{
if (!$location || !preg_match('/[?&]dht=([a-f0-9]{16})/', $location, $m)) return null;
return $GLOBALS['wp_transients']['dht_state_' . $m[1]][0] ?? null;
}
function render_form(array $atts = [], ?string $location = null): string
{
$_GET = [];
if ($location && preg_match('/[?&]dht=([a-f0-9]{16})/', $location, $m)) $_GET['dht'] = $m[1];
return Deskhub_Tickets_Form::render_ticket_form($atts);
}
/** Ein gültig aussehendes Formular wie vom Plugin erzeugt: unterschriebene Angaben und Zeitstempel aus der Ausgabe lesen. */
function valid_post(string $html, array $over = []): array
{
preg_match('/name="dht_cfg" value="([^"]*)"/', $html, $c);
preg_match('/name="dht_sig" value="([^"]*)"/', $html, $s);
preg_match('/name="dht_ts" value="([^"]*)"/', $html, $t);
preg_match('/name="dht_back" value="([^"]*)"/', $html, $b);
return array_merge([
'action' => 'deskhub_ticket_submit', 'dht_cfg' => html_entity_decode($c[1] ?? ''), 'dht_sig' => $s[1] ?? '', 'dht_ts' => $t[1] ?? '', 'dht_back' => html_entity_decode($b[1] ?? ''),
'dht_website' => '', 'dht_name' => 'Erika Muster', 'dht_email' => 'erika@kunde.example', 'dht_subject' => 'Rechnung fehlt', 'dht_message' => "Hallo,\nbitte schicken Sie mir die Rechnung.",
], $over);
}
function ok_handler(array &$seen = null): callable
{
return function (string $url, array $args) use (&$seen) {
if (str_contains($url, 'form=1') || str_contains($url, 'topics=1')) return wp_stub_response(422, ['error' => ['code' => 'MISSING_PARAMS', 'message' => 'x']]); // wie ein älterer Server
$seen[] = ['url' => $url, 'args' => $args];
if (($args['method'] ?? 'GET') === 'POST') return wp_stub_response(201, ['number' => '261101-0001', 'status' => 'open']);
return wp_stub_response(200, ['ticket' => ['number' => '261101-0001', 'subject' => 'Rechnung', 'status' => 'answered', 'createdAt' => '2026-11-01 10:00:00', 'updatedAt' => '2026-11-01 11:00:00',
'messages' => [['author' => 'customer', 'message' => 'Hallo <script>alert(1)</script>', 'createdAt' => '2026-11-01 10:00:00'], ['author' => 'staff', 'message' => "Erledigt.\nViele Grüße", 'createdAt' => '2026-11-01 11:00:00']]]]);
};
}
// ── Server-Adresse ─────────────────────────────────────────────────────────
reset_wp();
foreach ([
['support.example.de', 'https://support.example.de'],
[' https://support.example.de/ ', 'https://support.example.de'],
['https://example.de/helpdesk/', 'https://example.de/helpdesk'],
['http://127.0.0.1:8080', 'http://127.0.0.1:8080'],
['http://localhost', 'http://localhost'],
] as [$in, $want]) {
$check(Deskhub_Tickets_Client::normalize_url($in) === $want, "Adresse '$in' wird zu '$want'");
}
foreach (['', 'http://support.example.de', 'https://user:pw@example.de', 'https://example.de/?x=1', 'https://example.de/#a', 'ftp://example.de', 'http://localhost.evil.de'] as $bad) {
$check(is_wp_error(Deskhub_Tickets_Client::normalize_url($bad)), "Adresse '$bad' wird abgelehnt");
}
// ── Nicht eingerichtet ──────────────────────────────────────────────────────
reset_wp(['server_url' => '', 'api_key' => '']);
$check(Deskhub_Tickets_Form::render_ticket_form() === '', 'Ohne Einrichtung sieht ein Besucher nichts');
$GLOBALS['wp_user']['caps'] = ['manage_options'];
$check(str_contains(Deskhub_Tickets_Form::render_ticket_form(), 'noch nicht eingerichtet'), 'Ein Administrator sieht den Einrichtungshinweis');
// ── Formular wird angezeigt ─────────────────────────────────────────────────
reset_wp(['consent_text' => 'Ich habe die <a href="/datenschutz" onclick="x()">Datenschutzerklärung</a> gelesen.<script>bad()</script>']);
$html = render_form();
foreach (['name="dht_name"', 'name="dht_email"', 'name="dht_subject"', 'name="dht_message"', 'name="dht_website"', 'name="dht_consent"', 'name="dht_ts"', 'name="dht_sig"'] as $f) {
$check(str_contains($html, $f), "Formular enthält $f");
}
$check(!str_contains($html, 'dh_geheimer_schluessel_123') && !str_contains($html, 'support.example.de'), 'Weder API-Schlüssel noch Server-Adresse stehen im Formular');
$check(str_contains($html, 'href="/datenschutz"') && !str_contains($html, 'onclick') && !str_contains($html, '<script'), 'Einwilligungstext: Link bleibt, Skripte und Ereignisse werden entfernt');
$check(in_array('deskhub-tickets', $GLOBALS['wp_enqueued'], true), 'Stylesheet wird eingebunden');
$GLOBALS['wp_user'] = ['id' => 7, 'caps' => [], 'name' => 'Lena', 'email' => 'lena@example.de'];
$check(str_contains(render_form(), 'value="lena@example.de"'), 'Angemeldete Besucher: E-Mail-Adresse ist vorausgefüllt');
$GLOBALS['wp_user'] = ['id' => 0, 'caps' => [], 'name' => '', 'email' => ''];
// ── Absenden: Erfolg und Inhalt der Anfrage ─────────────────────────────────
reset_wp(['department_id' => 2, 'priority_id' => 1]);
$seen = [];
$GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form();
$loc = run_form_post(valid_post($html, ['dht_departmentId' => '99', 'dht_priority' => '9', 'dht_topic' => '77']));
$state = state_from($loc);
$check($state && !empty($state['ok']) && $state['number'] === '261101-0001', 'Absenden gelingt, Ticketnummer wird gemerkt');
$check(str_starts_with((string) $loc, 'https://wp.test/kontakt/?dht=') && str_ends_with((string) $loc, '#deskhub-ticket'), 'Danach geht es auf die Seite des Formulars zurück');
$req = $seen[0] ?? null;
$sent = json_decode((string) ($req['args']['body'] ?? ''), true);
$check($req && ($req['args']['method'] ?? '') === 'POST' && str_ends_with($req['url'], '/api/public/v1/tickets.php'), 'Anfrage geht per POST an die Ticket-API');
$check(($req['args']['headers']['X-API-Key'] ?? '') === 'dh_geheimer_schluessel_123', 'Der API-Schlüssel wird im Header gesendet');
$check(($sent['departmentId'] ?? null) === 2 && ($sent['priorityId'] ?? null) === 1, 'Abteilung und Priorität kommen aus den Einstellungen, nicht aus dem Formular');
$check(!isset($sent['helpTopicId']), 'Ein vom Besucher erfundenes Thema wird nicht übernommen');
$check(str_contains((string) ($sent['message'] ?? ''), "bitte schicken Sie mir die Rechnung.") && str_contains((string) $sent['message'], 'https://wp.test/kontakt/'), 'Nachricht enthält den Text und die Herkunftsseite');
$check(render_form([], $loc) !== '' && str_contains(render_form([], $loc), 'Vielen Dank') === false, 'Der Erfolgszustand wird nur einmal angezeigt (danach wieder das leere Formular)');
$loc2 = run_form_post(valid_post($html));
$check(str_contains(render_form([], $loc2), '261101-0001'), 'Erfolgsseite zeigt die Ticketnummer');
// ── Manipulation und Roboter ────────────────────────────────────────────────
reset_wp();
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form();
$bad = valid_post($html, ['dht_cfg' => base64_encode('{"d":99,"t":0,"p":9,"o":[]}')]);
$st = state_from(run_form_post($bad));
$check($st && !empty($st['errors']) && !$seen, 'Veränderte Abteilung/Priorität (falsche Unterschrift) wird abgelehnt, nichts gesendet');
$st = state_from(run_form_post(valid_post($html, ['dht_website' => 'http://spam.example'])));
$check($st && !empty($st['ok']) && $st['number'] === '' && !$seen, 'Falle-Feld ausgefüllt: scheinbar Erfolg, aber nichts angelegt');
$st = state_from(run_form_post(valid_post($html, ['dht_ts' => 'abc'])));
$check($st && !empty($st['errors']) && !$seen, 'Gefälschter Zeitstempel wird abgelehnt');
reset_wp(['min_seconds' => 10]);
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$st = state_from(run_form_post(valid_post(render_form())));
$check($st && !empty($st['errors']) && !$seen, 'Zu schnell abgesendet (unter der Mindestzeit) wird abgelehnt');
// ── Pflichtfelder, Einwilligung, Themenauswahl ──────────────────────────────
reset_wp(['consent_text' => 'Ich stimme zu.']);
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form();
$st = state_from(run_form_post(valid_post($html, ['dht_name' => '', 'dht_email' => 'kein-email', 'dht_subject' => '', 'dht_message' => ' '])));
$check($st && count($st['errors']) === 5 && !$seen, 'Fehlende Pflichtfelder und fehlende Einwilligung ergeben fünf Fehler (nichts gesendet)');
$st = state_from(run_form_post(valid_post($html, ['dht_message' => 'Hallo <b>Welt</b> <script>alert(1)</script>', 'dht_consent' => '1'])));
$sent = json_decode((string) ($seen[0]['args']['body'] ?? ''), true);
$check(!str_contains((string) ($sent['message'] ?? ''), '<'), 'HTML in der Nachricht wird entfernt');
$check($st && !empty($st['ok']), 'Mit Einwilligung klappt es');
reset_wp(['consent_text' => '']);
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form(['topics' => 'Rechnung:3,Technik:4', 'department' => '5']);
$check(str_contains($html, '<option value="3"') && str_contains($html, 'Technik'), 'Themenliste wird als Auswahl angezeigt');
$st = state_from(run_form_post(valid_post($html, ['dht_topic' => '9'])));
$check($st && !empty($st['errors']) && !$seen, 'Ein Thema, das nicht in der Liste steht, wird abgelehnt');
$st = state_from(run_form_post(valid_post($html, [])));
$check($st && !empty($st['errors']) && !$seen, 'Ohne gewähltes Thema wird abgelehnt, wenn eine Auswahl angeboten wurde');
$st = state_from(run_form_post(valid_post($html, ['dht_topic' => '4'])));
$sent = json_decode((string) ($seen[0]['args']['body'] ?? ''), true);
$check(($sent['helpTopicId'] ?? null) === 4 && ($sent['departmentId'] ?? null) === 5, 'Gewähltes Thema und Abteilung aus dem Shortcode werden gesendet');
$check(count(Deskhub_Tickets_Form::parse_topics('A:1, B :2,kaputt,:3,C:x')) === 2, 'Themenangaben: Fehlerhafte Einträge werden übersprungen');
// ── Mengenbegrenzung ────────────────────────────────────────────────────────
reset_wp(['rate_limit_per_hour' => 3]);
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form();
$results = [];
for ($i = 0; $i < 5; $i++) { $st = state_from(run_form_post(valid_post($html))); $results[] = !empty($st['ok']); }
$check($results === [true, true, true, false, false] && count($seen) === 3, 'Je Besucher höchstens 3 Tickets pro Stunde (4. und 5. werden abgelehnt)');
$_SERVER['REMOTE_ADDR'] = '203.0.113.99';
$st = state_from(run_form_post(valid_post($html)));
$check(!empty($st['ok']), 'Ein anderer Besucher ist nicht betroffen');
$check(!array_filter(array_keys($GLOBALS['wp_transients']), fn($k) => str_contains($k, '203.0.113')), 'In der Datenbank stehen keine IP-Adressen');
// ── Fehler des Servers ──────────────────────────────────────────────────────
reset_wp();
$cases = [
[422, 'INVALID_EMAIL', 'gültige E-Mail'], [429, 'RATE_LIMITED', 'Minute'], [403, 'BANNED', 'nicht angenommen'], [500, '', 'nicht erreichbar'],
];
foreach ($cases as [$code, $api, $needle]) {
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response($code, $api ? ['error' => ['code' => $api, 'message' => 'Interne Meldung dh_geheimer_schluessel_123']] : 'Fehlerseite');
$st = state_from(run_form_post(valid_post(render_form())));
$msg = $st['errors'][0] ?? '';
$check(str_contains($msg, $needle) && !str_contains($msg, 'dh_geheimer'), "Serverfehler $code ($api): freundliche Meldung ohne Interna");
}
$GLOBALS['wp_http_handler'] = fn($u, $a) => new WP_Error('http_request_failed', 'cURL error 6: Could not resolve host');
$st = state_from(run_form_post(valid_post(render_form())));
$check(str_contains($st['errors'][0] ?? '', 'nicht erreichbar') && !str_contains($st['errors'][0], 'cURL'), 'Server nicht erreichbar: Meldung ohne technische Einzelheiten');
$check(($st['values']['message'] ?? '') !== '', 'Bei einem Fehler bleibt die Nachricht im Formular erhalten');
// ── Statusabfrage ───────────────────────────────────────────────────────────
reset_wp();
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST = ['dht_status_submit' => '1', 'dht_number' => '261101-0001', 'dht_status_email' => 'erika@kunde.example'];
$out = Deskhub_Tickets_Form::render_status_form();
$check(str_contains($out, 'Beantwortet') && str_contains($out, 'Erledigt.<br />') && str_contains($out, 'Support'), 'Status und Verlauf werden angezeigt');
$check(!str_contains($out, '<script>') && str_contains($out, '&lt;script&gt;'), 'Texte aus dem Verlauf werden maskiert');
$q = $seen[0]['url'] ?? '';
$check(str_contains($q, 'number=261101-0001') && str_contains($q, 'email=erika%40kunde.example'), 'Abfrage mit Ticketnummer und codierter E-Mail-Adresse');
$_POST = ['dht_status_submit' => '1', 'dht_number' => '../etc/passwd', 'dht_status_email' => 'erika@kunde.example'];
$seen = [];
$out = Deskhub_Tickets_Form::render_status_form();
$check(!$seen && str_contains($out, 'dht-error'), 'Ungültige Ticketnummer wird gar nicht erst an den Server geschickt');
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response(404, ['error' => ['code' => 'NOT_FOUND', 'message' => 'Ticket nicht gefunden.']]);
$_POST = ['dht_status_submit' => '1', 'dht_number' => '261101-0009', 'dht_status_email' => 'erika@kunde.example'];
$check(str_contains(Deskhub_Tickets_Form::render_status_form(), 'nichts gefunden'), 'Unbekanntes Ticket: neutrale Meldung');
reset_wp();
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response(404, ['error' => ['code' => 'NOT_FOUND', 'message' => 'x']]);
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST = ['dht_status_submit' => '1', 'dht_number' => '261101-0009', 'dht_status_email' => 'erika@kunde.example'];
$blocked = 0;
for ($i = 0; $i < 14; $i++) { if (str_contains(Deskhub_Tickets_Form::render_status_form(), 'Zu viele Abfragen')) $blocked++; }
$check($blocked === 4, 'Statusabfragen sind begrenzt (10 je 10 Minuten): das Durchprobieren von Nummern wird gebremst');
// ── Kundenformular aus DeskHub (eigene Felder, Formular der Abteilung) ──────
function form_fixture(): array
{
$f = function (int $id, string $label, string $type, array $o = []) {
return array_merge(['id' => $id, 'label' => $label, 'type' => $type, 'required' => false, 'options' => [], 'help' => '', 'helpHtml' => '', 'width' => 'full', 'section' => 'extra', 'sectionTitle' => '', 'radioInline' => false], $o);
};
return ['helpTopicId' => 3, 'departmentId' => 2, 'ownForm' => true, 'title' => 'Störungsmeldung', 'intro' => 'Bitte beschreiben Sie die Störung.',
'subject' => ['show' => false, 'label' => 'Betreff'], 'message' => ['show' => false, 'label' => 'Beschreibung', 'hint' => ''], 'submitLabel' => 'Störung melden',
'fields' => [
$f(11, 'Gerät', 'select', ['required' => true, 'options' => ['Drucker', 'Router'], 'width' => 'half', 'section' => 'topic', 'helpHtml' => 'Siehe <a href="https://x.example/hilfe" target="_blank" rel="noopener noreferrer">Hilfe</a><script>x()</script>']),
$f(12, 'Seit wann', 'date', ['section' => 'extra', 'sectionTitle' => 'Zusätzliche Angaben']),
$f(13, 'Anzahl', 'number'),
$f(14, 'Dringend', 'checkbox', ['required' => true, 'helpHtml' => 'Ich bestätige die <a href="https://x.example/agb" rel="noopener noreferrer">AGB</a>']),
$f(15, 'Bereich', 'radio', ['options' => ['Büro', 'Lager'], 'radioInline' => true, 'section' => 'contact_top']),
$f(16, 'Details', 'textarea', ['section' => 'issue']),
]];
}
/** Ein DeskHub mit Formular-Schnittstelle: liefert Formulare je Thema, protokolliert Ticket-Anfragen. */
function new_server(array &$log, array $forms, ?array $postReply = null): callable
{
return function (string $url, array $args) use (&$log, $forms, $postReply) {
$method = $args['method'] ?? 'GET';
if ($method === 'GET' && str_contains($url, 'topics=1')) {
return wp_stub_response(200, ['topics' => [['id' => 3, 'name' => 'Störung', 'path' => 'Technik / Störung', 'depth' => 1, 'parentId' => null, 'departmentId' => 2]]]);
}
if ($method === 'GET' && str_contains($url, 'form=1')) {
$log[] = ['kind' => 'form', 'url' => $url];
preg_match('/helpTopicId=(\d+)/', $url, $m);
$tid = (int) ($m[1] ?? 0);
return isset($forms[$tid]) ? wp_stub_response(200, ['form' => $forms[$tid]]) : wp_stub_response(422, ['error' => ['code' => 'INVALID_TOPIC', 'message' => 'x']]);
}
$log[] = ['kind' => 'post', 'url' => $url, 'body' => json_decode((string) ($args['body'] ?? ''), true)];
return $postReply ?? wp_stub_response(201, ['number' => '261101-0007', 'status' => 'open']);
};
}
$posts = fn(array $log) => array_values(array_filter($log, fn($e) => $e['kind'] === 'post'));
$forms = fn(array $log) => array_values(array_filter($log, fn($e) => $e['kind'] === 'form'));
reset_wp(['department_id' => 9]);
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture()]);
$html = render_form(['topic' => '3']);
$check(str_contains($html, 'Störungsmeldung') && str_contains($html, 'Bitte beschreiben Sie die Störung.') && str_contains($html, 'Störung melden</button>'), 'DeskHub-Formular: Titel, Einleitung und Knopf-Text kommen von DeskHub');
$check(str_contains($html, 'name="dht_f[11]"') && str_contains($html, '<option value="Router"') && str_contains($html, 'name="dht_f[12]"') && str_contains($html, 'type="date"') && str_contains($html, 'type="number"')
&& str_contains($html, 'type="checkbox" id="dht-f-3-14"') && str_contains($html, 'type="radio"') && str_contains($html, '<textarea id="dht-f-3-16"'), 'DeskHub-Formular: alle Feldtypen werden angezeigt');
$check(preg_match('/<select id="dht-f-3-11"[^>]* required/', $html) === 1 && str_contains($html, 'dht-req'), 'DeskHub-Formular: Pflichtfelder sind gekennzeichnet');
$check(str_contains($html, 'dht-w-half') && str_contains($html, 'dht-inline'), 'DeskHub-Formular: Breite und nebeneinander stehende Optionen');
$check(str_contains($html, 'href="https://x.example/hilfe"') && str_contains($html, 'href="https://x.example/agb"') && !str_contains($html, '<script'), 'DeskHub-Formular: Hinweistexte mit Links, aber ohne Skripte');
$check(strpos($html, 'name="dht_f[15]"') < strpos($html, 'name="dht_name"'), 'DeskHub-Formular: Feld „über den Kontaktdaten“ steht vor dem Namen');
$check(strpos($html, 'name="dht_f[11]"') > strpos($html, 'name="dht_email"') && strpos($html, 'name="dht_f[16]"') > strpos($html, 'name="dht_message"') && strpos($html, 'name="dht_f[12]"') > strpos($html, 'name="dht_f[16]"'), 'DeskHub-Formular: Felder stehen an ihrem Platz (Thema, nach der Beschreibung, Zusätzliche Angaben am Ende)');
$check(str_contains($html, '<h4 class="dht-group">Zusätzliche Angaben</h4>'), 'DeskHub-Formular: Überschrift „Zusätzliche Angaben“');
$check(preg_match('/class="dht-subject-wrap" hidden/', $html) === 1 && preg_match('/name="dht_subject"[^>]* disabled/', $html) === 1 && preg_match('/name="dht_message"[^>]*disabled/', $html) === 1, 'DeskHub-Formular: ohne Betreff und Beschreibung (Felder ausgeblendet)');
$check(count($forms($log)) === 1 && str_contains($forms($log)[0]['url'], 'helpTopicId=3') && !str_contains($forms($log)[0]['url'], 'departmentId='), 'Formular wird mit dem Thema geholt, die Standard-Abteilung der Einstellungen wird bei einem Thema nicht mitgeschickt');
render_form(['topic' => '3']);
$check(count($forms($log)) === 1, 'Formular wird zwischengespeichert (zweiter Aufruf fragt DeskHub nicht noch einmal)');
// Absenden mit DeskHub-Formular
$sub = fn(array $over = []) => valid_post(render_form(['topic' => '3']), array_merge(['dht_subject' => '', 'dht_message' => '', 'dht_f' => ['11' => 'Router', '12' => '2026-10-31', '14' => '1', '15' => 'Lager', '16' => 'Seit heute früh']], $over));
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture()]);
$st = state_from(run_form_post($sub()));
$p = $posts($log)[0]['body'] ?? [];
$check($st && !empty($st['ok']) && $st['number'] === '261101-0007', 'DeskHub-Formular: Absenden gelingt');
$check(($p['fields'] ?? null) == ['11' => 'Router', '12' => '2026-10-31', '13' => '', '14' => '1', '15' => 'Lager', '16' => 'Seit heute früh'], 'Feldwerte gehen mit ihrer Nummer an DeskHub');
$check(!array_key_exists('subject', $p) && !isset($p['departmentId']) && ($p['helpTopicId'] ?? 0) === 3, 'Ohne Betrefffeld wird kein Betreff gesendet (DeskHub setzt ihn), Abteilung bestimmt das Thema');
$check(str_contains((string) ($p['message'] ?? ''), "Gerät: Router\nSeit wann: 2026-10-31\nDringend: Ja\nBereich: Lager\nDetails: Seit heute früh") && str_contains((string) $p['message'], 'Gesendet über das Formular'), 'Ohne Beschreibungsfeld besteht der Text aus den Angaben der Felder, dazu die Herkunftsseite');
// Prüfungen
$cases = [
['Gerät fehlt', ['dht_f' => ['14' => '1']], 'Gerät'],
['Gerät nicht angeboten', ['dht_f' => ['11' => 'Fax', '14' => '1']], 'angebotenen'],
['Datum kaputt', ['dht_f' => ['11' => 'Router', '12' => '31.02.2026', '14' => '1']], 'Datum'],
['Zahl kaputt', ['dht_f' => ['11' => 'Router', '13' => 'viele', '14' => '1']], 'Zahl'],
['Pflicht-Checkbox fehlt', ['dht_f' => ['11' => 'Router']], 'bestätigen'],
];
foreach ($cases as [$what, $over, $needle]) {
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture()]);
$st = state_from(run_form_post($sub($over)));
$check($st && !empty($st['errors']) && str_contains(implode(' ', $st['errors']), $needle) && !$posts($log), "DeskHub-Formular: $what wird abgelehnt, bevor etwas gesendet wird");
}
// Fehler von DeskHub selbst (INVALID_FIELD) wird freundlich weitergegeben
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture()], wp_stub_response(422, ['error' => ['code' => 'INVALID_FIELD', 'message' => 'Bitte füllen Sie das Feld „Gerät“ aus.']]));
$st = state_from(run_form_post($sub()));
$check(($st['errors'][0] ?? '') === 'Bitte füllen Sie das Feld „Gerät“ aus.', 'Feldfehler von DeskHub wird dem Besucher gezeigt');
$check(($st['values']['fields']['11'] ?? '') === 'Router', 'Nach einem Fehler bleiben die Angaben in den Feldern erhalten');
// Nach einem Fehler: Felder sind wieder ausgefüllt
$again = render_form(['topic' => '3'], run_form_post($sub(['dht_f' => ['11' => 'Router', '14' => '1', '16' => 'Text']])) );
$check(str_contains($again, '<option value="Router" selected="selected">') && str_contains($again, 'checked="checked"'), 'Wieder angezeigtes Formular enthält die bisherigen Angaben');
// Mehrere Themen
reset_wp();
$second = form_fixture();
$second['helpTopicId'] = 4; $second['ownForm'] = false; $second['title'] = ''; $second['intro'] = ''; $second['subject'] = ['show' => true, 'label' => 'Betreff']; $second['message'] = ['show' => true, 'label' => 'Beschreibung', 'hint' => ''];
$second['submitLabel'] = 'Ticket erstellen'; $second['fields'] = [$second['fields'][1]];
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture(), 4 => $second]);
$html = render_form(['topics' => 'Störung:3,Allgemein:4']);
$check(count($forms($log)) === 2, 'Mehrere Themen: ein Formular je Thema wird geholt');
$check(str_contains($html, 'data-topic="3"') && str_contains($html, 'data-topic="4"') && !str_contains($html, 'dht-topic-active'), 'Mehrere Themen: Felder je Thema in eigenen Blöcken, noch keines gewählt');
$check(preg_match('/id="dht-f-3-11"[^>]* disabled/', $html) === 1 && preg_match('/id="dht-f-4-12"[^>]* disabled/', $html) === 1, 'Mehrere Themen: Felder nicht gewählter Themen sind gesperrt (werden nicht abgeschickt)');
$json = [];
preg_match('/data-dht-forms="([^"]*)"/', $html, $mm);
$json = json_decode(html_entity_decode($mm[1] ?? ''), true);
$check(($json[3]['subjectShow'] ?? null) === false && ($json[4]['subjectShow'] ?? null) === true && ($json[4]['submitLabel'] ?? '') === 'Ticket absenden' && ($json[3]['submitLabel'] ?? '') === 'Störung melden', 'Mehrere Themen: Texte je Thema für das Umschalten (Standardformular mit Texten des Plugins)');
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture(), 4 => $second]);
$st = state_from(run_form_post(valid_post($html, ['dht_topic' => '4', 'dht_f' => ['12' => '2026-10-31', '11' => 'Router']])));
$p = $posts($log)[0]['body'] ?? [];
$check(!empty($st['ok']) && ($p['helpTopicId'] ?? 0) === 4 && ($p['subject'] ?? '') === 'Rechnung fehlt' && array_map('strval', array_keys($p['fields'] ?? [])) === ['12'], 'Mehrere Themen: nur die Felder des gewählten Themas werden geprüft und gesendet');
// Ältere DeskHub-Version: einfaches Formular, Hinweis nur für Administratoren
reset_wp();
$seen = []; $GLOBALS['wp_http_handler'] = ok_handler($seen);
$html = render_form(['topic' => '3']);
$check(str_contains($html, 'name="dht_subject"') && !str_contains($html, 'dht_f[') && !str_contains($html, 'kennt die Formular-Schnittstelle'), 'Ältere DeskHub-Version: einfaches Formular, kein Hinweis für Besucher');
$GLOBALS['wp_user']['caps'] = ['manage_options'];
$check(str_contains(render_form(['topic' => '3']), 'kennt die Formular-Schnittstelle'), 'Ältere DeskHub-Version: Administratoren sehen den Hinweis');
$GLOBALS['wp_user']['caps'] = [];
run_form_post(valid_post($html));
$p = json_decode((string) ($seen[0]['args']['body'] ?? ''), true);
$check(!array_key_exists('fields', $p ?? []) && ($p['subject'] ?? '') === 'Rechnung fehlt', 'Ältere DeskHub-Version: Ticket wird wie bisher ohne Felder angelegt');
// Themenliste in den Einstellungen
reset_wp();
$GLOBALS['wp_user']['caps'] = ['manage_options'];
$log = [];
$GLOBALS['wp_http_handler'] = new_server($log, [3 => form_fixture()]);
ob_start(); Deskhub_Tickets_Settings::render(); $page = (string) ob_get_clean();
$check(str_contains($page, '<select name="deskhub_tickets_options[topic_id]"') && str_contains($page, 'Technik / Störung (3)'), 'Einstellungen: Hilfethema als Auswahl aus DeskHub');
$GLOBALS['wp_http_handler'] = ok_handler($seen);
ob_start(); Deskhub_Tickets_Settings::render(); $page = (string) ob_get_clean();
$check(str_contains($page, 'type="number" min="0" class="small-text" name="deskhub_tickets_options[topic_id]"'), 'Einstellungen: bei älterem DeskHub bleibt das Zahlenfeld');
$GLOBALS['wp_user']['caps'] = [];
// ── Verbindungstest ─────────────────────────────────────────────────────────
reset_wp();
foreach ([[404, true, 'Tickets lesen'], [403, true, 'keine Tickets lesen'], [401, false, 'abgelehnt'], [500, false, '500']] as [$code, $ok, $needle]) {
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response($code, ['error' => ['code' => 'X', 'message' => 'm']]);
$r = Deskhub_Tickets_Client::from_settings()->test_connection();
$check($r['ok'] === $ok && str_contains($r['message'], $needle), "Verbindungstest bei HTTP $code");
}
$GLOBALS['wp_http_handler'] = fn($u, $a) => new WP_Error('x', 'y');
$check(Deskhub_Tickets_Client::from_settings()->test_connection()['ok'] === false, 'Verbindungstest: Server nicht erreichbar');
$check(!str_contains(json_encode($GLOBALS['wp_http_log']), 'redirection":1'), 'Es werden keine Weiterleitungen verfolgt');
$check(($GLOBALS['wp_http_log'][0]['args']['redirection'] ?? null) === 0, 'Anfragen folgen keinen Weiterleitungen (redirection = 0)');
// ── Verbindungstest: verständliche Meldungen ────────────────────────────────
reset_wp();
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response(401, ['error' => ['code' => 'IP_NOT_ALLOWED', 'message' => 'Der Schlüssel ist gültig, aber für die IP-Adresse 203.0.113.9 nicht freigegeben.']]);
$r = Deskhub_Tickets_Client::from_settings()->test_connection();
$check($r['ok'] === false && str_contains($r['message'], '203.0.113.9') && str_contains($r['message'], 'lässt diese Adresse nicht zu'), 'Verbindungstest: gültiger Schlüssel, falsche IP: Meldung nennt die Adresse, die DeskHub sieht');
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response(401, ['error' => ['code' => 'UNAUTHENTICATED', 'message' => 'x']]);
$r = Deskhub_Tickets_Client::from_settings()->test_connection();
$check($r['ok'] === false && str_contains($r['message'], 'Erlaubte IPs') && !str_contains($r['message'], 'dh_geheimer'), 'Verbindungstest: abgelehnter Schlüssel nennt die möglichen Ursachen (auch Erlaubte IPs)');
$GLOBALS['wp_http_handler'] = fn($u, $a) => new WP_Error('http_request_failed', "cURL error 28:\nConnection timed out after 15001 milliseconds");
$r = Deskhub_Tickets_Client::from_settings()->test_connection();
$check($r['ok'] === false && str_contains($r['message'], 'nicht erreichbar') && str_contains($r['message'], 'timed out') && !str_contains($r['message'], "\n"), 'Verbindungstest: bei einem Verbindungsfehler steht die technische Ursache dabei');
// ── „Speichern und Verbindung testen“ prüft die gerade eingegebenen Werte ───
reset_wp(['server_url' => '', 'api_key' => '']);
$GLOBALS['wp_user'] = ['id' => 5, 'caps' => ['manage_options'], 'name' => 'Admin', 'email' => 'a@example.de'];
$GLOBALS['wp_http_handler'] = fn($u, $a) => (($a['headers']['X-API-Key'] ?? '') === 'dh_neu_eingegeben') ? wp_stub_response(404, ['error' => ['code' => 'NOT_FOUND', 'message' => 'x']]) : wp_stub_response(401, ['error' => ['code' => 'UNAUTHENTICATED', 'message' => 'x']]);
$_POST = ['dht_test_after' => '1'];
Deskhub_Tickets_Settings::sanitize(['server_url' => 'https://support.example.de', 'api_key' => 'dh_neu_eingegeben']);
$res = get_transient('deskhub_tickets_test_5');
$check(is_array($res) && $res['ok'] === true, 'Speichern und testen: der neu eingegebene Schlüssel wird sofort geprüft (noch bevor er gespeichert ist)');
delete_transient('deskhub_tickets_test_5');
Deskhub_Tickets_Settings::sanitize(['server_url' => 'https://support.example.de', 'api_key' => '']);
$res = get_transient('deskhub_tickets_test_5');
$check(is_array($res) && $res['ok'] === false && str_contains($res['message'], 'eintragen'), 'Speichern und testen: ohne Schlüssel kommt die Aufforderung, ihn einzutragen');
delete_transient('deskhub_tickets_test_5');
$_POST = [];
Deskhub_Tickets_Settings::sanitize(['server_url' => 'https://support.example.de', 'api_key' => 'dh_neu_eingegeben']);
$check(get_transient('deskhub_tickets_test_5') === false, 'Normales Speichern testet nichts');
$GLOBALS['wp_user']['caps'] = [];
$_POST = ['dht_test_after' => '1'];
Deskhub_Tickets_Settings::sanitize(['server_url' => 'https://support.example.de', 'api_key' => 'dh_neu_eingegeben']);
$check(get_transient('deskhub_tickets_test_5') === false, 'Ohne Berechtigung wird nichts getestet');
$_POST = [];
$GLOBALS['wp_user']['caps'] = ['manage_options'];
$GLOBALS['wp_http_handler'] = fn($u, $a) => wp_stub_response(404, ['error' => ['code' => 'NOT_FOUND', 'message' => 'x']]);
ob_start(); Deskhub_Tickets_Settings::render(); $page = (string) ob_get_clean();
$check(str_contains($page, 'Speichern und Verbindung testen') && !str_contains($page, 'deskhub_tickets_test"'), 'Einstellungsseite: Speichern und testen in einem Schritt, kein getrennter Test-Knopf mehr');
$GLOBALS['wp_user']['caps'] = [];
// ── Einstellungen speichern ─────────────────────────────────────────────────
reset_wp(['api_key' => 'dh_alt', 'turnstile_secret' => 'ts_alt', 'server_url' => 'https://alt.example.de']);
$out = Deskhub_Tickets_Settings::sanitize(['server_url' => 'neu.example.de', 'api_key' => '', 'department_id' => '3x', 'min_seconds' => '500', 'rate_limit_per_hour' => '-4',
'consent_text' => 'Text <a href="/x" onclick="e()">Link</a><iframe src=x></iframe>', 'status_page_url' => 'javascript:alert(1)', 'turnstile_site' => '<b>abc</b>', 'turnstile_secret' => '']);
$check($out['server_url'] === 'https://neu.example.de', 'Einstellungen: Adresse wird bereinigt');
$check($out['api_key'] === 'dh_alt' && $out['turnstile_secret'] === 'ts_alt', 'Einstellungen: Leere Schlüsselfelder behalten den gespeicherten Wert');
$check($out['department_id'] === 3 && $out['min_seconds'] === 60 && $out['rate_limit_per_hour'] === 4, 'Einstellungen: Zahlen werden begrenzt');
$check(!str_contains($out['consent_text'], 'onclick') && !str_contains($out['consent_text'], 'iframe') && str_contains($out['consent_text'], 'href="/x"'), 'Einstellungen: Einwilligungstext wird bereinigt');
$check($out['status_page_url'] === '' && $out['turnstile_site'] === 'abc', 'Einstellungen: gefährliche Adresse und HTML werden entfernt');
$out = Deskhub_Tickets_Settings::sanitize(['server_url' => 'http://evil.example', 'api_key' => ' dh_neu\n123 ', 'clear_turnstile' => '1']);
$check($out['server_url'] === 'https://alt.example.de' && $GLOBALS['wp_settings_errors'], 'Einstellungen: ungültige Adresse behält die alte und meldet einen Fehler');
$check($out['api_key'] === 'dh_neun123' || $out['api_key'] === 'dh_neu\n123' || preg_match('/^[\x21-\x7E]+$/', $out['api_key']) === 1, 'Einstellungen: Schlüssel enthält nur sichtbare Zeichen');
$check($out['turnstile_secret'] === '', 'Einstellungen: Turnstile-Schlüssel lässt sich entfernen');
$out = Deskhub_Tickets_Settings::sanitize(['clear_key' => '1', 'api_key' => 'dh_x']);
$check($out['api_key'] === '', 'Einstellungen: Schlüssel entfernen');
// ── Gegen den echten DeskHub-Server ─────────────────────────────────────────
$url = getenv('DESKHUB_URL') ?: '';
$key = getenv('DESKHUB_KEY') ?: '';
if ($url !== '' && $key !== '') {
$curl = function (string $u, array $args) {
$ch = curl_init($u);
$hdr = [];
foreach ($args['headers'] ?? [] as $k => $v) $hdr[] = "$k: $v";
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $args['method'] ?? 'GET', CURLOPT_HTTPHEADER => $hdr, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => false]);
if (isset($args['body'])) curl_setopt($ch, CURLOPT_POSTFIELDS, $args['body']);
$body = (string) curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
return $code === 0 ? new WP_Error('http_request_failed', $err) : wp_stub_response($code, $body);
};
reset_wp(['server_url' => $url, 'api_key' => $key]);
$GLOBALS['wp_http_handler'] = $curl;
$client = Deskhub_Tickets_Client::from_settings();
$t = $client->test_connection();
$check($t['ok'] === true, 'ECHT: Verbindungstest erfolgreich (' . $t['message'] . ')');
$res = $client->create_ticket(['name' => 'WordPress-Test', 'email' => 'wp-test@example.com', 'subject' => 'Test aus dem WordPress-Plugin', 'message' => "Zeile 1\nZeile 2 mit Umlauten äöü und <b>HTML</b>"]);
$check(is_array($res) && preg_match('/^\d{6}-\d{4}$/', $res['number'] ?? '') === 1, 'ECHT: Ticket angelegt: ' . (is_array($res) ? $res['number'] : $res->get_error_message()));
if (is_array($res)) {
$got = $client->get_ticket($res['number'], 'WP-Test@Example.com');
$check(is_array($got) && $got['subject'] === 'Test aus dem WordPress-Plugin' && ($got['messages'][0]['author'] ?? '') === 'customer', 'ECHT: Status und Verlauf abgerufen (E-Mail ohne Beachtung der Groß-/Kleinschreibung)');
$check(is_array($got) && str_contains($got['messages'][0]['message'], 'äöü'), 'ECHT: Umlaute kommen unverändert an');
$wrong = $client->get_ticket($res['number'], 'andere@example.com');
$check(is_wp_error($wrong) && Deskhub_Tickets_Client::user_message($wrong) !== '' && str_contains(Deskhub_Tickets_Client::user_message($wrong), 'nichts gefunden'), 'ECHT: Falsche E-Mail-Adresse liefert nichts');
// Der ganze Weg über das Formular
$GLOBALS['wp_transients'] = [];
$html = render_form();
$st = state_from(run_form_post(valid_post($html, ['dht_email' => 'form-test@example.com', 'dht_subject' => 'Formular-Test'])));
$check(!empty($st['ok']) && preg_match('/^\d{6}-\d{4}$/', $st['number']) === 1, 'ECHT: Ganzer Weg über das Formular, Ticket ' . ($st['number'] ?? '?'));
$st = state_from(run_form_post(valid_post($html, ['dht_email' => 'form-test@example.com', 'dht_subject' => 'Zweiter Test'])));
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST = ['dht_status_submit' => '1', 'dht_number' => $st['number'] ?? 'x', 'dht_status_email' => 'form-test@example.com'];
$out = Deskhub_Tickets_Form::render_status_form();
$check(str_contains($out, 'Zweiter Test') && str_contains($out, 'Offen'), 'ECHT: Statusseite zeigt das neue Ticket');
}
$bad = (new Deskhub_Tickets_Client($url, 'dh_falscher_schluessel'))->test_connection();
$check($bad['ok'] === false && str_contains($bad['message'], 'abgelehnt'), 'ECHT: Falscher Schlüssel wird erkannt');
$inv = $client->create_ticket(['name' => 'X', 'email' => 'kaputt', 'subject' => 'S', 'message' => 'M']);
$check(is_wp_error($inv) && str_contains(Deskhub_Tickets_Client::user_message($inv), 'gültige E-Mail'), 'ECHT: Ungültige E-Mail-Adresse wird vom Server abgelehnt');
$down = (new Deskhub_Tickets_Client('http://127.0.0.1:1', $key))->test_connection();
$check($down['ok'] === false, 'ECHT: Nicht erreichbarer Server wird gemeldet');
$keyIp = getenv('DESKHUB_KEY_IP') ?: '';
if ($keyIp !== '') {
$r = (new Deskhub_Tickets_Client($url, $keyIp))->test_connection();
$check($r['ok'] === false && str_contains($r['message'], '127.0.0.1') && str_contains($r['message'], 'lässt diese Adresse nicht zu'), 'ECHT: Schlüssel mit anderer erlaubter IP: die Meldung nennt die Adresse, die DeskHub sieht (' . $r['message'] . ')');
}
// Eigenes Formular eines Hilfethemas (nur wenn die Testinstallation eines hat: DESKHUB_TOPIC, DESKHUB_F_SELECT, DESKHUB_F_DATE, DESKHUB_F_CHECK, DESKHUB_DB)
$topic = (int) (getenv('DESKHUB_TOPIC') ?: 0);
if ($topic > 0) {
$fSel = (int) getenv('DESKHUB_F_SELECT'); $fDate = (int) getenv('DESKHUB_F_DATE'); $fChk = (int) getenv('DESKHUB_F_CHECK');
$pdoReal = new PDO('sqlite:' . getenv('DESKHUB_DB'));
$pdoReal->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$topics = $client->get_topics();
$check(is_array($topics) && in_array($topic, array_column($topics, 'id'), true), 'ECHT: Hilfethemen werden geholt');
$form = $client->get_form($topic, null);
$check(is_array($form) && !empty($form['ownForm']) && ($form['title'] ?? '') === 'Störungsmeldung' && count($form['fields']) === 3, 'ECHT: Formular des Hilfethemas wird geholt (eigenes Formular mit 3 Feldern)');
$GLOBALS['wp_transients'] = [];
$html = render_form(['topic' => (string) $topic]);
$check(str_contains($html, 'Störungsmeldung') && str_contains($html, 'name="dht_f[' . $fSel . ']"') && str_contains($html, '<option value="Router"') && str_contains($html, 'Störung melden</button>')
&& preg_match('/class="dht-subject-wrap" hidden/', $html) === 1, 'ECHT: Das Formular zeigt Texte und Felder aus DeskHub (ohne Betreff und Beschreibung)');
// zuerst falsch (Pflichtfelder fehlen), dann richtig
$st = state_from(run_form_post(valid_post($html, ['dht_subject' => '', 'dht_message' => '', 'dht_f' => [(string) $fDate => '2026-10-31']])));
$check(!empty($st['errors']) && empty($st['ok']), 'ECHT: Fehlende Pflichtfelder werden abgelehnt');
$st = state_from(run_form_post(valid_post($html, ['dht_email' => 'stoerung@example.com', 'dht_subject' => '', 'dht_message' => '', 'dht_f' => [(string) $fSel => 'Router', (string) $fDate => '2026-10-31', (string) $fChk => '1']])));
$check(!empty($st['ok']) && preg_match('/^\d{6}-\d{4}$/', $st['number'] ?? '') === 1, 'ECHT: Störung über das Formular gemeldet, Ticket ' . ($st['number'] ?? '?'));
$t = $pdoReal->query('SELECT * FROM tickets WHERE ticket_number = ' . $pdoReal->quote((string) ($st['number'] ?? '')))->fetch(PDO::FETCH_ASSOC);
$check(is_array($t) && $t['subject'] === 'Anfrage Störungen' && (int) $t['help_topic_id'] === $topic, 'ECHT: Betreff „Anfrage Störungen“ und Hilfethema sind gesetzt');
$vals = $t ? $pdoReal->query('SELECT custom_field_id, value FROM ticket_field_values WHERE ticket_id = ' . (int) $t['id'])->fetchAll(PDO::FETCH_KEY_PAIR) : [];
$check(($vals[$fSel] ?? '') === 'Router' && ($vals[$fDate] ?? '') === '2026-10-31' && ($vals[$fChk] ?? '') === '1', 'ECHT: Die Angaben der Felder sind im Ticket gespeichert');
$msg = $t ? (string) $pdoReal->query('SELECT message FROM ticket_threads WHERE ticket_id = ' . (int) $t['id'])->fetchColumn() : '';
$check(str_contains($msg, 'Gerät: Router') && str_contains($msg, 'Dringend: Ja') && str_contains($msg, 'Gesendet über das Formular'), 'ECHT: Ticket-Text besteht aus den Angaben der Felder');
}
} else {
fwrite(STDOUT, "(Prüfungen gegen den echten Server übersprungen: DESKHUB_URL und DESKHUB_KEY nicht gesetzt)\n");
}
// Zuletzt: Die Konstante bleibt für den Rest des Prozesses gesetzt
// ── Schlüssel aus der wp-config.php ─────────────────────────────────────────
reset_wp(['api_key' => 'aus_datenbank']);
define('DESKHUB_TICKETS_API_KEY', 'aus_config');
$check(deskhub_tickets_option('api_key') === 'aus_config' && deskhub_tickets_key_from_config(), 'Schlüssel aus der wp-config.php hat Vorrang');
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen (" . count($failures) . " von $total):\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo "WordPress-Plugin: alle $total Prüfungen bestanden.\n";