Quelltext des aktuellen Standes
<?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);
});
};