withoutMiddleware([ VerifyCsrfToken::class, \Illuminate\Session\Middleware\AuthenticateSession::class, ]); } // ─── Player ───────────────────────────────────── public function test_player_component_ships_the_end_screen_markup(): void { $u = $this->makeUser(); $v = $this->makeVideo($u); $html = view('components.video-player', ['video' => $v])->render(); $this->assertStringContainsString('id="ytpEndScreen"', $html); $this->assertStringContainsString('ytp-end-countdown', $html, 'countdown ring class hook'); $this->assertStringContainsString('id="ytpEndRing"', $html); $this->assertStringContainsString('id="ytpEndCancel"', $html); } public function test_player_binds_c_hotkey_to_lyrics_button(): void { $u = $this->makeUser(); $v = $this->makeVideo($u); $html = view('components.video-player', ['video' => $v])->render(); $this->assertStringContainsString("case 'c':", $html); $this->assertStringContainsString('ytpLyricsBtn', $html); } // ─── Guest Subscribe prompt ──────────────────── public function test_guest_subscribe_button_preserves_intended_url(): void { $u = $this->makeUser(); $v = $this->makeVideo($u); $html = view('components.video-actions', ['video' => $v])->render(); $this->assertStringContainsString('?intended=', $html, 'Guest Subscribe button must redirect to login with ?intended=.'); $this->assertStringContainsString('encodeURIComponent', $html); } // ─── /@handle tab aliases ─────────────────────── public function test_handle_tab_alias_302_redirects_to_hash_url(): void { $u = User::factory()->create(['username' => 'hta-'.substr(bin2hex(random_bytes(3)), 0, 6)]); $r = $this->get('/@' . $u->username . '/videos'); $r->assertStatus(302); $this->assertStringEndsWith('#videos', $r->headers->get('Location')); } public function test_handle_tab_alias_404s_on_unknown_tab(): void { $u = User::factory()->create(['username' => 'htn-'.substr(bin2hex(random_bytes(3)), 0, 6)]); $this->get('/@' . $u->username . '/nonsense')->assertStatus(404); } // ─── Channel videos cursor endpoint ───────────── public function test_channel_videos_more_returns_cursor_json(): void { $u = $this->makeUser(); // Twice the page size so has_more is true. for ($i = 0; $i < 30; $i++) { $this->makeVideo($u, ['title' => 'Vid '.$i]); } $r = $this->getJson(route('channel.videos.more', ['username' => $u->username])); $r->assertOk() ->assertJsonStructure(['items', 'next_cursor', 'has_more']) ->assertJson(['has_more' => true]); $this->assertStringContainsString('yt-video-card', $r->json('items')); } // ─── Studio ───────────────────────────────────── public function test_studio_requires_authentication(): void { $this->get(route('studio'))->assertStatus(302); } public function test_studio_renders_only_my_videos(): void { $me = $this->makeUser(); $other = $this->makeUser(); $mine = $this->makeVideo($me, ['title' => 'MyStudioVideo']); $theirs = $this->makeVideo($other, ['title' => 'TheirVideo']); $this->actingAs($me); $html = $this->get(route('studio'))->getContent(); $this->assertStringContainsString('MyStudioVideo', $html); $this->assertStringNotContainsString('TheirVideo', $html); } public function test_studio_bulk_visibility_refuses_other_users_videos(): void { $me = $this->makeUser(); $other = $this->makeUser(); $mine = $this->makeVideo($me, ['visibility' => 'public']); $theirs = $this->makeVideo($other, ['visibility' => 'public']); $this->actingAs($me); $this->postJson(route('studio.bulkVisibility'), [ 'video_ids' => [$mine->id, $theirs->id], 'visibility' => 'private', ])->assertOk()->assertJson(['updated' => 1]); $this->assertEquals('private', $mine->fresh()->visibility); $this->assertEquals('public', $theirs->fresh()->visibility, 'A crafted request body must not flip another user\'s video.'); } public function test_studio_bulk_delete_scoped_to_owner(): void { $me = $this->makeUser(); $other = $this->makeUser(); $mine = $this->makeVideo($me); $theirs = $this->makeVideo($other); $this->actingAs($me); $this->postJson(route('studio.bulkDelete'), [ 'video_ids' => [$mine->id, $theirs->id], ])->assertOk()->assertJson(['deleted' => 1]); $this->assertNull(Video::find($mine->id)); $this->assertNotNull(Video::find($theirs->id)); } // ─── Helpers ──────────────────────────────────── private function makeUser(): User { return User::factory()->create([ 'username' => 'u'.substr(bin2hex(random_bytes(4)), 0, 8), ]); } private function makeVideo(User $u, array $attrs = []): Video { return Video::create(array_merge([ 'user_id' => $u->id, 'title' => 'T', 'description' => '', 'filename' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4', 'path' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4', 'type' => 'generic', 'visibility' => 'public', 'status' => 'ready', 'share_token' => bin2hex(random_bytes(8)), ], $attrs)); } }