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.
239 lines
9.1 KiB
PHP
239 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Livewire;
|
|
|
|
use App\Enums\UserRole;
|
|
use App\Livewire\AdminReleaseForm;
|
|
use App\Models\Downloads;
|
|
use App\Models\Episode;
|
|
use App\Models\Hentai;
|
|
use App\Models\User;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Features\SupportTesting\Testable;
|
|
use Livewire\Livewire;
|
|
use Tests\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class AdminReleaseFormTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Queue::fake();
|
|
Cache::flush();
|
|
Storage::fake('public');
|
|
|
|
config([
|
|
'hstream.download_domain' => ['https://dl-a.test'],
|
|
'hstream.download_domain_4k' => ['https://dl4k-a.test'],
|
|
'hstream.stream_domain' => ['https://stream-a.test'],
|
|
]);
|
|
}
|
|
|
|
private function makeForm(): Testable
|
|
{
|
|
return Livewire::test(AdminReleaseForm::class)
|
|
->set('title', 'Amazing Title 2026')
|
|
->set('titleJpn', '素晴らしいタイトル')
|
|
->set('studio', 'Test Studio')
|
|
->set('tags', ['Action', 'Romance'])
|
|
->set('releasedate', '2026-08-04')
|
|
->set('baseurl', '2026/AmazingTitle')
|
|
->set('episodes.0.description', 'A lovely description');
|
|
}
|
|
|
|
public function test_valid_release_is_persisted_with_sizes(): void
|
|
{
|
|
Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]);
|
|
|
|
$this->makeForm()
|
|
->set('episodes.0.cover', UploadedFile::fake()->image('cover.jpg'))
|
|
->set('episodes.0.gallery', [
|
|
UploadedFile::fake()->image('g1.jpg'),
|
|
UploadedFile::fake()->image('g2.jpg'),
|
|
])
|
|
->set('episodes.0.downloads.fhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.uhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.uhdi', '2026/AmazingTitle/E01.mkv')
|
|
->call('save')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('home.index'));
|
|
|
|
$this->assertDatabaseCount('hentais', 1);
|
|
$this->assertDatabaseHas('hentais', ['slug' => 'amazing-title-2026']);
|
|
|
|
$hentai = Hentai::where('slug', 'amazing-title-2026')->firstOrFail();
|
|
$this->assertDatabaseCount('episodes', 1);
|
|
|
|
$episode = Episode::where('hentai_id', $hentai->id)->firstOrFail();
|
|
$this->assertSame('2026/AmazingTitle/E01', $episode->url);
|
|
$this->assertSame(1, $episode->interpolated);
|
|
$this->assertSame(1, $episode->interpolated_uhd);
|
|
|
|
$this->assertDatabaseCount('gallery', 2);
|
|
$this->assertDatabaseCount('downloads', 3);
|
|
|
|
$fhd = Downloads::where('episode_id', $episode->id)->where('type', 'FHD')->firstOrFail();
|
|
$this->assertSame('2026/AmazingTitle/E01.mkv', $fhd->url);
|
|
$this->assertEquals(9999, $fhd->size);
|
|
$this->assertNotNull($fhd->validated_at);
|
|
|
|
$uhd = Downloads::where('episode_id', $episode->id)->where('type', 'UHD')->firstOrFail();
|
|
$this->assertEquals(9999, $uhd->size);
|
|
|
|
$uhdi = Downloads::where('episode_id', $episode->id)->where('type', 'UHDi')->firstOrFail();
|
|
$this->assertEquals(9999, $uhdi->size);
|
|
|
|
$this->assertTrue(Storage::disk('public')->exists('/images/hentai/amazing-title-2026/cover-ep-1.webp'));
|
|
$this->assertTrue(Storage::disk('public')->exists('/images/hentai/amazing-title-2026/gallery-ep-1-0.webp'));
|
|
$this->assertTrue(Storage::disk('public')->exists('/images/hentai/amazing-title-2026/gallery-ep-1-1.webp'));
|
|
}
|
|
|
|
public function test_invalid_cdn_path_blocks_save_with_error(): void
|
|
{
|
|
Http::fake([
|
|
'https://dl4k-a.test/*' => Http::response(['valid' => false, 'error' => 'File not found!'], 404),
|
|
]);
|
|
|
|
$this->makeForm()
|
|
->set('episodes.0.cover', UploadedFile::fake()->image('cover.jpg'))
|
|
->set('episodes.0.downloads.fhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.uhd', '2026/AmazingTitle/E01.mkv')
|
|
->call('save')
|
|
->assertHasErrors('episodes.0.downloads.fhd');
|
|
|
|
$this->assertDatabaseCount('hentais', 0);
|
|
$this->assertDatabaseCount('episodes', 0);
|
|
}
|
|
|
|
public function test_override_validation_allows_saving_invalid_paths(): void
|
|
{
|
|
Http::fake([
|
|
'https://dl4k-a.test/*' => Http::response(['valid' => false, 'error' => 'File not found!'], 404),
|
|
]);
|
|
|
|
$this->makeForm()
|
|
->set('episodes.0.cover', UploadedFile::fake()->image('cover.jpg'))
|
|
->set('episodes.0.downloads.fhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.uhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('overrideValidation', true)
|
|
->call('save')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('home.index'));
|
|
|
|
$this->assertDatabaseCount('hentais', 1);
|
|
|
|
$hentai = Hentai::where('slug', 'amazing-title-2026')->firstOrFail();
|
|
$episode = Episode::where('hentai_id', $hentai->id)->firstOrFail();
|
|
|
|
$uhd = Downloads::where('episode_id', $episode->id)->where('type', 'UHD')->firstOrFail();
|
|
$this->assertSame('2026/AmazingTitle/E01.mkv', $uhd->url);
|
|
$this->assertNull($uhd->size);
|
|
}
|
|
|
|
public function test_existing_release_created_today_is_blocked(): void
|
|
{
|
|
Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]);
|
|
|
|
Hentai::create(['slug' => 'amazing-title-2026', 'description' => 'existing']);
|
|
|
|
$this->makeForm()
|
|
->set('episodes.0.cover', UploadedFile::fake()->image('cover.jpg'))
|
|
->set('episodes.0.downloads.fhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.uhd', '2026/AmazingTitle/E01.mkv')
|
|
->call('save')
|
|
->assertHasErrors('title');
|
|
|
|
$this->assertDatabaseCount('hentais', 1);
|
|
$this->assertDatabaseCount('episodes', 0);
|
|
}
|
|
|
|
public function test_validation_requires_required_fields(): void
|
|
{
|
|
Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]);
|
|
|
|
Livewire::test(AdminReleaseForm::class)
|
|
->call('save')
|
|
->assertHasErrors([
|
|
'title' => 'required',
|
|
'titleJpn' => 'required',
|
|
'studio' => 'required',
|
|
'tags' => 'required',
|
|
'releasedate' => 'required',
|
|
'baseurl' => 'required',
|
|
'episodes.0.description' => 'required',
|
|
'episodes.0.cover' => 'required',
|
|
'episodes.0.downloads.fhd' => 'required',
|
|
'episodes.0.downloads.uhd' => 'required',
|
|
]);
|
|
|
|
$this->assertDatabaseCount('hentais', 0);
|
|
}
|
|
|
|
public function test_save_uses_fresh_cdn_check_ignoring_cached_results(): void
|
|
{
|
|
$valid = false;
|
|
|
|
Http::fake(function ($request) use (&$valid) {
|
|
return Http::response(['valid' => $valid, 'type' => 'file', 'size' => 9999]);
|
|
});
|
|
|
|
$component = $this->makeForm()
|
|
->set('episodes.0.cover', UploadedFile::fake()->image('cover.jpg'))
|
|
->set('episodes.0.downloads.uhd', '2026/AmazingTitle/E01.mkv')
|
|
->set('episodes.0.downloads.fhd', '2026/AmazingTitle/E01.mkv');
|
|
|
|
// Live typing validation caches the invalid CDN result.
|
|
$validation = $component->get('validation');
|
|
$this->assertSame('invalid', $validation['episodes.0.downloads.fhd']['state'] ?? null);
|
|
|
|
// The file gets "fixed" on the CDN within the cache window.
|
|
$valid = true;
|
|
|
|
// Saving must re-check the CDN without using the cached invalid result.
|
|
$component
|
|
->call('save')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('home.index'));
|
|
|
|
$this->assertDatabaseCount('hentais', 1);
|
|
$this->assertDatabaseCount('downloads', 2);
|
|
}
|
|
|
|
public function test_admin_release_page_renders(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$user->addRole(UserRole::ADMINISTRATOR);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.upload.index'))
|
|
->assertOk()
|
|
->assertSee('admin-release-form');
|
|
}
|
|
|
|
public function test_tagify_inputs_are_excluded_from_livewire_morphing(): void
|
|
{
|
|
$component = Livewire::test(AdminReleaseForm::class);
|
|
|
|
// Tagify inputs must live inside wire:ignore scopes so Livewire's
|
|
// DOM morphing never replaces the Tagify-generated markup.
|
|
$component
|
|
->assertSeeHtml('wire:ignore x-data="adminReleaseTags"')
|
|
->assertSeeHtml('wire:ignore x-data="adminReleaseStudio"');
|
|
|
|
// The wrappers must survive a server round-trip (e.g. a debounced
|
|
// download/baseurl update) untouched.
|
|
$component
|
|
->set('title', 'Changed Title')
|
|
->assertSeeHtml('wire:ignore x-data="adminReleaseTags"')
|
|
->assertSeeHtml('wire:ignore x-data="adminReleaseStudio"');
|
|
}
|
|
}
|