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