OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

ApiCollectionsTest.php

12 KB
<?php

declare(strict_types=1);

use OpenNIT\Api\ApiRegistry;

/**
 * Hält die Entscheidungen der neueren Sammlungen fest: was sie bewusst **nicht**
 * herausgeben, und warum sie schreibgeschützt sind.
 *
 * Die allgemeinen Zusagen (Tabellen existieren, keine Geheimnisse, Scope je
 * Sammlung, Eintrag in der Spezifikation) prüft `ApiRegistryTest`. Hier steht
 * nur, was sich aus der allgemeinen Regel nicht ergibt.
 */
return static function (): void {

    $wurzel = dirname(__DIR__);

    /** Felder einer Sammlung als Menge der Ausgabenamen. */
    $felder = static function (string $key): array {
        $r = ApiRegistry::get($key);
        assertTrue($r !== null, 'Sammlung fehlt: ' . $key);
        return array_keys($r->fields);
    };

    /** Die SQL-Ausdrücke einer Sammlung als ein Text. */
    $ausdruecke = static function (string $key): string {
        $r = ApiRegistry::get($key);
        return $r === null ? '' : implode(' | ', $r->fields);
    };

    // ----------------------------------------------------------------
    // Die Sammlungen gibt es überhaupt
    // ----------------------------------------------------------------

    test('Die ergänzten Sammlungen sind angemeldet', function () {
        foreach ([
            'orders', 'requests', 'software', 'phonebook', 'pages', 'monitoring',
            'xdr-alerts', 'unifi-devices', 'backup-jobs', 'backup-repositories', 'network-diagrams',
            'events',
        ] as $key) {
            assertTrue(ApiRegistry::get($key) !== null, $key);
        }
    });

    test('Jedes Modul mit Bestand hat eine Sammlung', function () use ($wurzel) {
        // Module ohne eigenen Bestand bleiben bewusst draußen; der Grund steht
        // im Klassenkommentar der Registrierung. Kommt ein Modul dazu, fällt
        // hier auf, dass die Schnittstelle es noch nicht kennt.
        $ohneSammlung = ['vault', 'todos', 'deadlines', 'files', 'ai_chat', 'api_auto', 'teamboard'];

        preg_match(
            "/'modules'\s*=>\s*\[(.*?)\n\s*\]/s",
            (string)file_get_contents($wurzel . '/public/index.php'),
            $block
        );
        assertTrue($block !== [], 'Die Modulliste liess sich nicht lesen');
        preg_match_all("/'([a-z_]+)'\s*=>/", $block[1], $namen);
        assertTrue(count($namen[1]) > 20, 'Die Modulliste ist unerwartet kurz');

        $mitSammlung = [];
        foreach (ApiRegistry::all() as $r) {
            $mitSammlung[$r->module] = true;
        }

        foreach ($namen[1] as $modul) {
            if (in_array($modul, $ohneSammlung, true)) {
                assertFalse(isset($mitSammlung[$modul]), $modul . ' steht als bewusst draußen, hat aber eine Sammlung');
                continue;
            }
            assertTrue(isset($mitSammlung[$modul]), 'Modul ohne Sammlung in der Schnittstelle: ' . $modul);
        }
    });

    test('Der Klassenkommentar begründet jedes ausgelassene Modul', function () use ($wurzel) {
        $quelle = (string)file_get_contents($wurzel . '/src/Api/ApiRegistry.php');
        $kopf   = substr($quelle, 0, (int)strpos($quelle, 'final class'));
        foreach (['Tresor', 'Aufgabenliste', 'Fristen', 'Dateiablage', 'Notizbrett'] as $wort) {
            assertTrue(str_contains($kopf, $wort), 'Ohne Begründung ausgelassen: ' . $wort);
        }
    });

    test('Jede ergänzte Sammlung nennt den Grund für den Schreibschutz', function () {
        foreach ([
            'orders', 'requests', 'software', 'phonebook', 'pages', 'monitoring',
            'xdr-alerts', 'unifi-devices', 'backup-jobs', 'backup-repositories', 'network-diagrams',
            'events',
        ] as $key) {
            $r = ApiRegistry::get($key);
            assertFalse($r->schreibbar(), $key . ' ist unerwartet schreibbar');
            // Ohne Begründung sucht die aufrufende Seite den Fehler bei sich.
            assertTrue(trim($r->writeNote) !== '', $key . ' ohne writeNote');
        }
    });

    // ----------------------------------------------------------------
    // Was bewusst draußen bleibt
    // ----------------------------------------------------------------

    test('Die Telefonliste ist kein Personalauszug', function () use ($felder) {
        $f = $felder('phonebook');
        foreach (['employee_id', 'employee_type', 'external_id', 'image_path'] as $weg) {
            assertFalse(in_array($weg, $f, true), 'phonebook gibt ' . $weg . ' heraus');
        }
        // Nur aktive Personen – Ausgeschiedene stehen nicht in einer Telefonliste.
        assertSame('dp.is_active = 1', ApiRegistry::get('phonebook')->baseWhere);
    });

    test('Dokuseiten geben ihr Verzeichnis heraus, nicht ihren Inhalt', function () use ($felder, $ausdruecke) {
        $f = $felder('pages');
        foreach (['content', 'body', 'current_version_id'] as $weg) {
            assertFalse(in_array($weg, $f, true), 'pages gibt ' . $weg . ' heraus');
        }
        assertFalse(str_contains($ausdruecke('pages'), 'page_versions'),
            'Der Seitentext folgt den Rechten am Themengebiet, die ein Token nicht kennt');
        assertSame('p.is_archived = 0', ApiRegistry::get('pages')->baseWhere);
    });

    test('Anträge geben weder Formularwerte noch die eingefrorene Vorlage heraus', function () use ($felder, $ausdruecke) {
        $f = $felder('requests');
        foreach (['definition_snapshot', 'requester_email', 'public_token'] as $weg) {
            assertFalse(in_array($weg, $f, true), 'requests gibt ' . $weg . ' heraus');
        }
        assertFalse(str_contains($ausdruecke('requests'), 'request_field_values'),
            'Die ausgefüllten Felder können personenbezogene Angaben führen');
        // Ob bereits anonymisiert, muss die auswertende Seite erkennen können.
        assertTrue(in_array('anonymized', $f, true));
    });

    test('Bestellungen geben die Adresse der empfangenden Person nicht heraus', function () use ($felder) {
        assertFalse(in_array('recipient_email', $felder('orders'), true));
    });

    test('Überwachungsmeldungen verweisen nicht in das fremde System', function () use ($felder) {
        assertFalse(in_array('trigger_url', $felder('monitoring'), true));
    });

    test('XDR-Warnungen geben die Rohdaten des Anbieters nicht heraus', function () use ($felder) {
        $f = $felder('xdr-alerts');
        foreach (['raw_payload', 'mitre_techniques', 'agent_guid', 'mac_address'] as $weg) {
            assertFalse(in_array($weg, $f, true), 'xdr-alerts gibt ' . $weg . ' heraus');
        }
        // Ohne die Einstufung wäre die Sammlung für eine Auswertung wertlos.
        assertTrue(in_array('severity', $f, true));
        assertTrue(in_array('local_status', $f, true));
    });

    test('Der Sicherungsspeicher nennt seinen Ablagepfad nicht', function () use ($felder) {
        assertFalse(in_array('path', $felder('backup-repositories'), true));
    });

    test('Der Ereignisstrom nennt den Datensatz, nicht seine Werte', function () use ($felder) {
        $f = $felder('events');
        foreach (['details_json', 'old_value_json', 'new_value_json', 'object_name',
                  'ip_address', 'user_agent', 'session_id', 'checksum'] as $weg) {
            assertFalse(in_array($weg, $f, true), 'events gibt ' . $weg . ' heraus');
        }
        // Ohne diese drei liesse sich kein Datensatz nachschlagen.
        foreach (['object_type', 'object_id', 'action'] as $noetig) {
            assertTrue(in_array($noetig, $f, true), 'events nennt ' . $noetig . ' nicht');
        }
    });

    test('Der Ereignisstrom lässt sich lückenlos abholen', function () {
        $r = ApiRegistry::get('events');
        // Aufsteigend gelesen kommt kein Ereignis doppelt und keines gar nicht.
        assertSame('al.id ASC', $r->orderBy);
        assertSame('al.id > ?', $r->filters['since'] ?? '');
    });

    test('Der Tresor bleibt auch aus dem Ereignisstrom heraus', function () {
        $wo = ApiRegistry::get('events')->baseWhere;
        assertTrue(str_contains($wo, "action_category <> 'vault'"), 'Tresor-Vorgänge stehen im Strom');
        assertTrue(str_contains($wo, "NOT LIKE 'vault%'"), 'Tresor-Datensätze stehen im Strom');
    });

    test('Netzwerkpläne geben ihr Verzeichnis heraus, nicht die Zeichnung', function () use ($felder) {
        assertFalse(in_array('data', $felder('network-diagrams'), true));
        assertTrue(in_array('name', $felder('network-diagrams'), true));
    });

    // ----------------------------------------------------------------
    // Kennungen aus fremden Systemen
    // ----------------------------------------------------------------

    test('Eine Sammlung mit Textkennung nimmt sie an, eine zählende nicht', function () {
        $sicherung = ApiRegistry::get('backup-jobs');
        assertTrue($sicherung->textId, 'backup-jobs zählt nicht aufwärts');
        assertSame('a1b2-c3d4', $sicherung->kennungAus(' a1b2-c3d4 '));
        assertSame(null, $sicherung->kennungAus('   '));
        assertSame(null, $sicherung->kennungAus(str_repeat('x', 256)));

        $vertrag = ApiRegistry::get('contracts');
        assertFalse($vertrag->textId);
        assertSame(7, $vertrag->kennungAus('7'));
        assertSame(null, $vertrag->kennungAus('7x'), 'Eine halbe Zahl ist keine Kennung');
        assertSame(null, $vertrag->kennungAus('0'));
        assertSame(null, $vertrag->kennungAus('-3'));
    });

    test('Der Einzelabruf lässt die Sammlung über die Kennung entscheiden', function () use ($wurzel) {
        $quelle = (string)file_get_contents($wurzel . '/src/Controllers/Api/ResourceController.php');
        assertTrue(str_contains($quelle, '$resource->kennungAus('),
            'Der Einzelabruf prüft die Kennung nicht über die Sammlung');
    });

    test('Das Software-Inventar zählt Geräte, ohne sie zu nennen', function () use ($felder) {
        $f = $felder('software');
        assertTrue(in_array('device_count', $f, true));
        assertFalse(in_array('asset_id', $f, true), 'Welche Geräte, steht in der Oberfläche');
    });

    // ----------------------------------------------------------------
    // Modul-Zuordnung
    // ----------------------------------------------------------------

    test('Jede Sammlung hängt an einem Modulschlüssel, den es gibt', function () use ($wurzel) {
        $quelle = (string)file_get_contents($wurzel . '/public/index.php');
        foreach (ApiRegistry::all() as $key => $r) {
            if ($r->module === '') {
                continue; // modulfrei: immer erreichbar
            }
            assertTrue(
                (bool)preg_match("/'" . preg_quote($r->module, '/') . "'\s*=>/", $quelle),
                'Sammlung ' . $key . ' hängt an Modul ' . $r->module . ', das es nicht gibt'
            );
        }
    });

    // ----------------------------------------------------------------
    // Die Beschreibung trägt, was die Doku verspricht
    // ----------------------------------------------------------------

    test('Geändert wird über PATCH, nicht über POST', function () use ($wurzel) {
        // Die Referenz auf der Webseite nennt PATCH; weicht die Route ab, geht
        // die Beschreibung an der Anwendung vorbei.
        $quelle = (string)file_get_contents($wurzel . '/src/Core/Application.php');
        assertTrue(str_contains($quelle, "router->patch('/api/ex/v1/{resource}/{id}'"));
        assertFalse(str_contains($quelle, "router->post('/api/ex/v1/{resource}/{id}'"));
    });

    test('Die Richtung der Sortierung kommt aus order, nicht aus einem Vorzeichen', function () use ($wurzel) {
        $quelle = (string)file_get_contents($wurzel . '/src/Controllers/Api/ResourceController.php');
        $anfang = strpos($quelle, 'private function sortierung(');
        assertTrue($anfang !== false);
        $rumpf = substr($quelle, $anfang, 500);
        assertTrue(str_contains($rumpf, "query('order'"));
    });

    test('Die Seitengrößen stehen so da, wie die Referenz sie nennt', function () {
        assertSame(50, ApiRegistry::DEFAULT_LIMIT);
        assertSame(200, ApiRegistry::MAX_LIMIT);
    });
};