['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"'); } }