expectException(\InvalidArgumentException::class); Slug::random(3); } /** * @return array */ public static function slugValidationProvider(): array { return [ // --- zulaessig ------------------------------------------ 'einfach' => ['messe2026', true], 'mit Bindestrich' => ['messe-2026-halle-3', true], 'nur Ziffern' => ['12345', true], 'einzelnes Zeichen' => ['a', true], 'Grossbuchstaben' => ['Messe2026', true], 'gemischte Schreibweise' => ['1AEJ0X90ZmF', true], 'Unterstrich' => ['messe_2026', true], 'Punkt' => ['messe.2026', true], 'zwei Abschnitte' => ['g/1AEJ0X90ZmF', true], 'mehrere Abschnitte' => ['a/b/c/d', true], 'Abschnitte mit Bindestrich' => ['flyer-2026/halle-3', true], 'maximale Abschnittslaenge' => [str_repeat('a', 64), true], // --- unzulaessig ---------------------------------------- 'leer' => ['', false], 'Umlaut' => ['grüße', false], 'Leerzeichen' => ['messe 2026', false], 'fuehrender Bindestrich' => ['-messe', false], 'abschliessender Bindestrich' => ['messe-', false], 'fuehrender Schraegstrich' => ['/messe', false], 'abschliessender Schraegstrich' => ['messe/', false], 'leerer Abschnitt' => ['g//abc', false], 'Abschnitt mit fuehrendem Punkt' => ['g/.abc', false], 'Punkt-Abschnitt' => ['g/./abc', false], 'Aufwaerts-Abschnitt' => ['g/../abc', false], 'reserviert admin' => ['admin', false], 'reserviert als erster Abschnitt' => ['admin/geheim', false], 'reserviert login' => ['login', false], 'reserviert well-known' => ['.well-known/x', false], 'Abschnitt zu lang' => [str_repeat('a', 65), false], 'zu viele Abschnitte' => ['a/b/c/d/e/f/g/h/i', false], 'insgesamt zu lang' => [str_repeat('ab/', 70) . 'x', false], ]; } /** * @dataProvider slugValidationProvider */ public function testValidation(string $slug, bool $expected): void { self::assertSame($expected, Slug::isValid($slug)); self::assertSame($expected, Slug::validationError($slug) === null); } public function testNormalizeCreatesUsableSlugs(): void { self::assertSame('Messe-2026', Slug::normalize('Messe 2026')); self::assertSame('Gruesse-aus-Muenchen', Slug::normalize('Grüße aus München')); self::assertSame('a-b', Slug::normalize('--a---b--')); // "ß" wird zu "ss", das fuehrende "S" bleibt gross. self::assertSame('Strasse', Slug::normalize('Straße')); self::assertSame('strasse', Slug::normalize('straße')); self::assertSame('', Slug::normalize(' ')); } /** * Gross- und Kleinschreibung muss erhalten bleiben: bereits * gedruckte QR-Codes lassen sich nicht mehr aendern. */ public function testNormalizeKeepsLetterCase(): void { self::assertSame('1AEJ0X90ZmF', Slug::normalize('1AEJ0X90ZmF')); self::assertSame('g/1AEJ0X90ZmF', Slug::normalize('g/1AEJ0X90ZmF')); self::assertSame('MesseFlyer', Slug::normalize('MesseFlyer')); } public function testNormalizeKeepsPathSegments(): void { self::assertSame('g/1AEJ0X90ZmF', Slug::normalize('g/1AEJ0X90ZmF')); self::assertSame('g/1AEJ0X90ZmF', Slug::normalize('/g/1AEJ0X90ZmF')); self::assertSame('g/1AEJ0X90ZmF', Slug::normalize('g//1AEJ0X90ZmF/')); self::assertSame('a/b/c', Slug::normalize('a / b / c')); } /** * Wer die komplette alte Adresse einfuegt, soll nicht scheitern. */ public function testNormalizeAcceptsAFullUrl(): void { self::assertSame( 'g/1AEJ0X90ZmF', Slug::normalize('https://qr.beispiel.de/g/1AEJ0X90ZmF'), ); self::assertSame('messe', Slug::normalize('http://beispiel.de/messe')); } public function testNormalizedInputIsAlwaysValidOrEmpty(): void { foreach ([ 'Messe 2026', 'Grüße!', 'a/b/c', '###', 'Test_Fall', '/g/1AEJ0X90ZmF', 'g//x//y', '---', '...', 'https://x.de/a/b', ] as $input) { $normalized = Slug::normalize($input); if ($normalized !== '') { self::assertTrue(Slug::isValid($normalized), $input . ' -> ' . $normalized); } } } public function testReservedFirstSegmentIsDetected(): void { self::assertTrue(Slug::isReserved('admin')); self::assertTrue(Slug::isReserved('admin/links')); self::assertTrue(Slug::isReserved('ADMIN/links')); self::assertFalse(Slug::isReserved('administration')); self::assertFalse(Slug::isReserved('g/admin')); } }