- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
98 lines
3.6 KiB
PHP
98 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Libraries\CatalogBuilder;
|
|
use App\Libraries\FormatCatalog;
|
|
use App\Libraries\Pipeline\Youtube as YoutubeLib;
|
|
use App\Libraries\UrlGuard;
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
|
|
|
/**
|
|
* Pure unit tests — no database required.
|
|
*/
|
|
final class CatalogTest extends CIUnitTestCase
|
|
{
|
|
public function testCatalogBuildsUniqueSlugs(): void
|
|
{
|
|
$slugs = array_column(CatalogBuilder::build(), 'slug');
|
|
|
|
$this->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'));
|
|
}
|
|
}
|