Files
hstream/tests/Unit/CdnPathValidatorTest.php
w33b d08e280ce0 refactor(episode): migrate episode upload to Livewire with real-time CDN verification
Replace the server-rendered episode upload form with a Livewire component,
introducing an interactive admin experience that validates the new episode's
stream path against all configured mirrors before saving. The controller-based
`store` method and its route are removed, and episode creation now flows
through the `AdminEpisodeForm` component with frontend upload handling and
dynamic quality checks.

Key changes:
- Add `AdminEpisodeForm` Livewire component with file uploads, tag handling,
  and debounced stream/base-url validation.
- Extend `CdnPathValidator::validateStream` to accept an episode number so the
  manifest path can point to the specific episode being added.
- Remove the `store` method from `EpisodeController` and its POST route.
- Delete the legacy `createEpisode` method from `EpisodeService`.
- Replace the Blade upload modal with the Livewire view, including progress
  indicators and validation badges.
- Add feature tests for the Livewire component and update unit tests for the
  CDN validator's episode-specific manifest check.
2026-08-08 14:29:05 +02:00

175 lines
7.3 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_stream_can_target_specific_episode_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', 1, false, 2);
$this->assertTrue($result['valid']);
$this->assertArrayHasKey('hentai-stream/2026/Title', $result['mirrors']['https://dl4k-a.test']);
$this->assertArrayHasKey('hentai-stream/2026/Title/E02/720/manifest.mpd', $result['mirrors']['https://dl4k-a.test']);
$this->assertArrayHasKey('hentai-stream/2026/Title/E02/720/manifest.mpd', $result['mirrors']['https://dl4k-b.test']);
$this->assertArrayNotHasKey('hentai-stream/2026/Title/E01/720/manifest.mpd', $result['mirrors']['https://dl4k-a.test']);
$this->assertArrayNotHasKey('hentai-stream/2026/Title/E01/720/manifest.mpd', $result['mirrors']['https://dl4k-b.test']);
$this->assertSame(4, count(Http::recorded()));
}
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());
}
}