OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

TicketColumnsTest.php

12 KB
<?php

declare(strict_types=1);

/**
 * Die frei wählbaren Spalten der Ticketliste.
 *
 * Drei Zusagen stehen hier im Mittelpunkt. Die Auswahl gehört der einzelnen
 * Person und niemandem sonst. Eine eingeblendete Spalte lässt sich filtern –
 * und ein gesetzter Filter bleibt sichtbar, auch wenn seine Spalte fort ist,
 * damit die Liste nie unsichtbar gefiltert wird. Und jeder Filter wirkt auf
 * Liste *und* Zählung, sonst zeigte die Seitenzahl einen anderen Bestand als
 * die Zeilen darunter.
 */

use OpenNIT\Tickets\TicketColumns;

return static function (): void {

    $wurzel = dirname(__DIR__);
    $quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);

    // ----------------------------------------------------------------
    // Der Katalog
    // ----------------------------------------------------------------

    test('Jede Spalte ist vollständig beschrieben', function () {
        foreach (TicketColumns::COLUMNS as $schluessel => $spalte) {
            foreach (['label', 'hint', 'default', 'sort', 'filter', 'braucht'] as $feld) {
                assertTrue(array_key_exists($feld, $spalte), 'Der Spalte ' . $schluessel . ' fehlt ' . $feld);
            }
            assertTrue(trim((string)$spalte['label']) !== '', 'Die Spalte ' . $schluessel . ' hat keine Überschrift');
            assertTrue(
                in_array($spalte['filter'], ['select', 'bucket', 'none'], true),
                'Die Spalte ' . $schluessel . ' hat eine unbekannte Filterart'
            );
            assertTrue(
                (bool)preg_match('/^[a-z][a-z_]{1,29}$/', (string)$schluessel),
                'Der Schlüssel ' . $schluessel . ' taugt nicht als Feldname'
            );
        }
    });

    test('Jeder Filter mit fester Liste hat auch Werte', function () {
        // Ein Auswahlfeld ohne Werte wäre ein Feld, das nichts tut.
        foreach (TicketColumns::COLUMNS as $schluessel => $spalte) {
            if ($spalte['filter'] === 'bucket') {
                assertTrue(
                    !empty(TicketColumns::FILTER_OPTIONS[$schluessel]),
                    'Dem Zeitraum-Filter ' . $schluessel . ' fehlen die Werte'
                );
            }
        }
        // Umgekehrt: keine Werte ohne zugehörige Spalte.
        foreach (array_keys(TicketColumns::FILTER_OPTIONS) as $schluessel) {
            assertTrue(
                isset(TicketColumns::COLUMNS[$schluessel]),
                'Zu den Werten von ' . $schluessel . ' gibt es keine Spalte'
            );
        }
    });

    // ----------------------------------------------------------------
    // Die persönliche Auswahl
    // ----------------------------------------------------------------

    test('Ohne eigene Auswahl gilt der Standard', function () {
        $erwartet = array_keys(array_filter(
            TicketColumns::COLUMNS,
            static fn (array $s): bool => $s['default'] && $s['braucht'] === null
        ));

        assertSame(array_values($erwartet), TicketColumns::sichtbar(null));
        assertSame(array_values($erwartet), TicketColumns::sichtbar(''));
    });

    test('Mit eigener Auswahl zählt nur sie', function () {
        // Auch dann, wenn dadurch eine Standard-Spalte verschwindet.
        assertSame(['priority', 'due'], TicketColumns::sichtbar('due,priority'));
        assertSame(['status'], TicketColumns::sichtbar('status'));
    });

    test('Keine Spalte ist eine gültige Auswahl', function () {
        // Sonst ließe sich die letzte Spalte nie abwählen: Ein leerer Wert
        // brächte den Standard zurück.
        assertSame([], TicketColumns::sichtbar('-'));
        assertSame([], TicketColumns::sichtbar('gibtesnicht'));
    });

    test('Die Reihenfolge kommt aus dem Katalog, nicht aus der Eingabe', function () {
        // Sonst bestimmte die Reihenfolge der Häkchen im Formular, wie die
        // Tabelle aussieht.
        $katalog = array_keys(TicketColumns::COLUMNS);
        $paar    = array_slice($katalog, 0, 2);

        assertSame($paar, TicketColumns::sichtbar(implode(',', array_reverse($paar))));
        assertSame($paar, TicketColumns::normalisieren(array_reverse($paar)));
    });

    test('Eine abgeschaltete Funktion bietet ihre Spalte nicht an', function () {
        // Eine Spalte, die immer leer bliebe, gehört nicht in die Auswahl.
        $mitSchalter = ['sentiment' => true, 'time' => true];

        assertFalse(TicketColumns::verfuegbar('sentiment'), 'Die Stimmung wird ohne die Funktion angeboten');
        assertTrue(TicketColumns::verfuegbar('sentiment', $mitSchalter));
        assertFalse(TicketColumns::verfuegbar('time'), 'Die erfasste Zeit wird ohne die Funktion angeboten');
        assertTrue(TicketColumns::verfuegbar('time', $mitSchalter));

        // Auch dann nicht, wenn sie ausdrücklich gewählt wurde.
        assertSame([], TicketColumns::sichtbar('sentiment,time'));
        assertSame(['sentiment', 'time'], TicketColumns::sichtbar('sentiment,time', $mitSchalter));
        assertSame([], TicketColumns::normalisieren(['sentiment']));
    });

    test('Eine unbekannte Spalte überlebt das Speichern nicht', function () {
        assertSame(['status'], TicketColumns::normalisieren(['status', 'boese', '../etc/passwd']));
    });

    // ----------------------------------------------------------------
    // Filterleiste
    // ----------------------------------------------------------------

    test('Die Filterleiste folgt der Spaltenauswahl', function () {
        $leiste = TicketColumns::filterleiste(['priority', 'due'], []);

        assertSame(['priority', 'due'], $leiste, 'Die Leiste zeigt nicht die gewählten Spalten');
    });

    test('Eine Spalte ohne Filter taucht in der Leiste nicht auf', function () {
        // Der Status hat seine eigenen Reiter über der Liste.
        assertSame([], TicketColumns::filterleiste(['status'], []));
        assertSame('none', TicketColumns::COLUMNS['status']['filter'], 'Der Status hat plötzlich ein eigenes Feld');
    });

    test('Ein gesetzter Filter bleibt sichtbar, auch ohne seine Spalte', function () {
        // Sonst filterte die Liste weiter, ohne dass es noch jemand sähe.
        $leiste = TicketColumns::filterleiste(['priority'], ['category' => '7']);

        assertTrue(in_array('category', $leiste, true), 'Ein gesetzter Filter verschwindet mit seiner Spalte');
        assertTrue(in_array('priority', $leiste, true), 'Die gewählte Spalte fehlt');
    });

    test('Ein leerer Wert hält keinen Filter in der Leiste', function () {
        assertSame([], TicketColumns::filterleiste([], ['category' => '', 'due' => '   ']));
    });

    // ----------------------------------------------------------------
    // Wirkung auf die Abfrage
    // ----------------------------------------------------------------

    test('Jeder Filter wirkt auf Liste und Zählung', function () use ($quelle) {
        // Sonst zeigt die Liste gefilterte Zeilen und die Seitenzahl den
        // ungefilterten Bestand.
        $tm = $quelle('src/Tickets/TicketManager.php');
        assertSame(
            2,
            substr_count($tm, '$this->applyColumnFilters($filters, $where, $params);'),
            'Die Spaltenfilter greifen nicht in beiden Abfragen'
        );
    });

    test('Nur bekannte Werte formen die Abfrage', function () use ($quelle) {
        $tm    = $quelle('src/Tickets/TicketManager.php');
        $start = (int)strpos($tm, 'private function applyColumnFilters(');
        $body  = substr($tm, $start, 3200);

        assertTrue(
            str_contains($body, 'isset(TicketColumns::FILTER_OPTIONS[$schluessel][$w])'),
            'Ein beliebiger Wert landet in der Abfrage'
        );
        // Jeder Filter des Katalogs wird auch wirklich umgesetzt.
        foreach (['due', 'created', 'updated', 'sla', 'sentiment', 'time'] as $schluessel) {
            assertTrue(str_contains($body, "'" . $schluessel . "'"), 'Der Filter ' . $schluessel . ' wird nicht umgesetzt');
        }
    });

    test('Die Summe der Zeiten wird nur bei sichtbarer Spalte gelesen', function () use ($quelle) {
        // Eine Gruppierung über alle Zeiteinträge kostet; sie gehört nicht in
        // jede Listenabfrage.
        $tm = $quelle('src/Tickets/TicketManager.php');
        assertTrue(
            str_contains($tm, "\$zeigtZeit = in_array('time', (array)(\$filters['_columns'] ?? []), true);"),
            'Die Summe hängt nicht an der Spaltenauswahl'
        );
        assertTrue(str_contains($tm, "\$zeitJoin  = \$zeigtZeit"), 'Die Verknüpfung läuft immer mit');

        // Und der Filter kommt ohne sie aus.
        $start = (int)strpos($tm, 'private function applyColumnFilters(');
        $body  = substr($tm, $start, 3200);
        assertTrue(str_contains($body, 'EXISTS (SELECT 1 FROM ticket_time_entries'), 'Der Zeit-Filter braucht die Verknüpfung');
    });

    // ----------------------------------------------------------------
    // Zugang und Oberfläche
    // ----------------------------------------------------------------

    test('Die Auswahl gehört der einzelnen Person', function () use ($quelle) {
        $ctrl = $quelle('src/Controllers/TicketController.php');
        assertTrue(
            str_contains($ctrl, '$this->setUserSetting(TicketColumns::SETTING'),
            'Die Auswahl wird nicht persönlich gespeichert'
        );
        assertTrue(
            str_contains($ctrl, '$this->getUserSetting(TicketColumns::SETTING)'),
            'Die Auswahl wird nicht persönlich gelesen'
        );
        assertTrue(
            str_contains($ctrl, "\$this->authorize('view_tickets');"),
            'Das Speichern prüft kein Recht'
        );

        // Der Rückweg kommt aus dem Formular und wird auf die eigene Anwendung begrenzt.
        assertTrue(str_contains($ctrl, 'safeLocalReturn('), 'Der Rückweg wird nicht begrenzt');

        $app = $quelle('src/Core/Application.php');
        $zeile = strstr($app, "TicketController@saveColumns'");
        assertTrue(is_string($zeile), 'Die Route fehlt');
        assertTrue(
            str_contains(substr($zeile, 0, 160), "'capability' => 'view_tickets'"),
            'Die Route hängt an keinem Recht'
        );
    });

    test('Eine gemerkte Standort-Spalte geht beim Umstieg nicht verloren', function () use ($quelle) {
        // Die Spalte war bisher einzeln umschaltbar; wer sie eingeschaltet
        // hatte, soll sie behalten.
        $ctrl = $quelle('src/Controllers/TicketController.php');
        assertTrue(
            str_contains($ctrl, "\$this->getUserSetting('ticket_show_location_col', '0') === '1'"),
            'Die frühere Einstellung wird nicht übernommen'
        );
        assertTrue(
            str_contains($ctrl, "array_merge(TicketColumns::sichtbar(null, \$spaltenSchalter), ['location'])"),
            'Die Standort-Spalte wird nicht zum Standard hinzugenommen'
        );
    });

    test('Das Modal bietet jede verfügbare Spalte an', function () use ($quelle) {
        $vorlage = $quelle('templates/tickets/index.php');
        assertTrue(str_contains($vorlage, 'id="tkColsOverlay"'), 'Der Dialog fehlt');
        assertTrue(str_contains($vorlage, 'action="/tickets/columns"'), 'Der Dialog speichert nirgends');
        assertTrue(str_contains($vorlage, 'name="_csrf"'), 'Dem Formular fehlt der Token');
        assertTrue(str_contains($vorlage, 'name="col_<?= $this->e((string)$cKey) ?>"'), 'Die Häkchen folgen nicht dem Katalog');
        assertTrue(str_contains($vorlage, 'tkColsOpen()'), 'Der Dialog lässt sich nicht öffnen');
        assertTrue(str_contains($vorlage, 'tkColsEsc'), 'Der Dialog lässt sich nicht per Escape schließen');

        // Kopfzeile und Zellen folgen derselben Auswahl.
        assertTrue(str_contains($vorlage, 'foreach ($columns as $colKey): $col = $column_catalog[$colKey];'), 'Die Kopfzeile folgt nicht der Auswahl');
        assertTrue(str_contains($vorlage, 'foreach ($columns as $colKey): ?><?= $tkZelle('), 'Die Zellen folgen nicht der Auswahl');
    });

    test('Jede Spalte wird in der Zeile auch dargestellt', function () use ($quelle) {
        // Eine Spalte ohne Fall in der Zellen-Funktion bliebe stumm leer.
        $vorlage = $quelle('templates/tickets/index.php');
        $start   = (int)strpos($vorlage, '$tkZelle = function');
        $body    = substr($vorlage, $start, 4000);

        foreach (array_keys(TicketColumns::COLUMNS) as $schluessel) {
            assertTrue(
                str_contains($body, "case '" . $schluessel . "':"),
                'Für die Spalte ' . $schluessel . ' fehlt die Darstellung'
            );
        }
    });
};