OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

ExternalApiDocsTest.php

8 KB
<?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');
    });
};