resolver = new MediaUrlResolver(); } #[DataProvider('nonMediaUrls')] public function test_returns_null_for_non_media_urls(string $url): void { $this->assertNull($this->resolver->resolve($url)); } public static function nonMediaUrls(): array { return [ 'normal image' => ['/photo.jpg'], 'svg' => ['/icon.svg'], 'png' => ['/avatar.png'], 'no extension' => ['/foo'], 'empty string' => [''], 'javascript scheme' => ['javascript:alert(1)'], 'host-only' => ['http://'], 'youtu.be lookalike host' => ['https://example.com/youtu.be-fake/abc'], ]; } #[DataProvider('videoUrls')] public function test_video_urls_produce_video_tag(string $url): void { $html = $this->resolver->resolve($url); $this->assertNotNull($html); $this->assertStringStartsWith('assertStringContainsString('controls', $html); $this->assertStringContainsString('class="kb-video"', $html); } public static function videoUrls(): array { return [ 'mp4' => ['/demo.mp4'], 'webm' => ['/demo.webm'], 'ogv' => ['/demo.ogv'], 'mov' => ['/demo.mov'], 'm4v' => ['/demo.m4v'], 'uppercase extension' => ['/demo.MP4'], 'with query string' => ['https://example.com/path/demo.mp4?token=abc'], 'absolute http' => ['https://example.com/demo.mp4'], ]; } public function test_video_url_is_html_escaped(): void { $html = $this->resolver->resolve('/path/with"quote.mp4'); $this->assertNotNull($html); $this->assertStringNotContainsString('"quote.mp4"', $html); $this->assertStringContainsString('"', $html); } #[DataProvider('audioUrls')] public function test_audio_urls_produce_audio_tag(string $url): void { $html = $this->resolver->resolve($url); $this->assertNotNull($html); $this->assertStringStartsWith('assertStringContainsString('controls', $html); $this->assertStringContainsString('class="kb-audio"', $html); } public static function audioUrls(): array { return [ 'mp3' => ['/clip.mp3'], 'wav' => ['/clip.wav'], 'ogg' => ['/clip.ogg'], 'm4a' => ['/clip.m4a'], ]; } #[DataProvider('youtubeUrls')] public function test_youtube_urls_produce_iframe(string $url): void { $html = $this->resolver->resolve($url); $this->assertNotNull($html); $this->assertStringStartsWith('assertStringContainsString('youtube-nocookie.com/embed/dQw4w9WgXcQ', $html); $this->assertStringContainsString('class="kb-embed kb-embed-youtube"', $html); $this->assertStringContainsString('loading="lazy"', $html); $this->assertStringContainsString('allowfullscreen', $html); } public static function youtubeUrls(): array { return [ 'short youtu.be' => ['https://youtu.be/dQw4w9WgXcQ'], 'watch v=' => ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'], 'shorts' => ['https://www.youtube.com/shorts/dQw4w9WgXcQ'], 'embed' => ['https://www.youtube.com/embed/dQw4w9WgXcQ'], 'mobile' => ['https://m.youtube.com/watch?v=dQw4w9WgXcQ'], 'no www watch' => ['https://youtube.com/watch?v=dQw4w9WgXcQ'], ]; } #[DataProvider('invalidYoutubeUrls')] public function test_invalid_youtube_urls_return_null(string $url): void { $this->assertNull($this->resolver->resolve($url)); } public static function invalidYoutubeUrls(): array { return [ 'too short id' => ['https://youtu.be/short'], 'host mismatch' => ['https://example.com/watch?v=dQw4w9WgXcQ'], 'XSS attempt in id' => ['https://youtu.be/abc">