OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

MdmConnectionTest.php

14 KB
<?php

declare(strict_types=1);

use OpenNIT\Services\MdmConnectionManager;

/**
 * Deckt mehrere Relution-Instanzen nebeneinander ab.
 *
 * Der Abgleich mustert Geräte aus, die er nicht mehr sieht. Diese Regel war an
 * die Quelle `relution` gebunden – bei zwei Instanzen nebeneinander hätte
 * damit jeder Lauf die Geräte der jeweils anderen ausgemustert, und zwar
 * lautlos: Der Lauf meldet Erfolg, und die Geräte stehen plötzlich auf
 * „ausgemustert", ohne dass sich etwas geändert hätte.
 *
 * Das ist der Fall, gegen den die Fälle hier laufen. Dazu kommen zwei
 * Nachbarn derselben Art: das Wiederfinden über die Seriennummer, das sonst
 * einer anderen Instanz ihr Gerät wegnimmt, und die Prozentgrenze, die über
 * alle Instanzen gerechnet zu großzügig ausfiele.
 */
return static function (): void {

    $wurzel = dirname(__DIR__);
    $quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);

    // ----------------------------------------------------------------
    // Die Ausmusterung bleibt bei ihrer Instanz
    // ----------------------------------------------------------------

    test('Die Ausmusterung grenzt auf die eigene Instanz ein', function () use ($quelle) {
        $src   = $quelle('src/Services/RelutionSyncService.php');
        $start = strpos($src, 'private function decommissionMissing(');
        assertTrue($start !== false, 'Die Ausmusterung fehlt');
        $body = substr($src, (int)$start, 2600);

        assertTrue(str_contains($body, '$this->bestandsGrenze()'), 'Die Ausmusterung greift über alle Instanzen');
        assertTrue(str_contains($body, 'array_merge($grenzParams, $activeMdmIds)'), 'Die Eingrenzung landet nicht in der Abfrage');
    });

    test('Die Prozentgrenze misst am eigenen Bestand', function () use ($quelle) {
        // Über alle Instanzen gerechnet fiele sie zu großzügig aus: Bei einer
        // kleinen Instanz neben einer großen ginge eine Ausmusterung durch,
        // die dort jedes Gerät betrifft.
        $src   = $quelle('src/Services/RelutionSyncService.php');
        $start = strpos($src, '$managed = (int)$this->db->fetchColumn(');
        assertTrue($start !== false, 'Die Zählung fehlt');
        $body = substr($src, (int)$start, 400);

        assertTrue(str_contains($body, '$grenze'), 'Die Zählung greift über alle Instanzen');
        assertTrue(str_contains($body, '$grenzParams'), 'Die Eingrenzung landet nicht in der Abfrage');
    });

    test('Ohne Instanz bleibt es beim Bestandsverhalten', function () use ($quelle) {
        // Eine Installation, die noch nicht umgestellt ist, soll sich nicht
        // ändern – dort tragen die Geräte keine Instanz.
        $src   = $quelle('src/Services/RelutionSyncService.php');
        $start = strpos($src, 'private function bestandsGrenze(): array');
        assertTrue($start !== false, 'Die Eingrenzung fehlt');
        $body = substr($src, (int)$start, 400);

        assertTrue(str_contains($body, "' AND mdm_connection_id = ?'"), 'Mit Instanz wird nicht eingegrenzt');
        assertTrue(str_contains($body, "' AND mdm_connection_id IS NULL'"), 'Ohne Instanz fehlt die Abgrenzung');
    });

    // ----------------------------------------------------------------
    // Das Wiederfinden
    // ----------------------------------------------------------------

    test('Ein Gerät einer fremden Instanz wird nicht übernommen', function () use ($quelle) {
        // Über die Seriennummer fände sonst jede Instanz das Gerät der anderen
        // und schriebe es auf sich um – bei jedem Lauf aufs Neue.
        $src   = $quelle('src/Services/RelutionSyncService.php');
        $start = strpos($src, '$matchedBySerial = false;');
        assertTrue($start !== false, 'Der Rückfall über die Seriennummer fehlt');
        $body = substr($src, (int)$start, 2200);

        assertTrue(
            str_contains($body, "' AND (mdm_connection_id IS NULL OR mdm_connection_id = ?)'"),
            'Die Suche über die Seriennummer greift auf fremde Instanzen zu'
        );
        assertTrue(str_contains($body, "' AND mdm_connection_id IS NULL'"), 'Ohne Instanz fehlt die Abgrenzung');
    });

    test('Die Suche über die Kennung bleibt in der eigenen Instanz', function () use ($quelle) {
        $src   = $quelle('src/Services/RelutionSyncService.php');
        $start = strpos($src, '[$grenze, $grenzParams] = $this->bestandsGrenze();');
        assertTrue($start !== false, 'Die Eingrenzung wird beim Suchen nicht verwendet');
        $body = substr($src, (int)$start, 600);

        assertTrue(str_contains($body, 'mdm_id = ? AND mdm_source = "relution"'), 'Die Suche fehlt');
        assertTrue(str_contains($body, "' . \$grenze . '"), 'Die Suche greift über alle Instanzen');
    });

    test('Ein Gerät bekommt seine Instanz mit', function () use ($quelle) {
        $src = $quelle('src/Services/RelutionSyncService.php');
        // Beim Anlegen …
        assertTrue(str_contains($src, "'mdm_connection_id' => \$this->connectionId ?: null,"), 'Ein neues Gerät bleibt ohne Herkunft');
        // … und beim Aktualisieren.
        assertTrue(str_contains($src, 'mdm_connection_id  = ?,'), 'Ein bekanntes Gerät behält keine Herkunft');
    });

    // ----------------------------------------------------------------
    // Der Lauf über mehrere Instanzen
    // ----------------------------------------------------------------

    test('Eine unerreichbare Instanz kostet nicht die übrigen', function () use ($quelle) {
        $src   = $quelle('src/Services/RelutionSyncRunner.php');
        $start = strpos($src, 'foreach ($instanzen as $instanz)');
        $body  = substr($src, (int)$start);

        assertTrue(str_contains($body, 'catch (\\Throwable $ex)'), 'Ein Fehler bricht den ganzen Lauf ab');
        assertTrue(str_contains($src, "'ok'             => \$geglueckt > 0,"), 'Ein einzelner Fehlschlag gilt als Gesamtfehler');
    });

    test('Eine unvollständig eingerichtete Instanz wird übersprungen, nicht verschwiegen', function () use ($quelle) {
        $src = $quelle('src/Services/RelutionSyncRunner.php');
        assertTrue(str_contains($src, 'Nicht vollständig eingerichtet'), 'Der Fall wird nicht benannt');
        assertTrue(str_contains($src, '$fehler[]     = $name'), 'Der Fall taucht nicht im Ergebnis auf');
    });

    test('Ein Probelauf steht nicht als Vorgang im Protokoll', function () use ($quelle) {
        // Er ändert nichts; ein Eintrag suggerierte einen Abgleich.
        $src   = $quelle('src/Services/RelutionSyncRunner.php');
        $start = strpos($src, 'private function protokolliere(');
        $body  = substr($src, (int)$start, 700);
        assertTrue(str_contains($body, 'if ($dryRun) {'), 'Ein Probelauf wird protokolliert');

        // Und er überschreibt den letzten Stand nicht.
        $ctrl = $quelle('src/Controllers/Admin/RelutionAdminController.php');
        assertTrue(str_contains($ctrl, 'if (!$dryRun) {'), 'Ein Probelauf überschreibt den letzten Stand');
    });

    test('Jeder Lauf steht an seiner Instanz und im Protokoll', function () use ($quelle) {
        $src = $quelle('src/Services/RelutionSyncRunner.php');
        assertTrue(str_contains($src, '$manager->recordRun($id, $ok, $meldung);'), 'Das Ergebnis bleibt nicht an der Instanz');
        assertTrue(str_contains($src, "'relution_sync_failed'"), 'Ein Fehlschlag wird nicht protokolliert');
    });

    // ----------------------------------------------------------------
    // Entfernen
    // ----------------------------------------------------------------

    test('Das Entfernen einer Instanz löscht keine Geräte', function () use ($quelle) {
        // Sie hängen an Personen, Tickets, Verträgen und Racks.
        $src   = $quelle('src/Services/MdmConnectionManager.php');
        $start = strpos($src, 'public function delete(int $id): int');
        $body  = substr($src, (int)$start, 700);

        assertTrue(str_contains($body, 'SET mdm_connection_id = NULL'), 'Die Herkunft bleibt stehen');
        assertFalse(str_contains($body, "delete('assets'"), 'Die Geräte werden gelöscht');
        assertTrue(str_contains($body, "delete('mdm_connections', 'id = ?'"), 'Die Instanz bleibt stehen');
    });

    // ----------------------------------------------------------------
    // Zugangsdaten
    // ----------------------------------------------------------------

    test('Das Token steht verschlüsselt und nicht im Formular', function () use ($quelle) {
        $mgr = $quelle('src/Services/MdmConnectionManager.php');
        assertTrue(str_contains($mgr, "public const SECRET_FIELDS = ['access_token']"), 'Das Geheimnis ist nicht benannt');
        assertTrue(str_contains($mgr, 'CredentialCrypto::encrypt('), 'Das Token geht im Klartext in die Datenbank');
        assertTrue(str_contains($mgr, "\$eingabe !== ''"), 'Eine leere Eingabe löscht das hinterlegte Token');

        $tpl = $quelle('templates/admin/inventory/relution.php');
        assertTrue(str_contains($tpl, 'name="relution_access_token"'), 'Das Feld fehlt');
        assertFalse(
            str_contains($tpl, 'name="relution_access_token" class="form-control" value='),
            'Das Token wird in die Seite geschrieben'
        );
    });

    // ----------------------------------------------------------------
    // Übernahme der bestehenden Instanz
    // ----------------------------------------------------------------

    test('Die bestehende Instanz wird vollständig übernommen', function () use ($quelle) {
        // Anders als beim Verzeichnis lagen die Werte hier schon in der
        // Datenbank – die Übernahme läuft deshalb ganz in der Migration, ohne
        // dass jemand etwas neu eintragen müsste.
        $sql = $quelle('database/migrations/225_mdm_connections.sql');

        assertTrue(str_contains($sql, 'CREATE TABLE IF NOT EXISTS `mdm_connections`'), 'Die Tabelle fehlt');
        assertTrue(str_contains($sql, 'ADD COLUMN `mdm_connection_id`'), 'Die Herkunft fehlt am Gerät');

        // Jeder frühere Einstellungsschlüssel muss mitkommen, sonst geht beim
        // Update eine Einstellung verloren.
        foreach ([
            'relution_server_url', 'relution_access_token', 'relution_platforms',
            'relution_platforms_all', 'relution_statuses', 'relution_default_category_id',
            'relution_decommission_missing', 'relution_category_smartphone',
            'relution_category_tablet', 'relution_category_other',
            'relution_decommission_grace_days', 'relution_decommission_max_percent',
        ] as $schluessel) {
            assertTrue(str_contains($sql, "'" . $schluessel . "'"), $schluessel . ' wird nicht übernommen');
        }

        // Und die vorhandenen Geräte bekommen ihre Herkunft – sonst liefe der
        // erste Abgleich gegen einen leeren Bestand und legte alles neu an.
        assertTrue(str_contains($sql, 'SET a.`mdm_connection_id` = c.`id`'), 'Bestehende Geräte bleiben ohne Herkunft');
        assertTrue(str_contains($sql, 'WHERE a.`mdm_connection_id` IS NULL'), 'Die Zuordnung ist nicht wiederholbar');
    });

    test('Ohne bestehende Einrichtung entsteht keine leere Instanz', function () use ($quelle) {
        $sql = $quelle('database/migrations/225_mdm_connections.sql');
        assertTrue(
            str_contains($sql, "WHERE `key` = 'relution_server_url' AND `value` <> ''"),
            'Es entsteht eine Instanz ohne Einrichtung'
        );
    });

    test('Das Altskript läuft nicht mehr an den Instanzen vorbei', function () use ($quelle) {
        // Nach der Migration tragen alle Geräte eine Instanz; das Skript hätte
        // in seiner früheren Form nichts mehr gefunden und alles neu angelegt.
        $cli = $quelle('bin/relution-sync.php');
        assertTrue(str_contains($cli, 'RelutionSyncRunner'), 'Das Skript geht nicht über den Runner');
        assertFalse(str_contains($cli, 'relution_server_url'), 'Das Skript liest noch die alten Einstellungen');
    });

    // ----------------------------------------------------------------
    // Oberfläche
    // ----------------------------------------------------------------

    test('Die Instanzen sind im Backend erreichbar und gesichert', function () use ($quelle) {
        $app = $quelle('src/Core/Application.php');
        foreach (['index', 'form', 'save', 'testConnection', 'preview', 'runSync', 'destroy'] as $aktion) {
            assertTrue(str_contains($app, 'RelutionAdminController@' . $aktion), 'Route fehlt: ' . $aktion);
        }

        // Die feste Strecke muss vor der {id}-Strecke stehen.
        $neu = strpos($app, '/admin/inventory/relution/new');
        $id  = strpos($app, '/admin/inventory/relution/{id}/edit');
        assertTrue($neu !== false && $id !== false && $neu < $id, 'Die {id}-Route fängt „new" ab');

        // Jede Route hängt am Recht für die Inventar-Verwaltung.
        assertSame(
            substr_count($app, 'RelutionAdminController@'),
            substr_count($app, "'capability' => 'admin_assets']);", 0) > 0
                ? substr_count(
                    implode("\n", array_filter(
                        explode("\n", $app),
                        fn (string $z): bool => str_contains($z, 'RelutionAdminController@')
                    )),
                    "'capability' => 'admin_assets'"
                )
                : 0,
            'Nicht jede Route ist gesichert'
        );
    });

    test('Das Gerät zeigt, aus welcher Instanz es stammt', function () use ($quelle) {
        // Bei mehreren Instanzen sagt „Relution" allein nicht, aus welcher.
        $ctrl = $quelle('src/Controllers/AssetController.php');
        assertTrue(str_contains($ctrl, 'mc.name AS mdm_connection_name'), 'Der Name wird nicht geladen');
        assertTrue(str_contains($ctrl, 'LEFT JOIN mdm_connections mc'), 'Die Instanz wird nicht verbunden');

        $tpl = $quelle('templates/assets/show.php');
        assertTrue(str_contains($tpl, "\$asset['mdm_connection_name']"), 'Die Instanz steht nicht am Gerät');
    });

    test('Die Einstellungen einer Instanz bleiben auf die bekannten Felder begrenzt', function () {
        // Sonst landete beliebiger Formularinhalt in `settings_json`.
        $ref  = new ReflectionClass(MdmConnectionManager::class);
        $prop = $ref->getConstant('SECRET_FIELDS');
        assertSame(['access_token'], $prop);

        $felder = $ref->getReflectionConstant('FIELDS');
        assertTrue($felder !== false, 'Die Feldliste fehlt');
    });
};