OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

AuditJsonCanonicalTest.php

5 KB
<?php

declare(strict_types=1);

use OpenNIT\Audit\JsonCanonicalizer;

/**
 * Deckt die kanonische Schreibweise der JSON-Felder im Audit-Log ab.
 *
 * Die Prüfsumme eines Eintrags muss seinen Inhalt belegen und nicht die
 * Formatierung, in der die Datenbank den Wert zurückliefert. Geprüft wird
 * deshalb vor allem, dass zwei Schreibweisen desselben Inhalts dieselbe
 * kanonische Form ergeben – und dass inhaltliche Unterschiede erhalten bleiben.
 */
return static function (): void {

    /** Rendert JSON so, wie eine kanonisierende Datenbank es zurückgibt. */
    $wieDatenbank = static function (array $keysSortiert): string {
        $teile = [];
        foreach ($keysSortiert as $k => $v) {
            $teile[] = json_encode((string)$k) . ': ' . json_encode($v, JSON_UNESCAPED_UNICODE);
        }
        return '{' . implode(', ', $teile) . '}';
    };

    test('Schlüsselreihenfolge und Trennzeichen ändern die kanonische Form nicht', function () use ($wieDatenbank) {
        // So schreibt PHP …
        $vonPhp = json_encode(['role_id' => 7, 'role_name' => 'System Admin', 'topic_id' => null], JSON_UNESCAPED_UNICODE);
        // … so gibt eine kanonisierende Datenbank denselben Inhalt zurück
        $vonDb = $wieDatenbank(['role_id' => 7, 'topic_id' => null, 'role_name' => 'System Admin']);

        assertTrue($vonPhp !== $vonDb);
        assertSame(
            JsonCanonicalizer::canonicalize($vonPhp),
            JsonCanonicalizer::canonicalize($vonDb)
        );
    });

    test('Verschachtelte Objekte werden ebenfalls angeglichen', function () {
        $a = '{"b":{"z":1,"a":2},"a":[3,2,1]}';
        $b = '{"a": [3, 2, 1], "b": {"a": 2, "z": 1}}';

        assertSame(JsonCanonicalizer::canonicalize($a), JsonCanonicalizer::canonicalize($b));
    });

    test('Die Reihenfolge in Listen bleibt erhalten', function () {
        $eins = JsonCanonicalizer::canonicalize('{"werte":[3,1,2]}');
        $zwei = JsonCanonicalizer::canonicalize('{"werte":[1,2,3]}');

        assertTrue($eins !== $zwei);
        assertSame('{"werte":[3,1,2]}', $eins);
    });

    test('Inhaltliche Unterschiede überleben die Kanonisierung', function () {
        assertTrue(
            JsonCanonicalizer::canonicalize('{"betrag":100}')
            !== JsonCanonicalizer::canonicalize('{"betrag":101}')
        );
        assertTrue(
            JsonCanonicalizer::canonicalize('{"aktiv":true}')
            !== JsonCanonicalizer::canonicalize('{"aktiv":false}')
        );
    });

    test('Leere und fehlende Werte bleiben unverändert', function () {
        assertSame(null, JsonCanonicalizer::canonicalize(null));
        assertSame('', JsonCanonicalizer::canonicalize(''));
        assertSame('[]', JsonCanonicalizer::canonicalize('[]'));
        assertSame('{}', JsonCanonicalizer::canonicalize('{}'));
    });

    test('Kein gültiges JSON bleibt Rohtext', function () {
        assertSame('kein json', JsonCanonicalizer::canonicalize('kein json'));
        assertSame('{"offen":', JsonCanonicalizer::canonicalize('{"offen":'));
    });

    test('Die Schreibweise entspricht der des Loggers', function () {
        // Umlaute bleiben lesbar, Schrägstriche werden escaped – wie beim Schreiben.
        $kanonisch = JsonCanonicalizer::canonicalize('{"pfad": "/admin/audit", "ort": "Münster"}');
        assertSame('{"ort":"Münster","pfad":"\\/admin\\/audit"}', $kanonisch);

        // Ein unveränderter Wert ergibt exakt den Text, den PHP geschrieben hat
        $vonPhp = json_encode(['ort' => 'Münster', 'pfad' => '/admin/audit'], JSON_UNESCAPED_UNICODE);
        assertSame($vonPhp, $kanonisch);
    });

    test('Die Abweichungs-Erkennung trennt umformatierte von unveränderten Werten', function () use ($wieDatenbank) {
        // Von der Datenbank umformatiert zurückgegeben
        assertTrue(JsonCanonicalizer::differsFromCanonical($wieDatenbank(['a' => 1, 'b' => 2])));
        assertTrue(JsonCanonicalizer::differsFromCanonical('{"zebra":1,"a":2}'));

        // Bereits kanonisch – kein Hinweis auf eine Umformatierung
        assertFalse(JsonCanonicalizer::differsFromCanonical('{"a":1,"b":2}'));
        assertFalse(JsonCanonicalizer::differsFromCanonical('{"method":"local"}'));

        // Ohne Inhalt gibt es nichts umzuformatieren
        assertFalse(JsonCanonicalizer::differsFromCanonical(null));
        assertFalse(JsonCanonicalizer::differsFromCanonical('[]'));
        assertFalse(JsonCanonicalizer::differsFromCanonical('{}'));
    });

    test('Die kanonische Form ist stabil bei erneuter Anwendung', function () {
        $einmal  = JsonCanonicalizer::canonicalize('{"z": 1, "a": {"y": 2, "b": 3}}');
        $zweimal = JsonCanonicalizer::canonicalize($einmal);

        assertSame($einmal, $zweimal);
        assertSame('{"a":{"b":3,"y":2},"z":1}', $einmal);
    });
};