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