Files
hstream/tests/Unit/CdnPathValidatorTest.php
T
w33b 30a6b080eb feat(admin): rebuild release upload form with Livewire and CDN validation
Replace the server-rendered release upload form with a Livewire component,
introducing an interactive admin release form that supports dynamic episode
management, real‑time CDN path validation, and direct download file checks.

Key changes:
- New `AdminReleaseForm` Livewire component with dynamic episode fields,
  Tagify integration, and upload handling via Livewire's file uploads.
- Added `CdnPathValidator` service to verify download and stream paths across
  all configured mirrors, caching results and capturing file sizes.
- Extended `EpisodeService` and `GalleryService` to support array‑based data
  and temporary uploaded files from Livewire.
- Persist validated download sizes and store the validation timestamp on the
  `downloads` table via a new migration; also add a unique constraint on
  `hentais.slug` to prevent duplicates.
- Remove the old POST `/admin/release/upload` route and controller logic,
  along with the legacy `upload.js` script.
- Add comprehensive feature and unit tests covering the new component and the
  CDN validation service.
2026-08-08 13:18:57 +02:00

157 lines
6.1 KiB
PHP

<?php
namespace Tests\Unit;
use App\Services\CdnPathValidator;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CdnPathValidatorTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Cache::flush();
config([
'hstream.download_domain_4k' => ['https://dl4k-a.test', 'https://dl4k-b.test'],
]);
}
public function test_fhd_uses_4k_domains_and_hentai_1080p_prefix(): void
{
Http::fake([
'https://dl4k-a.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]),
'https://dl4k-b.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]),
]);
$result = app(CdnPathValidator::class)->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertTrue($result['valid']);
$this->assertSame(12345, $result['size']);
$this->assertSame([
'https://dl4k-a.test' => ['hentai-1080p/2026/Title/E01.mkv' => true],
'https://dl4k-b.test' => ['hentai-1080p/2026/Title/E01.mkv' => true],
], $result['mirrors']);
Http::assertSent(fn ($request) => str_contains($request->url(), 'https://dl4k-a.test/check/'));
}
public function test_uhd_uses_download_domain_4k_and_hentai_prefix(): void
{
Http::fake([
'https://dl4k-a.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 777]),
'https://dl4k-b.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 777]),
]);
$result = app(CdnPathValidator::class)->validateDownload('UHD', '2026/Title/E01.mkv');
$this->assertTrue($result['valid']);
$this->assertArrayHasKey('hentai/2026/Title/E01.mkv', $result['mirrors']['https://dl4k-a.test']);
$this->assertArrayNotHasKey('hentai-1080p/2026/Title/E01.mkv', $result['mirrors']['https://dl4k-a.test']);
}
public function test_missing_on_one_mirror_is_invalid(): void
{
Http::fake([
'https://dl4k-a.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]),
'https://dl4k-b.test/*' => Http::response(['valid' => false, 'error' => 'File not found!'], 404),
]);
$result = app(CdnPathValidator::class)->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertFalse($result['valid']);
$this->assertNull($result['size']);
$this->assertTrue($result['mirrors']['https://dl4k-a.test']['hentai-1080p/2026/Title/E01.mkv']);
$this->assertFalse($result['mirrors']['https://dl4k-b.test']['hentai-1080p/2026/Title/E01.mkv']);
}
public function test_connection_error_is_invalid(): void
{
Http::fake(function ($request) {
if (str_starts_with($request->url(), 'https://dl4k-a.test/')) {
return Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]);
}
throw new ConnectionException('Connection timed out', 0);
});
$result = app(CdnPathValidator::class)->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertFalse($result['valid']);
$this->assertNull($result['size']);
$this->assertFalse($result['mirrors']['https://dl4k-b.test']['hentai-1080p/2026/Title/E01.mkv']);
}
public function test_stream_checks_base_directory_and_first_manifest(): void
{
Http::fake([
'https://dl4k-a.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 1024]),
'https://dl4k-b.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 1024]),
]);
$result = app(CdnPathValidator::class)->validateStream('2026/Title', 3);
$this->assertTrue($result['valid']);
$this->assertArrayHasKey('hentai-stream/2026/Title', $result['mirrors']['https://dl4k-a.test']);
$this->assertArrayHasKey('hentai-stream/2026/Title/E01/720/manifest.mpd', $result['mirrors']['https://dl4k-a.test']);
Http::assertSentCount(4);
}
public function test_results_are_cached_for_short_period(): void
{
Http::fake([
'https://dl4k-a.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]),
'https://dl4k-b.test/*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 12345]),
]);
$validator = app(CdnPathValidator::class);
$first = $validator->validateDownload('FHD', '2026/Title/E01.mkv');
$second = $validator->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertTrue($first['valid']);
$this->assertTrue($second['valid']);
Http::assertSentCount(2);
}
public function test_fresh_check_bypasses_cache(): void
{
$valid = true;
Http::fake(function ($request) use (&$valid) {
return Http::response(
$valid ? ['valid' => true, 'type' => 'file', 'size' => 12345] : ['valid' => false, 'error' => 'File not found!'],
$valid ? 200 : 404
);
});
$validator = app(CdnPathValidator::class);
$cached = $validator->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertTrue($cached['valid']);
$this->assertCount(2, Http::recorded());
// The CDN result changes within the cache window, but a non-fresh
// check still serves the cached valid result.
$valid = false;
$stale = $validator->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertTrue($stale['valid']);
$this->assertCount(2, Http::recorded());
// A fresh check bypasses the cache and overwrites the stored result.
$fresh = $validator->validateDownload('FHD', '2026/Title/E01.mkv', true);
$this->assertFalse($fresh['valid']);
$this->assertCount(4, Http::recorded());
// The fresh (invalid) result is now what subsequent cached checks serve.
$after = $validator->validateDownload('FHD', '2026/Title/E01.mkv');
$this->assertFalse($after['valid']);
$this->assertCount(4, Http::recorded());
}
}