OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

InfrastructureTest.php

10 KB
<?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']);
    });
};