withoutMiddleware([ VerifyCsrfToken::class, \Illuminate\Session\Middleware\AuthenticateSession::class, ]); } // ─── Trending window ───────────────────────────── public function test_trending_accepts_24_168_720_and_snaps_others_to_24(): void { $this->get(route('videos.trending', ['hours' => 24]))->assertOk(); $this->get(route('videos.trending', ['hours' => 168]))->assertOk(); $this->get(route('videos.trending', ['hours' => 720]))->assertOk(); // Garbage input must not crash; it snaps to 24. $this->get(route('videos.trending', ['hours' => 'abc']))->assertOk(); $this->get(route('videos.trending', ['hours' => 99999]))->assertOk(); } public function test_trending_ranks_by_in_window_views_then_likes(): void { $owner = $this->makeUser(); // Three videos — "A" has most views in last 24h, "B" has most likes // but no views, "C" has nothing. A should be first; B should NOT // appear (no in-window views → filtered out); C should not appear. $a = $this->makeVideo($owner, 'A'); $b = $this->makeVideo($owner, 'B'); $c = $this->makeVideo($owner, 'C'); $this->seedViews($a, 10); $this->seedViews($b, 0); $this->seedLikes($b, 5); $this->seedViews($c, 0); $r = $this->get(route('videos.trending', ['hours' => 24])); $r->assertOk(); $html = $r->getContent(); $this->assertStringContainsString('A', $html, 'Video A must appear — it has in-window views.'); $this->assertStringNotContainsString('>B<', $html, 'Video B must not appear — zero in-window views.'); } // ─── Music feed newest-first ───────────────────── public function test_music_feed_opens_on_newest_video(): void { $owner = $this->makeUser(); // created_at isn't in Video::$fillable (Eloquent auto-manages it), // so set it after the row exists. $old = $this->makeVideo($owner, 'OLD MUSIC', ['type' => 'music']); $old->created_at = now()->subDays(5); $old->save(); $new = $this->makeVideo($owner, 'NEW MUSIC', ['type' => 'music']); $new->created_at = now()->subMinutes(5); $new->save(); $html = $this->get(route('videos.index', ['filter' => 'music']))->assertOk()->getContent(); $posNew = strpos($html, 'NEW MUSIC'); $posOld = strpos($html, 'OLD MUSIC'); $this->assertNotFalse($posNew); $this->assertNotFalse($posOld); $this->assertLessThan($posOld, $posNew, 'Newer music video must appear before the older one.'); } // ─── /videos/search → /search (301) ────────────── public function test_old_videos_search_url_301_redirects_to_search(): void { $this->get('/videos/search?q=hello') ->assertStatus(301) ->assertRedirect('/search?q=hello'); } // ─── /search returns channel + playlist buckets ── public function test_search_matches_video_channel_and_playlist(): void { $owner = User::factory()->create([ 'name' => 'Searchable Owner', 'username' => 'searchable-channel-'.substr(bin2hex(random_bytes(4)), 0, 6), ]); $video = $this->makeVideo($owner, 'SearchableVideoTitle', ['description' => 'nothing special']); $pl = Playlist::create([ 'user_id' => $owner->id, 'name' => 'SearchablePlaylistName', 'visibility' => 'public', 'share_token'=> bin2hex(random_bytes(8)), ]); // 1. Query that matches a video title $html = $this->get(route('search', ['q' => 'SearchableVideo']))->assertOk()->getContent(); $this->assertStringContainsString('SearchableVideoTitle', $html); // 2. Query that matches a channel name $html = $this->get(route('search', ['q' => 'Searchable Owner']))->assertOk()->getContent(); $this->assertStringContainsString('Channels', $html); $this->assertStringContainsString('Searchable Owner', $html); // 3. Query that matches a playlist name $html = $this->get(route('search', ['q' => 'SearchablePlaylist']))->assertOk()->getContent(); $this->assertStringContainsString('Playlists', $html); $this->assertStringContainsString('SearchablePlaylistName', $html); } public function test_empty_search_redirects_to_browse(): void { $this->get(route('search', ['q' => ''])) ->assertRedirect(route('videos.index')); } // ─── Helpers ───────────────────────────────────── private function makeUser(): User { return User::factory()->create([ 'username' => 'u'.substr(bin2hex(random_bytes(4)), 0, 8), ]); } private function makeVideo(User $u, string $title, array $attrs = []): Video { return Video::create(array_merge([ 'user_id' => $u->id, 'title' => $title, '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)); } private function seedViews(Video $v, int $n): void { for ($i = 0; $i < $n; $i++) { DB::table('video_views')->insert([ 'video_id' => $v->id, 'user_id' => null, 'device_id' => 'dev-'.$i, 'watched_at' => now()->subMinutes(10), ]); } } private function seedLikes(Video $v, int $n): void { for ($i = 0; $i < $n; $i++) { $u = $this->makeUser(); DB::table('video_likes')->insert([ 'video_id' => $v->id, 'user_id' => $u->id, 'created_at' => now(), 'updated_at' => now(), ]); } } }