Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
Jede Display-Seite bringt jetzt ihr eigenes Erscheinungsbild mit – der Empfang sieht anders aus als der Tagungsraum nebenan: - eigenes Logo (leer = Logo aus den Einstellungen) - formatfüllendes Hintergrundbild mit einstellbarer Abdunklung (0–90 %), damit die Karte auf hellen Fotos lesbar bleibt - eigene Akzentfarbe für den Knopf (leer = Farbe aus dem Design-Tab) - Karte wahlweise hell oder dunkel; auf Fotos wirkt dunkel meist ruhiger Logo und Hintergrund lassen sich hochladen oder als URL hinterlegen; beim Löschen einer Display-Seite verschwinden die hochgeladenen Dateien mit. Nebenbei aufgeräumt: das Bildfeld (Vorschau + Upload + URL + Entfernen) lag als Funktion in admin/settings.php und wird jetzt von beiden Seiten genutzt – Ui::imageField() für die Darstellung, Upload::resolveField() für die Auswertung. Sicherheit: die Akzentfarbe landet in einem style-Attribut, deshalb wird sie sowohl beim Speichern als auch beim Ausgeben auf eine echte Hex-Farbe geprüft; ein Test hält das fest. Migration 0006, database.sql nachgezogen, 4 neue Tests (42 gesamt), Screenshots ergänzt, Version 2.8.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
6.4 KiB
PHP
205 lines
6.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Tests;
|
||
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
require_once __DIR__ . '/../includes/Kiosk.php';
|
||
|
||
/**
|
||
* Datenbank-Attrappe: liefert feste Zählwerte und Einstellungen.
|
||
*/
|
||
class FakeKioskDb
|
||
{
|
||
public int $usedToday = 0;
|
||
/** @var array<string, string> */
|
||
private array $settings;
|
||
|
||
/** @param array<string, string> $settings */
|
||
public function __construct(array $settings = [])
|
||
{
|
||
$this->settings = $settings;
|
||
}
|
||
|
||
public function fetchOne($sql, $params = [])
|
||
{
|
||
return ['c' => $this->usedToday];
|
||
}
|
||
|
||
public function getSetting($key, $default = null)
|
||
{
|
||
return $this->settings[$key] ?? $default;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Die Grenzen der Kiosk-Seite sind das, was sie vor Missbrauch schützt –
|
||
* der Link ist öffentlich, also muss diese Logik stimmen.
|
||
*/
|
||
class KioskTest extends TestCase
|
||
{
|
||
/** @param array<string, mixed> $overrides */
|
||
private function kiosk(array $overrides = []): array
|
||
{
|
||
return array_merge([
|
||
'id' => 1,
|
||
'daily_limit' => 100,
|
||
'cooldown_seconds' => 20,
|
||
'last_used_at' => null,
|
||
], $overrides);
|
||
}
|
||
|
||
public function testTokenHasFixedShape(): void
|
||
{
|
||
$token = \Kiosk::newToken();
|
||
|
||
$this->assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $token);
|
||
$this->assertNotSame($token, \Kiosk::newToken(), 'Tokens dürfen sich nicht wiederholen');
|
||
}
|
||
|
||
public function testSanitizeTokenRejectsAnythingElse(): void
|
||
{
|
||
$valid = \Kiosk::newToken();
|
||
|
||
$this->assertSame($valid, \Kiosk::sanitizeToken($valid));
|
||
$this->assertSame($valid, \Kiosk::sanitizeToken(strtoupper($valid)));
|
||
$this->assertSame('', \Kiosk::sanitizeToken('kurz'));
|
||
$this->assertSame('', \Kiosk::sanitizeToken("' OR 1=1 --"));
|
||
$this->assertSame('', \Kiosk::sanitizeToken(null));
|
||
$this->assertSame('', \Kiosk::sanitizeToken($valid . 'ff'));
|
||
}
|
||
|
||
public function testCooldownBlocksSecondCode(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
$kiosk = $this->kiosk(['last_used_at' => date('Y-m-d H:i:s', time() - 5)]);
|
||
|
||
$result = \Kiosk::checkLimits($db, $kiosk);
|
||
|
||
$this->assertFalse($result['allowed']);
|
||
$this->assertSame('cooldown', $result['reason']);
|
||
$this->assertGreaterThan(0, $result['wait']);
|
||
$this->assertLessThanOrEqual(20, $result['wait']);
|
||
}
|
||
|
||
public function testCooldownExpires(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
$kiosk = $this->kiosk(['last_used_at' => date('Y-m-d H:i:s', time() - 60)]);
|
||
|
||
$this->assertTrue(\Kiosk::checkLimits($db, $kiosk)['allowed']);
|
||
}
|
||
|
||
public function testDailyLimitBlocks(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
$db->usedToday = 100;
|
||
|
||
$result = \Kiosk::checkLimits($db, $this->kiosk());
|
||
|
||
$this->assertFalse($result['allowed']);
|
||
$this->assertSame('daily_limit', $result['reason']);
|
||
}
|
||
|
||
public function testZeroMeansUnlimited(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
$db->usedToday = 5000;
|
||
|
||
$kiosk = $this->kiosk(['daily_limit' => 0, 'cooldown_seconds' => 0]);
|
||
|
||
$this->assertTrue(\Kiosk::checkLimits($db, $kiosk)['allowed']);
|
||
}
|
||
|
||
public function testVoucherSettingsPreferTemplate(): void
|
||
{
|
||
$db = new FakeKioskDb(['default_max_uses' => '1', 'default_expire_minutes' => '480']);
|
||
$kiosk = $this->kiosk([
|
||
'tpl_max_uses' => 5,
|
||
'tpl_expire_minutes' => 240,
|
||
'qos_rate_max_down' => 20000,
|
||
'qos_rate_max_up' => 5000,
|
||
'qos_usage_quota' => 1024,
|
||
]);
|
||
|
||
$settings = \Kiosk::voucherSettings($db, $kiosk);
|
||
|
||
$this->assertSame(5, $settings['max_uses']);
|
||
$this->assertSame(240, $settings['expire_minutes']);
|
||
$this->assertSame(20000, $settings['qos']['down']);
|
||
$this->assertSame(1024, $settings['qos']['quota_mb']);
|
||
}
|
||
|
||
public function testVoucherSettingsFallBackToDefaults(): void
|
||
{
|
||
$db = new FakeKioskDb(['default_max_uses' => '3', 'default_expire_minutes' => '120']);
|
||
|
||
$settings = \Kiosk::voucherSettings($db, $this->kiosk());
|
||
|
||
$this->assertSame(3, $settings['max_uses']);
|
||
$this->assertSame(120, $settings['expire_minutes']);
|
||
$this->assertSame(0, $settings['qos']['down']);
|
||
}
|
||
|
||
public function testAppearanceFallsBackToSystemLogo(): void
|
||
{
|
||
$db = new FakeKioskDb(['logo_url' => 'uploads/global.png']);
|
||
|
||
$look = \Kiosk::appearance($db, $this->kiosk());
|
||
|
||
$this->assertSame('uploads/global.png', $look['logo']);
|
||
$this->assertSame('', $look['background']);
|
||
$this->assertSame('', $look['accent'], 'Ohne eigene Farbe bleibt das Design-System zuständig');
|
||
$this->assertSame('light', $look['card']);
|
||
}
|
||
|
||
public function testAppearanceUsesOwnValues(): void
|
||
{
|
||
$db = new FakeKioskDb(['logo_url' => 'uploads/global.png']);
|
||
$kiosk = $this->kiosk([
|
||
'logo_url' => 'uploads/hotel.svg',
|
||
'background_url' => 'uploads/lobby.jpg',
|
||
'bg_overlay' => 60,
|
||
'accent_color' => '#0F766E',
|
||
'card_style' => 'dark',
|
||
]);
|
||
|
||
$look = \Kiosk::appearance($db, $kiosk);
|
||
|
||
$this->assertSame('uploads/hotel.svg', $look['logo']);
|
||
$this->assertSame('uploads/lobby.jpg', $look['background']);
|
||
$this->assertSame(0.6, $look['overlay']);
|
||
$this->assertSame('#0f766e', $look['accent']);
|
||
$this->assertSame('dark', $look['card']);
|
||
}
|
||
|
||
public function testAppearanceRejectsUnsafeColour(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
|
||
// Der Wert landet in einem style-Attribut – nur echte Hex-Farben durch.
|
||
$look = \Kiosk::appearance($db, $this->kiosk(['accent_color' => 'red;background:url(evil)']));
|
||
|
||
$this->assertSame('', $look['accent']);
|
||
}
|
||
|
||
public function testAppearanceClampsOverlay(): void
|
||
{
|
||
$db = new FakeKioskDb();
|
||
|
||
$this->assertSame(0.9, \Kiosk::appearance($db, $this->kiosk(['bg_overlay' => 250]))['overlay']);
|
||
$this->assertSame(0.0, \Kiosk::appearance($db, $this->kiosk(['bg_overlay' => -10]))['overlay']);
|
||
}
|
||
|
||
public function testPublicUrl(): void
|
||
{
|
||
$token = \Kiosk::newToken();
|
||
|
||
$this->assertSame(
|
||
'https://wlan.example.com/kiosk.php?k=' . $token,
|
||
\Kiosk::publicUrl($token, 'https://wlan.example.com/')
|
||
);
|
||
}
|
||
}
|