Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Deckt die Zuordnung von Lizenzplätzen an Geräte und Personen ab.
*
* Der heikle Punkt ist `seats_used`: Bei Lizenzen aus dem Microsoft-365-
* Abgleich gehört die Zahl der Quelle und wird bei jedem Lauf überschrieben.
* Würde die Zuordnung sie mitführen, stritten sich zwei Stellen um denselben
* Wert – und die lokale Zählung verlöre bei jedem Abgleich.
*
* Der zweite Punkt ist die Zählung selbst: Die Zuordnung zeigt auf Geräte oder
* Personen und kann deshalb keinen Fremdschlüssel tragen. Verschwindet das
* Ziel, bleibt eine Zeile ohne Gegenstück zurück, die weder in der Liste noch
* in der Zahl auftauchen darf.
*/
return static function (): void {
$wurzel = dirname(__DIR__);
$manager = (string)file_get_contents($wurzel . '/src/Content/LicenseManager.php');
$ctrl = (string)file_get_contents($wurzel . '/src/Controllers/LicenseController.php');
$sql = (string)file_get_contents($wurzel . '/database/migrations/219_license_assignments.sql');
// ----------------------------------------------------------------
// Die abgeglichene Belegung bleibt unberührt
// ----------------------------------------------------------------
test('Eine Zuordnung rechnet seats_used nicht fort', function () use ($manager) {
// Weder assign() noch unassign() dürfen die gepflegte Zahl anfassen –
// sonst überschreiben sich Abgleich und Zuordnung gegenseitig.
foreach (['assign', 'unassign'] as $methode) {
$start = strpos($manager, 'public function ' . $methode . '(');
assertTrue($start !== false, "Methode {$methode} fehlt");
$ende = strpos($manager, "\n /**", $start);
$body = substr($manager, $start, ($ende ?: strlen($manager)) - $start);
assertFalse(str_contains($body, 'seats_used'), "{$methode}() fasst seats_used an");
}
});
test('Die Übernahme ist abgeglichenen Lizenzen verwehrt', function () use ($ctrl) {
// Bei ihnen käme die Zahl beim nächsten Lauf ohnehin zurück; die
// Schaltfläche würde also etwas versprechen, das nicht hält.
$start = strpos($ctrl, 'public function seatsFromAssignments(');
assertTrue($start !== false, 'Die Übernahme fehlt');
$body = substr($ctrl, $start, 1200);
// `external_source` ist NOT NULL DEFAULT 'manual' – also nie leer. Eine
// Prüfung auf „gesetzt" träfe deshalb auf jede Lizenz zu und verwehrte
// die Übernahme auch dort, wo sie gerade gedacht ist.
assertFalse(str_contains($body, "!empty(\$entry['external_source'])"), 'Die Prüfung trifft auf jede Lizenz zu');
assertTrue(
str_contains($body, "(\$entry['external_source'] ?? 'manual') !== 'manual'"),
'Selbst gepflegte Lizenzen werden nicht als solche erkannt'
);
// Die Prüfung muss vor dem Schreiben stehen.
$pruefung = strpos($body, "external_source");
$schreiben = strpos($body, 'setSeatsUsed(');
assertTrue($pruefung !== false && $schreiben !== false && $pruefung < $schreiben, 'Es wird vor der Prüfung geschrieben');
});
test('Die Seite unterscheidet selbst gepflegte von abgeglichenen Lizenzen', function () use ($wurzel) {
// Dieselbe Falle wie oben: `!empty()` liefert für jede Lizenz true und
// versteckte damit die Übernahme-Schaltfläche überall.
$seite = (string)file_get_contents($wurzel . '/templates/licenses/show.php');
assertTrue(
str_contains($seite, "\$abgeglichen = (\$e['external_source'] ?? 'manual') !== 'manual';"),
'Die Seite erkennt selbst gepflegte Lizenzen nicht'
);
});
test('Die Belegung selbst gepflegter Lizenzen lässt sich übernehmen', function () use ($wurzel) {
$seite = (string)file_get_contents($wurzel . '/templates/licenses/show.php');
assertTrue(str_contains($seite, '/seats-from-assignments'), 'Es gibt keinen Weg zur Übernahme');
// Nur im Zweig für selbst gepflegte Lizenzen.
$stelle = strpos($seite, '/seats-from-assignments');
$davor = substr($seite, max(0, $stelle - 700), 700);
assertTrue(str_contains($davor, '<?php else: ?>'), 'Die Übernahme steht nicht im Zweig der selbst gepflegten Lizenzen');
});
// ----------------------------------------------------------------
// Zählung und Liste laufen nicht auseinander
// ----------------------------------------------------------------
test('Verwaiste Zuordnungen zählen nicht mit', function () use ($manager) {
// Liste und Zahl müssen denselben Verbund nutzen, sonst zeigt die Seite
// „3 zugewiesen" und darunter zwei Zeilen.
foreach (['getAssignments', 'countAssignments'] as $methode) {
$start = strpos($manager, 'public function ' . $methode . '(');
$body = substr($manager, $start, 1200);
assertTrue(str_contains($body, 'a.id IS NOT NULL OR p.id IS NOT NULL'), "{$methode}() lässt verwaiste Zeilen durch");
}
});
test('Dieselbe Lizenz belegt ein Ziel nur einmal', function () use ($sql, $manager) {
// Ein zweiter Klick darf keinen zweiten Platz erzeugen.
assertTrue(str_contains($sql, 'UNIQUE KEY `uq_license_subject`'), 'Der Datenbank fehlt die Eindeutigkeit');
$start = strpos($manager, 'public function assign(');
$body = substr($manager, $start, 2000);
assertTrue(str_contains($body, 'return false;'), 'Eine bestehende Zuordnung wird nicht erkannt');
});
test('Die Zuordnung verschwindet mit ihrer Lizenz', function () use ($sql) {
assertTrue(str_contains($sql, 'ON DELETE CASCADE'), 'Gelöschte Lizenzen lassen Zuordnungen zurück');
});
// ----------------------------------------------------------------
// Eingaben
// ----------------------------------------------------------------
test('Nur bekannte Zieltypen werden angenommen', function () use ($manager) {
$start = strpos($manager, 'public function assign(');
$body = substr($manager, $start, 2000);
assertTrue(str_contains($body, "in_array(\$subjectType, ['asset', 'person'], true)"), 'Der Zieltyp wird nicht geprüft');
assertTrue(str_contains($body, 'InvalidArgumentException'), 'Ein unbekannter Typ wird stillschweigend hingenommen');
});
test('Ein nicht vorhandenes Ziel wird abgewiesen', function () use ($manager) {
// Sonst entstünde eine Zuordnung, die nirgends auftaucht – die Liste
// blendet sie aus, die Zahl ebenso.
$start = strpos($manager, 'public function assign(');
$body = substr($manager, $start, 2000);
assertTrue(str_contains($body, 'Das Ziel der Zuordnung gibt es nicht.'), 'Das Ziel wird nicht geprüft');
});
test('Der Tabellenname für das Ziel kommt nicht aus der Eingabe', function () use ($manager) {
// Der Name steht in der Abfrage und muss daher aus dem Code stammen.
$start = strpos($manager, 'public function assign(');
$body = substr($manager, $start, 2000);
assertTrue(
str_contains($body, "\$tabelle = \$subjectType === 'asset' ? 'assets' : 'directory_persons';"),
'Der Tabellenname wird nicht aus dem Code abgeleitet'
);
});
// ----------------------------------------------------------------
// Protokoll und Bedienung
// ----------------------------------------------------------------
test('Zuweisen und Aufheben stehen im Protokoll', function () use ($manager) {
foreach (['license_assigned', 'license_unassigned', 'license_seats_recounted'] as $vorgang) {
assertTrue(str_contains($manager, "'{$vorgang}'"), "Vorgang {$vorgang} wird nicht protokolliert");
}
// Der Eintrag nennt das Ziel, nicht nur dessen Kennung.
assertTrue(str_contains($manager, 'subjectLabel('), 'Das Protokoll benennt das Ziel nicht');
});
test('Beide Richtungen sind sichtbar', function () use ($wurzel) {
// Nur von der Lizenz aus wäre die Zuordnung halb so nützlich: Die Frage
// lautet meist „welche Lizenzen laufen auf diesem Gerät".
$lizenzseite = (string)file_get_contents($wurzel . '/templates/licenses/show.php');
assertTrue(str_contains($lizenzseite, 'Zugewiesen an'), 'Die Lizenzseite zeigt die Zuordnung nicht');
$geraeteseite = (string)file_get_contents($wurzel . '/templates/assets/show.php');
assertTrue(str_contains($geraeteseite, '$assigned_licenses'), 'Die Geräteseite zeigt die Lizenzen nicht');
$assetCtrl = (string)file_get_contents($wurzel . '/src/Controllers/AssetController.php');
assertTrue(str_contains($assetCtrl, "getAssignedLicenses('asset'"), 'Die Geräteseite lädt die Lizenzen nicht');
// Nur bei aktivem Modul und passender Berechtigung.
assertTrue(str_contains($assetCtrl, "(\$modules['licenses'] ?? false) && \$this->can('view_licenses')"), 'Modul und Berechtigung werden nicht geprüft');
});
test('Die Geräte-Suche folgt den Standort-Rechten des Inventars', function () use ($ctrl) {
// Die Suche gibt Inventar- und Seriennummern heraus.
$start = strpos($ctrl, 'public function apiAssetSearch(');
assertTrue($start !== false, 'Die Geräte-Suche fehlt');
$body = substr($ctrl, $start, 1500);
assertTrue(str_contains($body, "\$this->can('view_assets')"), 'Ohne Inventar-Recht gäbe es Treffer');
assertTrue(str_contains($body, "locationScopeSql('view_assets'"), 'Die Standortgrenzen werden übergangen');
});
};