getMessage() . "\n"); } } private function get(string $page): \CodeIgniter\Test\TestResponse { return $this->call('get', $page); } public function testToolPageRendersFullSeoContract(): void { $response = $this->get('/youtube-to-mp3'); $response->assertStatus(200); $html = (string) $response->getBody(); // canonical, title, description, OG $this->assertMatchesRegularExpression('##', $html); $this->assertMatchesRegularExpression('/[^<]+<\/title>/', $html); $this->assertStringContainsString('name="description"', $html); $this->assertStringContainsString('property="og:title"', $html); $this->assertStringContainsString('name="twitter:card" content="summary_large_image"', $html); // structured data: FAQPage + BreadcrumbList + WebApplication $jsonLd = ''; if (preg_match('/<script type="application\/ld\+json">(.*?)<\/script>/s', $html, $m)) { $jsonLd = $m[1]; } $this->assertNotSame('', $jsonLd, 'JSON-LD block missing'); foreach (['BreadcrumbList', 'WebApplication', 'FAQPage'] as $type) { $this->assertStringContainsString('"' . $type . '"', $jsonLd, "{$type} missing from JSON-LD"); } // content in first response: H1, how-to steps, FAQ questions visible $this->assertStringContainsString('<h1>', $html); $this->assertStringContainsString('Frequently asked questions', $html); $this->assertStringContainsString('Related tools', $html); $this->assertStringContainsString('breadcrumbs', $html); // robots meta allows indexing $this->assertStringContainsString('content="index, follow', $html); } public function testCategoryAndHomepageAreIndexable(): void { foreach (['/', '/youtube-tools', '/tools'] as $page) { $html = (string) $this->get($page)->getBody(); $this->assertStringContainsString('rel="canonical"', $html, "canonical missing on {$page}"); $this->assertStringContainsString('index, follow', $html, "robots meta wrong on {$page}"); } } public function testSearchIsNoindexed(): void { $html = (string) $this->get('/search?q=mp3')->getBody(); $this->assertStringContainsString('noindex', $html); } public function testSitemapsContainOnlyCanonicalUrls(): void { $index = (string) $this->get('/sitemap.xml')->getBody(); $this->assertStringContainsString('sitemapindex', $index); $this->assertStringContainsString('sitemap-tools-1.xml', $index); $this->assertStringContainsString('sitemap-guides.xml', $index); $tools = (string) $this->get('/sitemap-tools-1.xml')->getBody(); $this->assertStringContainsString('/youtube-to-mp3</loc>', $tools); $this->assertDoesNotMatchRegularExpression('#<loc>[^<]*/(api/|admin|download/|search\?)#', $tools, 'Non-indexable URL leaked into sitemap'); $categories = (string) $this->get('/sitemap-categories.xml')->getBody(); $this->assertStringContainsString('/youtube-tools</loc>', $categories); } public function testRobotsTxtBlocksProcessingEndpoints(): void { $body = (string) $this->get('/robots.txt')->getBody(); $this->assertStringContainsString('Disallow: /api/', $body); $this->assertStringContainsString('Disallow: /download/', $body); $this->assertStringContainsString('Disallow: /admin', $body); $this->assertStringContainsString('Sitemap:', $body); } public function testUppercaseUrl301RedirectsToCanonical(): void { $this->get('/YouTube-To-MP3')->assertStatus(301); } public function testUnknownSlugReturnsUseful404(): void { $result = $this->get('/this-tool-does-not-exist'); $result->assertStatus(404); $html = (string) $result->getBody(); $this->assertStringContainsString('Tool not found', $html); $this->assertStringContainsString('Popular tools', $html, '404 page must link popular tools'); } }