assertSame($slugs, array_values(array_unique($slugs)), 'Tool slugs must be unique'); $this->assertGreaterThan(100, count($slugs)); } public function testEveryToolHasCompleteSeoFields(): void { foreach (CatalogBuilder::build() as $tool) { $this->assertNotSame('', trim((string) $tool['seo_title']), "seo_title empty for {$tool['slug']}"); $this->assertNotSame('', trim((string) $tool['h1']), "h1 empty for {$tool['slug']}"); $this->assertGreaterThanOrEqual(20, mb_strlen((string) $tool['seo_description']), "short description for {$tool['slug']}"); $this->assertNotEmpty($tool['how_to'], "how_to missing for {$tool['slug']}"); $this->assertNotEmpty($tool['faqs'], "faqs missing for {$tool['slug']}"); $this->assertLessThanOrEqual(65, mb_strlen((string) $tool['seo_title']) + 0, "title too long for {$tool['slug']}"); } } public function testConverterTitlesAreFormatSpecific(): void { $bySlug = []; foreach (CatalogBuilder::build() as $tool) { $bySlug[$tool['slug']] = $tool; } // each converter page must mention its actual formats foreach (['jpg-to-png', 'mp4-to-webm', 'flac-to-mp3'] as $slug) { $this->assertArrayHasKey($slug, $bySlug); $title = (string) $bySlug[$slug]['seo_title']; [$from, $to] = explode('-to-', $slug); $this->assertStringContainsString(FormatCatalog::name($from), $title); $this->assertStringContainsString(FormatCatalog::name($to), $title); } } public function testYoutubeIdExtraction(): void { $cases = [ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' => 'dQw4w9WgXcQ', 'https://youtu.be/dQw4w9WgXcQ?t=42' => 'dQw4w9WgXcQ', 'https://www.youtube.com/shorts/abcdefghijk' => 'abcdefghijk', 'https://www.youtube.com/embed/abcdefghijk' => 'abcdefghijk', 'dQw4w9WgXcQ' => 'dQw4w9WgXcQ', 'https://evil.com/watch?v=dQw4w9WgXcQ' => null, 'not a url' => null, ]; foreach ($cases as $input => $expected) { $this->assertSame($expected, YoutubeLib::extractId($input), "Failed extracting from: {$input}"); } } public function testUrlGuardBlocksPrivateNetworks(): void { $guard = new UrlGuard(); $this->expectException(\InvalidArgumentException::class); $guard->check('http://127.0.0.1/secret'); } public function testUrlGuardBlocksFileScheme(): void { $guard = new UrlGuard(); $this->expectException(\InvalidArgumentException::class); $guard->check('file:///etc/passwd'); } public function testIsPublicIp(): void { $guard = new UrlGuard(); $this->assertFalse($guard->isPublicIp('10.0.0.5')); $this->assertFalse($guard->isPublicIp('192.168.1.1')); $this->assertFalse($guard->isPublicIp('169.254.169.254')); // cloud metadata $this->assertTrue($guard->isPublicIp('142.250.185.78')); } }