OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

TopicPathVisibilityTest.php

5 KB
<?php

declare(strict_types=1);

use OpenNIT\Authorization\CapabilityChecker;

/**
 * Deckt den Weg zu einem zugänglichen Themengebiet ab.
 *
 * Liegt ein lesbares Themengebiet tief im Baum, müssen die übergeordneten
 * Gebiete als Weg sichtbar sein – ohne dass ihre eigenen Seiten zugänglich
 * werden. Deshalb steht der Weg bewusst neben der Liste der zugänglichen
 * Gebiete, an der Suche, Export und KI-Kontext hängen.
 */
return static function (): void {

    /**
     * Datenbank-Doppel für einen Benutzer mit einer standortgebundenen Rolle.
     *
     * @param array<int,array{path:string,locs:int[]}> $themen
     * @param int[]                                    $zugewieseneStandorte
     */
    $db = function (array $themen, array $zugewieseneStandorte): object {
        return new class ($themen, $zugewieseneStandorte) extends \OpenNIT\Core\Database {
            public function __construct(private array $themen, private array $standorte)
            {
                // Verbindung bewusst nicht aufbauen – die Abfragen werden bedient.
            }

            public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed
            {
                if (str_contains($sql, 'INFORMATION_SCHEMA')) {
                    return 1;               // scope_type ist vorhanden
                }
                if (str_contains($sql, 'SELECT is_active FROM users')) {
                    return 1;
                }
                if (str_contains($sql, 'SELECT is_admin FROM users')) {
                    return 0;               // kein globaler Admin
                }
                return 0;                   // keine globale/themengebundene Zuweisung
            }

            public function fetchAll(string $sql, array $params = []): array
            {
                if (str_contains($sql, 'urt.location_id FROM user_role_topics')) {
                    return array_map(static fn($id) => ['location_id' => $id], $this->standorte);
                }
                if (str_contains($sql, 'user_role_topics')) {
                    return [];              // keine themengebundene Zuweisung
                }
                if (str_contains($sql, 'FROM ticket_locations')) {
                    return [['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => null]];
                }
                if (str_contains($sql, 'FROM topic_locations')) {
                    $rows = [];
                    foreach ($this->themen as $id => $t) {
                        foreach ($t['locs'] as $loc) {
                            $rows[] = ['topic_id' => $id, 'location_id' => $loc];
                        }
                    }
                    return $rows;
                }
                if (str_contains($sql, 'FROM topics')) {
                    $rows = [];
                    foreach ($this->themen as $id => $t) {
                        $rows[] = [
                            'id'          => $id,
                            'name'        => 'Thema ' . $id,
                            'path'        => $t['path'],
                            'location_id' => $t['locs'][0] ?? null,
                        ];
                    }

                    // Die Abfrage mit IN-Liste liefert nur die angefragten Gebiete.
                    if (str_contains($sql, 'id IN (')) {
                        $erlaubt = array_fill_keys(array_map('intval', $params), true);
                        return array_values(array_filter($rows, static fn($r) => isset($erlaubt[$r['id']])));
                    }
                    return $rows;
                }
                return [];
            }
        };
    };

    // Kunde Müller (Standort 1) mit tief liegender gemeinsamer Anleitung, die
    // auch Kunde Schulze (Standort 2) zugeordnet ist.
    $themen = [
        10 => ['path' => '/10/',          'locs' => [1]],
        11 => ['path' => '/10/11/',       'locs' => []],
        12 => ['path' => '/10/11/12/',    'locs' => [1, 2]],
        20 => ['path' => '/20/',          'locs' => [2]],
    ];

    test('Der Weg zu einem zugänglichen Themengebiet ist sichtbar', function () use ($db, $themen) {
        $checker = new CapabilityChecker($db($themen, [2]));

        $zugaenglich = array_column($checker->getTopicsWithCapability(7, 'read_page'), 'id');
        sort($zugaenglich);
        assertSame([12, 20], $zugaenglich, 'Die gemeinsame Anleitung und der eigene Bereich');

        $weg = $checker->getPathTopicIds(7, 'read_page');
        sort($weg);
        assertSame([10, 11], $weg, 'Beide übergeordneten Gebiete führen zur Anleitung');
    });

    test('Der Weg enthält nie ein ohnehin zugängliches Themengebiet', function () use ($db, $themen) {
        $checker = new CapabilityChecker($db($themen, [1]));

        $zugaenglich = array_column($checker->getTopicsWithCapability(7, 'read_page'), 'id');
        sort($zugaenglich);
        assertSame([10, 11, 12], $zugaenglich, 'Kunde Müller sieht seinen ganzen Zweig');

        assertSame([], $checker->getPathTopicIds(7, 'read_page'), 'Kein Weg nötig, alles ist zugänglich');
    });

    test('Ohne zugängliches Themengebiet gibt es auch keinen Weg', function () use ($db, $themen) {
        $checker = new CapabilityChecker($db($themen, [99]));

        assertSame([], $checker->getTopicsWithCapability(7, 'read_page'));
        assertSame([], $checker->getPathTopicIds(7, 'read_page'));
    });
};