['https://dl-a.test'], 'hstream.download_domain_4k' => ['https://dl4k-a.test'], 'hstream.stream_domain' => ['https://stream-a.test'], ]); $this->admin = User::factory()->create(); $this->admin->addRole(UserRole::ADMINISTRATOR); $this->actingAs($this->admin); } private function makeReferenceEpisode(string $slug = 'amazing-title-2026', array $attributes = []): Episode { $hentai = Hentai::factory()->create([ 'slug' => $slug, 'description' => 'An existing series', ]); $studio = Studios::factory()->create([ 'name' => 'Test Studio', 'slug' => 'test-studio', ]); $episode = Episode::factory()->create(array_merge([ 'hentai_id' => $hentai->id, 'studios_id' => $studio->id, 'title' => 'Amazing Title 2026', 'title_search' => 'Amazing Title 2026', 'title_jpn' => '素晴らしいタイトル', 'slug' => $slug.'-1', 'episode' => 1, 'description' => 'A lovely description', 'url' => '2026/AmazingTitle/E01', 'cover_url' => '/images/hentai/'.$slug.'/cover-ep-1.webp', 'interpolated' => 1, 'interpolated_uhd' => 1, 'release_date' => '2026-08-04', 'view_count' => 0, ], $attributes)); $episode->tag('Action'); return $episode; } private function makeForm(?Episode $episode = null): Testable { $episode ??= $this->makeReferenceEpisode(); return Livewire::test(AdminEpisodeForm::class, ['episodeId' => $episode->id]); } public function test_valid_episode_is_persisted(): void { Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]); $this->makeForm() ->set('cover', UploadedFile::fake()->image('cover.jpg')) ->set('gallery', [ UploadedFile::fake()->image('g1.jpg'), UploadedFile::fake()->image('g2.jpg'), ]) ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhdi', '2026/AmazingTitle/E02.mkv') ->call('save') ->assertHasNoErrors() ->assertRedirect(route('hentai.index', 'amazing-title-2026-2')); $this->assertDatabaseCount('episodes', 2); $episode = Episode::where('slug', 'amazing-title-2026-2')->firstOrFail(); $this->assertSame('Amazing Title 2026', $episode->title); $this->assertSame('素晴らしいタイトル', $episode->title_jpn); $this->assertSame('Test Studio', $episode->studio->name); $this->assertSame(2, $episode->episode); $this->assertSame('2026/AmazingTitle/E02', $episode->url); $this->assertSame('2026-08-04', $episode->release_date); $this->assertSame(1, $episode->interpolated); $this->assertSame(1, $episode->interpolated_uhd); $this->assertTrue($episode->tags->contains('name', 'Action')); $this->assertDatabaseCount('gallery', 2); $this->assertDatabaseCount('downloads', 3); $fhd = Downloads::where('episode_id', $episode->id)->where('type', 'FHD')->firstOrFail(); $this->assertSame('2026/AmazingTitle/E02.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-2.webp')); $this->assertTrue(Storage::disk('public')->exists('/images/hentai/amazing-title-2026/gallery-ep-2-0.webp')); $this->assertTrue(Storage::disk('public')->exists('/images/hentai/amazing-title-2026/gallery-ep-2-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('cover', UploadedFile::fake()->image('cover.jpg')) ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->call('save') ->assertHasErrors('downloads.fhd'); $this->assertDatabaseCount('episodes', 1); } 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('cover', UploadedFile::fake()->image('cover.jpg')) ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->set('overrideValidation', true) ->call('save') ->assertHasNoErrors() ->assertRedirect(route('hentai.index', 'amazing-title-2026-2')); $this->assertDatabaseCount('episodes', 2); $episode = Episode::where('slug', 'amazing-title-2026-2')->firstOrFail(); $uhd = Downloads::where('episode_id', $episode->id)->where('type', 'UHD')->firstOrFail(); $this->assertSame('2026/AmazingTitle/E02.mkv', $uhd->url); $this->assertNull($uhd->size); } public function test_validation_requires_required_fields(): void { Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]); $component = $this->makeForm() ->set('baseurl', '') ->set('description', '') ->set('downloads.fhd', '') ->set('downloads.uhd', '') ->call('save') ->assertHasErrors([ 'baseurl' => 'required', 'description' => 'required', 'cover' => 'required', 'downloads.fhd' => 'required', 'downloads.uhd' => 'required', ]); $this->assertDatabaseCount('episodes', 1); // The base URL must match the directory pattern (2026/Title). $component ->set('baseurl', 'no-slash') ->set('description', 'A lovely description') ->set('cover', UploadedFile::fake()->image('cover.jpg')) ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->call('save') ->assertHasErrors(['baseurl' => 'regex']); $this->assertDatabaseCount('episodes', 1); } 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('cover', UploadedFile::fake()->image('cover.jpg')) ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv'); // Live typing validation caches the invalid CDN result. $validation = $component->get('validation'); $this->assertSame('invalid', $validation['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('hentai.index', 'amazing-title-2026-2')); $this->assertDatabaseCount('episodes', 2); $this->assertDatabaseCount('downloads', 2); } public function test_mount_and_save_require_admin_role(): void { $episode = $this->makeReferenceEpisode(); // Guests are rejected when the component mounts. Auth::logout(); Livewire::test(AdminEpisodeForm::class, ['episodeId' => $episode->id]) ->assertStatus(403); // Authenticated non-admins are rejected when the component mounts. $user = User::factory()->create(); $this->actingAs($user); Livewire::test(AdminEpisodeForm::class, ['episodeId' => $episode->id]) ->assertStatus(403); // Administrators can mount and save. $admin = User::factory()->create(); $admin->addRole(UserRole::ADMINISTRATOR); $this->actingAs($admin); Http::fake(['*' => Http::response(['valid' => true, 'type' => 'file', 'size' => 9999])]); $this->makeForm($episode) ->set('cover', UploadedFile::fake()->image('cover.jpg')) ->set('downloads.fhd', '2026/AmazingTitle/E02.mkv') ->set('downloads.uhd', '2026/AmazingTitle/E02.mkv') ->call('save') ->assertHasNoErrors() ->assertRedirect(route('hentai.index', 'amazing-title-2026-2')); $this->assertDatabaseCount('episodes', 2); } public function test_stream_page_renders_episode_modal_for_admin(): void { $episode = $this->makeReferenceEpisode('amazing-title-2026', [ 'interpolated' => 0, 'interpolated_uhd' => 0, ]); $this->get(route('hentai.index', $episode->slug)) ->assertOk() ->assertSee('admin-episode-form'); } }