Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Infrastructure\RackManager;
/**
* Deckt die rechnende Logik des Infrastruktur-Moduls ab.
*
* Schwerpunkte: die Standort-Begrenzung (Racks fremder Standorte dürfen weder
* auftauchen noch über eine Direkt-ID erreichbar sein), die Freiplatzsuche und
* die Bilanz der Stromeinspeisungen.
*/
return static function (): void {
/**
* Datenbank-Doppel: liefert Standorte, Racks und Einbauten aus dem Speicher.
*
* @param array<int,array<string,mixed>> $racks
* @param array<int,array<string,mixed>> $mounts
*/
$db = function (array $racks, array $mounts = [], array $locations = []): object {
return new class ($racks, $mounts, $locations) extends \OpenNIT\Core\Database {
public function __construct(
private array $racks,
private array $mounts,
private array $locations
) {
}
public function fetchAll(string $sql, array $params = []): array
{
if (str_contains($sql, 'FROM ticket_locations')) {
// Die Standort-Bedingung steckt als IN-Liste in den Parametern
if (str_contains($sql, 'IN (') && $params !== []) {
$ids = array_map('intval', $params);
return array_values(array_filter(
$this->locations,
static fn($l) => in_array((int)$l['id'], $ids, true)
));
}
return str_contains($sql, '0=1') ? [] : $this->locations;
}
if (str_contains($sql, 'FROM infrastructure_racks')) {
if (str_contains($sql, '0=1')) {
return [];
}
if (str_contains($sql, 'IN (') && $params !== []) {
$ids = array_map('intval', $params);
return array_values(array_filter(
$this->racks,
static fn($r) => in_array((int)$r['location_id'], $ids, true)
));
}
return $this->racks;
}
if (str_contains($sql, 'FROM infrastructure_rack_mounts')) {
$rackId = (int)($params[0] ?? 0);
return array_values(array_filter(
$this->mounts,
static fn($m) => (int)$m['rack_id'] === $rackId
));
}
return [];
}
public function fetchOne(string $sql, array $params = []): ?array
{
if (str_contains($sql, 'FROM infrastructure_racks')) {
foreach ($this->racks as $r) {
if ((int)$r['id'] === (int)($params[0] ?? 0)) {
return $r;
}
}
}
return null;
}
};
};
$audit = new class extends \OpenNIT\Audit\AuditLogger {
public function __construct() {}
public function log(
string $actionCategory,
string $actionType,
?string $objectType = null,
?int $objectId = null,
?string $objectName = null,
?array $details = null,
string $result = 'success',
string $riskLevel = 'low',
?array $oldValue = null,
?array $newValue = null
): int {
return 0;
}
};
$locations = [
['id' => 1, 'parent_id' => null, 'name' => 'Kunde A'],
['id' => 2, 'parent_id' => 1, 'name' => 'Serverraum A'],
['id' => 9, 'parent_id' => null, 'name' => 'Kunde B'],
];
$racks = [
['id' => 10, 'location_id' => 2, 'name' => 'Rack A1', 'height_u' => 42, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => null],
['id' => 20, 'location_id' => 9, 'name' => 'Rack B1', 'height_u' => 42, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => 1000],
];
test('Ohne Einschränkung sind alle Racks sichtbar', function () use ($db, $audit, $racks, $locations) {
$rm = new RackManager($db($racks, [], $locations), $audit);
assertTrue($rm->locationVisible(2));
assertTrue($rm->locationVisible(9));
assertSame(2, count($rm->findFreeSpace(1)));
});
test('Eingeschränkte Sicht blendet fremde Standorte aus', function () use ($db, $audit, $racks, $locations) {
$rm = (new RackManager($db($racks, [], $locations), $audit))->withLocationScope([1, 2]);
assertTrue($rm->locationVisible(2));
assertFalse($rm->locationVisible(9));
$frei = $rm->findFreeSpace(1);
assertSame(1, count($frei));
assertSame('Rack A1', $frei[0]['rack']);
});
test('Ein Rack außerhalb der Sicht verhält sich wie nicht vorhanden', function () use ($db, $audit, $racks, $locations) {
$rm = (new RackManager($db($racks, [], $locations), $audit))->withLocationScope([1, 2]);
assertTrue($rm->rackDetail(20) === null);
});
test('Ohne freigegebene Standorte bleibt alles verborgen', function () use ($db, $audit, $racks, $locations) {
$rm = (new RackManager($db($racks, [], $locations), $audit))->withLocationScope([]);
assertFalse($rm->locationVisible(2));
assertSame([], $rm->findFreeSpace(1));
});
test('Freiplatzsuche findet den größten zusammenhängenden Block', function () use ($db, $audit, $locations) {
// Rack mit 10 HE: 1–2 und 5 belegt → größter freier Block ist 6–10 (5 HE)
$racks = [['id' => 10, 'location_id' => 2, 'name' => 'Rack', 'height_u' => 10, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => null]];
$mounts = [
['rack_id' => 10, 'start_u' => 1, 'height_u' => 2, 'side' => 'front', 'full_depth' => 0, 'power_watt' => null, 'weight_kg' => null],
['rack_id' => 10, 'start_u' => 5, 'height_u' => 1, 'side' => 'front', 'full_depth' => 0, 'power_watt' => null, 'weight_kg' => null],
];
$rm = new RackManager($db($racks, $mounts, $locations), $audit);
$frei = $rm->findFreeSpace(4);
assertSame(1, count($frei));
assertSame(5, $frei[0]['block_len']);
assertSame(6, $frei[0]['block_start']);
// Ein 6-HE-Gerät passt nicht mehr
assertSame([], $rm->findFreeSpace(6));
});
test('Rückseitige Belegung blockiert die Vorderseite nicht', function () use ($db, $audit, $locations) {
$racks = [['id' => 10, 'location_id' => 2, 'name' => 'Rack', 'height_u' => 4, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => null]];
$mounts = [
['rack_id' => 10, 'start_u' => 1, 'height_u' => 4, 'side' => 'rear', 'full_depth' => 0, 'power_watt' => null, 'weight_kg' => null],
];
$rm = new RackManager($db($racks, $mounts, $locations), $audit);
assertSame(4, $rm->findFreeSpace(4, null, null, 'front')[0]['block_len']);
assertSame([], $rm->findFreeSpace(1, null, null, 'rear'));
});
test('Volltiefe Einbauten belegen beide Seiten', function () use ($db, $audit, $locations) {
$racks = [['id' => 10, 'location_id' => 2, 'name' => 'Rack', 'height_u' => 4, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => null]];
$mounts = [
['rack_id' => 10, 'start_u' => 1, 'height_u' => 4, 'side' => 'front', 'full_depth' => 1, 'power_watt' => null, 'weight_kg' => null],
];
$rm = new RackManager($db($racks, $mounts, $locations), $audit);
assertSame([], $rm->findFreeSpace(1, null, null, 'rear'));
});
test('Leistungsgrenze des Racks schließt zu große Geräte aus', function () use ($db, $audit, $locations) {
$racks = [['id' => 20, 'location_id' => 2, 'name' => 'Rack', 'height_u' => 10, 'numbering' => 'bottom_up', 'max_load_kg' => null, 'max_power_watt' => 1000]];
$mounts = [
['rack_id' => 20, 'start_u' => 1, 'height_u' => 1, 'side' => 'front', 'full_depth' => 0, 'power_watt' => 800, 'weight_kg' => null],
];
$rm = new RackManager($db($racks, $mounts, $locations), $audit);
assertSame(1, count($rm->findFreeSpace(1, 150))); // 800 + 150 ≤ 1000
assertSame([], $rm->findFreeSpace(1, 400)); // 800 + 400 > 1000
});
test('Ein Kabelende außerhalb der Sicht wird nicht benannt', function () use ($db, $audit) {
// Prüft die Anonymisierung direkt: sichtbarer Standort → Name, fremder → nicht.
$rm = (new RackManager($db([]), $audit))->withLocationScope([2]);
assertTrue($rm->locationVisible(2));
assertFalse($rm->locationVisible(9));
assertFalse($rm->locationVisible(null));
});
test('Einspeisungs-Bilanz rechnet den Ausfall einer Seite durch', function () use ($db, $audit) {
$rm = new RackManager($db([]), $audit);
$ref = new ReflectionMethod($rm, 'feedBalance');
$ref->setAccessible(true);
$bilanz = $ref->invoke($rm, [
['power_feed' => 'a', 'power_watt' => 300],
['power_feed' => 'b', 'power_watt' => 200],
['power_feed' => 'both', 'power_watt' => 400],
['power_feed' => 'none', 'power_watt' => 100],
]);
assertSame(300, $bilanz['a']);
assertSame(200, $bilanz['b']);
assertSame(400, $bilanz['both']);
// Fällt Seite B aus, trägt A die eigenen 300 W plus die 400 W der redundanten
assertSame(700, $bilanz['worst_case']);
assertSame(2, $bilanz['single_feed']);
assertSame(1, $bilanz['unassigned']);
});
};