Upload via GUI (30 Dateien)

This commit is contained in:
2026-11-01 11:17:41 +00:00
parent 53f8bf0577
commit c3b9948fa2
30 changed files with 5237 additions and 0 deletions
+518
View File
@@ -0,0 +1,518 @@
<?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";
+153
View File
@@ -0,0 +1,153 @@
<?php
/**
* Minimaler Nachbau der WordPress-Funktionen, die das Plugin benutzt. Nur für die Tests (es gibt hier kein echtes WordPress).
* Die Nachbauten folgen dem Verhalten von WordPress (zum Beispiel: add_query_arg codiert Werte nicht selbst).
*/
define('ABSPATH', __DIR__ . '/');
define('HOUR_IN_SECONDS', 3600);
define('MINUTE_IN_SECONDS', 60);
$GLOBALS['wp_options'] = [];
$GLOBALS['wp_transients'] = [];
$GLOBALS['wp_actions'] = [];
$GLOBALS['wp_filters'] = [];
$GLOBALS['wp_shortcodes'] = [];
$GLOBALS['wp_http_log'] = [];
$GLOBALS['wp_http_handler'] = null; // function(string $url, array $args): array|WP_Error
$GLOBALS['wp_user'] = ['id' => 0, 'caps' => [], 'name' => '', 'email' => ''];
$GLOBALS['wp_settings_errors'] = [];
$GLOBALS['wp_enqueued'] = [];
class WP_Error
{
public $code;
public $message;
public $data;
public function __construct($code = '', $message = '', $data = '') { $this->code = $code; $this->message = $message; $this->data = $data; }
public function get_error_message() { return $this->message; }
public function get_error_code() { return $this->code; }
public function get_error_data() { return $this->data; }
}
class WpRedirect extends Exception
{
public $location;
public function __construct(string $location) { parent::__construct('redirect'); $this->location = $location; }
}
function is_wp_error($v) { return $v instanceof WP_Error; }
function plugin_dir_path($file) { return rtrim(dirname($file), '/\\') . '/'; }
function plugin_dir_url($file) { return 'https://wp.test/wp-content/plugins/deskhub-tickets/'; }
function plugin_basename($file) { return 'deskhub-tickets/' . basename($file); }
function load_plugin_textdomain() { return true; }
function add_action($tag, $fn, $prio = 10, $args = 1) { $GLOBALS['wp_actions'][$tag][] = $fn; }
function add_filter($tag, $fn, $prio = 10, $args = 1) { $GLOBALS['wp_filters'][$tag][] = $fn; }
function add_shortcode($tag, $fn) { $GLOBALS['wp_shortcodes'][$tag] = $fn; }
function apply_filters($tag, $value, ...$rest) { foreach ($GLOBALS['wp_filters'][$tag] ?? [] as $fn) $value = $fn($value, ...$rest); return $value; }
function do_action($tag, ...$args) { foreach ($GLOBALS['wp_actions'][$tag] ?? [] as $fn) $fn(...$args); }
function get_option($k, $default = false) { return $GLOBALS['wp_options'][$k] ?? $default; }
function update_option($k, $v) { $GLOBALS['wp_options'][$k] = $v; return true; }
function delete_option($k) { unset($GLOBALS['wp_options'][$k]); return true; }
function get_transient($k) { $t = $GLOBALS['wp_transients'][$k] ?? null; if (!$t) return false; if ($t[1] < time()) { unset($GLOBALS['wp_transients'][$k]); return false; } return $t[0]; }
function set_transient($k, $v, $ttl = 0) { $GLOBALS['wp_transients'][$k] = [$v, time() + ($ttl ?: 86400 * 365)]; return true; }
function delete_transient($k) { unset($GLOBALS['wp_transients'][$k]); return true; }
function wp_salt($scheme = 'auth') { return 'test-salt-' . $scheme; }
function __($s, $d = null) { return $s; }
function esc_html__($s, $d = null) { return esc_html($s); }
function esc_attr__($s, $d = null) { return esc_attr($s); }
function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); }
function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); }
function esc_textarea($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); }
function esc_url($s) { $s = (string) $s; return preg_match('#^(https?://|/)#i', $s) ? htmlspecialchars($s, ENT_QUOTES, 'UTF-8') : ''; }
function esc_url_raw($s) { $s = (string) $s; return preg_match('#^https?://#i', $s) ? $s : ''; }
function wp_kses($s, $allowed) {
// vereinfacht: erlaubte Tags behalten (ohne Attribute zu prüfen außer href), alle anderen Tags entfernen
$tags = array_keys($allowed);
return preg_replace_callback('#<(/?)([a-z0-9]+)([^>]*)>#i', function ($m) use ($allowed, $tags) {
$name = strtolower($m[2]);
if (!in_array($name, $tags, true)) return '';
if ($m[1] === '/') return '</' . $name . '>';
$attrs = '';
if (preg_match_all('#\s([a-z-]+)="([^"]*)"#i', $m[3], $am, PREG_SET_ORDER)) {
foreach ($am as $a) {
if (isset($allowed[$name][strtolower($a[1])]) && !preg_match('#^\s*javascript:#i', $a[2])) $attrs .= ' ' . strtolower($a[1]) . '="' . $a[2] . '"';
}
}
return '<' . $name . $attrs . '>';
}, (string) $s);
}
function wp_parse_url($url, $component = -1) { return parse_url($url, $component); }
function add_query_arg($key, $value = null, $url = null) {
if (is_array($key)) { $args = $key; $url = $value; } else { $args = [$key => $value]; }
$parts = explode('#', (string) $url, 2);
$base = $parts[0];
$frag = isset($parts[1]) ? '#' . $parts[1] : '';
$q = '';
if (strpos($base, '?') !== false) { [$base, $q] = explode('?', $base, 2); }
parse_str($q, $existing);
foreach ($args as $k => $v) { $existing[$k] = $v; } // Werte werden NICHT codiert (wie in WordPress)
$pairs = [];
foreach ($existing as $k => $v) { $pairs[] = $k . '=' . $v; }
return $base . ($pairs ? '?' . implode('&', $pairs) : '') . $frag;
}
function remove_query_arg($key, $url = null) {
$parts = explode('?', (string) $url, 2);
if (count($parts) < 2) return $parts[0];
parse_str($parts[1], $q);
foreach ((array) $key as $k) unset($q[$k]);
return $parts[0] . ($q ? '?' . http_build_query($q) : '');
}
function wp_json_encode($v, $flags = 0) { return json_encode($v, $flags | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); }
function wp_remote_retrieve_response_code($r) { return is_array($r) ? ($r['response']['code'] ?? 0) : 0; }
function wp_remote_retrieve_body($r) { return is_array($r) ? ($r['body'] ?? '') : ''; }
function wp_remote_request($url, $args = []) {
$GLOBALS['wp_http_log'][] = ['url' => $url, 'args' => $args];
$h = $GLOBALS['wp_http_handler'];
if (!$h) return new WP_Error('http_no_handler', 'kein Server');
return $h($url, $args);
}
function wp_remote_post($url, $args = []) { $args['method'] = 'POST'; return wp_remote_request($url, $args); }
function shortcode_atts($pairs, $atts, $shortcode = '') { $out = []; foreach ($pairs as $k => $d) $out[$k] = array_key_exists($k, $atts) ? $atts[$k] : $d; return $out; }
function wp_register_style() {}
function wp_enqueue_style($h) { $GLOBALS['wp_enqueued'][] = $h; }
function wp_enqueue_script($h) { $GLOBALS['wp_enqueued'][] = $h; }
function current_user_can($cap) { return in_array($cap, $GLOBALS['wp_user']['caps'], true); }
function is_user_logged_in() { return $GLOBALS['wp_user']['id'] > 0; }
function wp_get_current_user() { return (object) ['display_name' => $GLOBALS['wp_user']['name'], 'user_email' => $GLOBALS['wp_user']['email']]; }
function get_current_user_id() { return $GLOBALS['wp_user']['id']; }
function admin_url($p = '') { return 'https://wp.test/wp-admin/' . ltrim($p, '/'); }
function home_url($p = '') { return 'https://wp.test' . ($p === '' ? '' : '/' . ltrim($p, '/')); }
function get_permalink() { return $GLOBALS['wp_permalink'] ?? 'https://wp.test/kontakt/'; }
function wp_validate_redirect($url, $default = '') { $h = parse_url((string) $url, PHP_URL_HOST); return ($h === 'wp.test') ? $url : $default; }
function wp_safe_redirect($url) { throw new WpRedirect((string) $url); }
function sanitize_text_field($s) { $s = strip_tags((string) $s); $s = preg_replace('/[\r\n\t ]+/', ' ', $s); return trim($s); }
function sanitize_textarea_field($s) { $s = strip_tags((string) $s); return trim(str_replace("\r\n", "\n", $s)); }
function sanitize_email($s) { return preg_replace('/[^a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~@-]/', '', (string) $s); }
function is_email($s) { return (bool) filter_var($s, FILTER_VALIDATE_EMAIL); }
function wp_unslash($v) { return $v; }
function absint($v) { return abs((int) $v); }
function selected($a, $b) { if ((string) $a === (string) $b) echo ' selected="selected"'; }
function date_i18n($fmt, $ts) { return date($fmt, $ts); }
function register_setting() {}
function settings_fields() {}
function settings_errors() {}
function add_settings_error($setting, $code, $message) { $GLOBALS['wp_settings_errors'][] = [$code, $message]; }
function add_options_page() {}
function submit_button() {}
function wp_nonce_field() {}
function check_admin_referer() { return true; }
function wp_die($m = '') { throw new RuntimeException('wp_die: ' . $m); }
function wp_kses_post($s) { return $s; }
/** Antwort für wp_remote_request nachbauen. */
function wp_stub_response(int $code, $body): array
{
return ['response' => ['code' => $code], 'body' => is_string($body) ? $body : json_encode($body, JSON_UNESCAPED_UNICODE)];
}
if (!function_exists('sanitize_hex_color')) {
function sanitize_hex_color($c) { return preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', (string) $c) ? $c : ''; }
}
if (!function_exists('checked')) {
function checked($a, $b = true, $echo = true) { $r = (string) $a === (string) $b ? " checked='checked'" : ''; if ($echo) echo $r; return $r; }
}