Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Die Doku der externen Schnittstelle bleibt an der Schnittstelle.
*
* Beschrieben wird sie an zwei Stellen – als OpenAPI-Spezifikation (Swagger-UI
* und JSON-Download) und als Markdown zum Herunterladen. Beide stehen im
* Admin-Controller, die Schnittstelle selbst woanders; ein neues Feld erreicht
* daher leicht nur den einen Ort. Wer ein Feld nicht beschrieben findet, hält es
* für nicht vorhanden: Der Dateianhang eines Tickets war über die Schnittstelle
* möglich, stand aber in keinem der beiden Dokumente.
*/
return static function (): void {
$wurzel = dirname(__DIR__);
$api = (string)file_get_contents($wurzel . '/src/Controllers/WordPressApiController.php');
$doku = (string)file_get_contents($wurzel . '/src/Controllers/Admin/WordPressAdminController.php');
// Die beiden Fassungen getrennt betrachten – sonst deckt ein Fund in der
// einen den fehlenden Eintrag in der anderen zu.
$beginnMarkdown = strpos($doku, 'public function downloadApiDocs');
$beginnSpec = strpos($doku, 'public function openapiSpec');
assertTrue($beginnMarkdown !== false && $beginnSpec !== false && $beginnMarkdown < $beginnSpec,
'Die beiden Doku-Fassungen wurden nicht gefunden');
$markdown = substr($doku, $beginnMarkdown, $beginnSpec - $beginnMarkdown);
$spec = substr($doku, $beginnSpec);
test('Jedes gelesene Body-Feld steht in der Doku', function () use ($api, $doku) {
// Was der Endpunkt aus dem JSON-Body liest, muss auch beschrieben sein.
preg_match_all("/\\\$body\['([a-z_0-9]+)'\]/", $api, $treffer);
$felder = array_unique($treffer[1]);
assertTrue(count($felder) > 20, 'Die Felder wurden nicht erkannt');
$fehlen = array_values(array_filter(
$felder,
static fn(string $feld): bool => !str_contains($doku, $feld)
));
assertSame([], $fehlen, 'Nicht dokumentiert: ' . implode(', ', $fehlen));
});
test('Der Dateianhang eines Tickets steht in beiden Dokumenten', function () use ($markdown, $spec) {
foreach (['attachment_b64', 'attachment_name', 'attachment_mime'] as $feld) {
assertTrue(str_contains($markdown, $feld), $feld . ' fehlt in der Markdown-Doku');
assertTrue(str_contains($spec, $feld), $feld . ' fehlt in der OpenAPI-Spezifikation');
}
// Die Grenzen gehören dazu, sonst ist das Feld nicht benutzbar.
assertTrue(str_contains($markdown, '10 MB'), 'Die Größengrenze fehlt im Markdown');
assertTrue(str_contains($spec, '10 MB'), 'Die Größengrenze fehlt in der Spezifikation');
assertTrue(str_contains($markdown, 'base64'), 'Die Kodierung wird im Markdown nicht genannt');
assertTrue(str_contains($spec, 'base64'), 'Die Kodierung wird in der Spezifikation nicht genannt');
});
test('Die erlaubten Anhang-Typen stimmen mit der Allowlist überein', function () use ($api, $doku) {
// Ein Typ, der in der Doku steht, aber nicht in der Allowlist, führt zu
// einem abgelehnten Anhang; umgekehrt bleibt ein erlaubter Typ ungenutzt.
preg_match('/private const ATTACHMENT_TYPES = \[(.+?)\n \];/s', $api, $m);
assertTrue(!empty($m[1]), 'Die Allowlist wurde nicht gefunden');
preg_match_all("/'([a-z]+\/[a-z0-9.\-+]+)'\s*(?:\n\s*)?=>/", $m[1], $typen);
$erlaubt = array_unique($typen[1]);
assertTrue(count($erlaubt) >= 8, 'Die Typen wurden nicht erkannt: ' . count($erlaubt));
$fehlen = array_values(array_filter(
$erlaubt,
static fn(string $typ): bool => !str_contains($doku, $typ)
));
assertSame([], $fehlen, 'Erlaubter Typ fehlt in der Doku: ' . implode(', ', $fehlen));
});
test('Ein abgelehnter Anhang wird gemeldet, nicht verschwiegen', function () use ($api) {
// Das Ticket entsteht trotzdem – der Absender muss aber erfahren, dass
// sein Screenshot nicht dabei ist.
assertTrue(
str_contains($api, 'private function saveTicketAttachment(')
&& str_contains($api, '): string {'),
'Die Ablage des Anhangs gibt keinen Grund zurück'
);
assertFalse(
(bool)preg_match('/saveTicketAttachment\([^)]*\n?[^)]*\): void/', $api),
'Die Ablage endet weiterhin ohne Rückmeldung'
);
assertTrue(
str_contains($api, 'Der Dateianhang konnte nicht übernommen werden: '),
'Der Grund erreicht den Absender nicht'
);
assertTrue(str_contains($api, "'attachment' =>"), 'Die Antwort nennt den Zustand des Anhangs nicht');
});
test('Jeder Ablehnungsgrund wird benannt', function () {
// Die Ablehnungswege berühren keine Datenbank, lassen sich also direkt
// prüfen. Ein Grund, der nur „fehlgeschlagen" sagt, hilft dem Absender
// nicht weiter – jede Ablehnung nennt, was zu tun ist.
$klasse = new ReflectionClass(\OpenNIT\Controllers\WordPressApiController::class);
$ctrl = $klasse->newInstanceWithoutConstructor();
$ablage = $klasse->getMethod('saveTicketAttachment');
$ablage->setAccessible(true);
$ablegen = static fn(string $b64, string $name, string $mime): string
=> (string)$ablage->invoke($ctrl, 1, $b64, $name, $mime);
$grund = $ablegen(base64_encode('x'), 'virus.exe', 'application/x-msdownload');
assertTrue(str_contains($grund, 'Format'), 'Die falsche Dateiart wird nicht benannt');
assertTrue(str_contains($grund, 'PNG') && str_contains($grund, 'PDF'),
'Die erlaubten Formate werden nicht genannt');
$grund = $ablegen('###keine-base64###', 'bild.png', 'image/png');
assertTrue(str_contains($grund, 'beschädigt'), 'Eine unlesbare Kodierung wird nicht benannt');
// 11 MB: die Größe entscheidet vor dem Inhalt.
$grund = $ablegen(base64_encode(str_repeat('A', 11 * 1024 * 1024)), 'gross.png', 'image/png');
assertTrue(str_contains($grund, '11 MB'), 'Die vorgefundene Größe fehlt: ' . $grund);
assertTrue(str_contains($grund, '10 MB'), 'Die erlaubte Größe fehlt');
// Text, der sich als Bild ausgibt – die inhaltliche Prüfung greift.
$grund = $ablegen(base64_encode("Das ist nur Text.\n"), 'bild.png', 'image/png');
assertTrue(str_contains($grund, 'passt nicht'), 'Der Widerspruch wird nicht benannt');
assertTrue(str_contains($grund, 'image/png'), 'Die angegebene Dateiart fehlt im Grund');
});
test('Die Antwortbeispiele der Doku nennen die Felder, die der Code sendet', function () use ($api, $doku) {
// Das Beispiel zum Anlegen nannte lange `id` und `status`; gesendet
// werden `success`, `ticket_id` und `message`.
assertTrue(str_contains($api, "'ticket_id' => \$ticketId"), 'Das Feld der Ticket-Kennung hat sich geändert');
assertTrue(str_contains($doku, '"ticket_id": 42'), 'Das Antwortbeispiel nennt die Ticket-Kennung nicht');
assertFalse(str_contains($doku, '{ "id": 42, "status": "open" }'), 'Das falsche Antwortbeispiel steht noch da');
});
test('Eine Antwort nimmt keinen Anhang – und die Doku sagt das', function () use ($api, $doku) {
// Die Felder gibt es nur beim Anlegen. Ohne diesen Hinweis wird
// Symmetrie unterstellt und der Anhang stillschweigend verworfen.
$reply = (string)(preg_split('/public function ticketReply/', $api)[1] ?? '');
$reply = substr($reply, 0, 3000);
assertFalse(str_contains($reply, 'attachment_b64'), 'Die Antwort-Route nimmt jetzt Anhänge – Doku anpassen');
assertTrue(str_contains($doku, 'keinen Dateianhang'), 'Der Hinweis zur Antwort-Route fehlt');
});
};