30a6b080eb
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.
157 lines
6.1 KiB
PHP
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());
|
|
}
|
|
}
|