withoutMiddleware([ VerifyCsrfToken::class, \Illuminate\Session\Middleware\AuthenticateSession::class, ]); } // ─── Dislike toggle ────────────────────────────── public function test_toggle_dislike_adds_then_removes_the_row(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($viewer); $this->postJson(route('videos.toggleDislike', $video)) ->assertOk() ->assertJson(['disliked' => true, 'dislike_count' => 1]); $this->assertDatabaseHas('video_dislikes', ['video_id' => $video->id, 'user_id' => $viewer->id]); $this->postJson(route('videos.toggleDislike', $video)) ->assertOk() ->assertJson(['disliked' => false, 'dislike_count' => 0]); $this->assertDatabaseMissing('video_dislikes', ['video_id' => $video->id, 'user_id' => $viewer->id]); } public function test_disliking_clears_an_existing_like(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($viewer); $video->likes()->attach($viewer->id); $this->postJson(route('videos.toggleDislike', $video)) ->assertOk() ->assertJson(['disliked' => true, 'liked' => false, 'like_count' => 0]); $this->assertDatabaseMissing('video_likes', ['video_id' => $video->id, 'user_id' => $viewer->id]); } public function test_liking_clears_an_existing_dislike(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($viewer); $video->dislikes()->attach($viewer->id); $this->postJson(route('videos.toggleLike', $video)) ->assertOk() ->assertJson(['liked' => true, 'disliked' => false, 'like_count' => 1, 'dislike_count' => 0]); $this->assertDatabaseMissing('video_dislikes', ['video_id' => $video->id, 'user_id' => $viewer->id]); } // ─── Heart ────────────────────────────────────── public function test_owner_can_heart_a_comment(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($owner); $comment = Comment::create([ 'user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'nice video!', ]); $this->postJson(route('comments.heart', $comment)) ->assertOk() ->assertJson(['hearted' => true]); $this->assertEquals(true, $comment->fresh()->hearted); $this->postJson(route('comments.heart', $comment)) ->assertOk() ->assertJson(['hearted' => false]); } public function test_non_owner_cannot_heart_a_comment(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $comment = Comment::create([ 'user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'nice video!', ]); $this->actingAs($viewer); $this->postJson(route('comments.heart', $comment))->assertStatus(403); $this->assertEquals(false, $comment->fresh()->hearted); } // ─── Pin ──────────────────────────────────────── public function test_owner_pins_at_most_one_comment_at_a_time(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($owner); $a = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'A']); $b = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'B']); $this->postJson(route('comments.pin', $a))->assertOk()->assertJson(['pinned' => true]); $this->assertTrue((bool) $a->fresh()->pinned); $this->assertFalse((bool) $b->fresh()->pinned); // Pinning B must unpin A. $this->postJson(route('comments.pin', $b))->assertOk()->assertJson(['pinned' => true]); $this->assertFalse((bool) $a->fresh()->pinned); $this->assertTrue((bool) $b->fresh()->pinned); } public function test_cannot_pin_a_reply(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($owner); $parent = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'top']); $reply = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'parent_id' => $parent->id, 'body' => 'r']); $this->postJson(route('comments.pin', $reply))->assertStatus(422); $this->assertFalse((bool) $reply->fresh()->pinned); } public function test_non_owner_cannot_pin_a_comment(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($viewer); $comment = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'self']); $this->postJson(route('comments.pin', $comment))->assertStatus(403); } // ─── Pinned comments sort first in the component ── public function test_pinned_comments_render_before_newer_unpinned_ones(): void { [$owner, $viewer, $video] = $this->makeOwnerViewerVideo(); $this->actingAs($owner); $pinned = Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'OLDPINNED', 'pinned' => true]); // Date it older than the second one so sort would normally send it to the bottom. $pinned->created_at = now()->subDays(5); $pinned->save(); Comment::create(['user_id' => $viewer->id, 'video_id' => $video->id, 'body' => 'NEWFRESH']); $html = view('components.video-comments', ['video' => $video])->render(); // Match on the unique comment-id markup so count/header text can't // interfere with the order check. $pOld = strpos($html, 'id="comment-' . $pinned->id . '"'); $pNew = strpos($html, 'id="comment-' . ($pinned->id + 1) . '"'); $this->assertNotFalse($pOld); $this->assertNotFalse($pNew); $this->assertLessThan($pNew, $pOld, 'Pinned comment must render before a newer unpinned one.'); } // ─── Helpers ──────────────────────────────────── private function makeOwnerViewerVideo(): array { $owner = User::factory()->create(['username' => 'own-'.substr(bin2hex(random_bytes(4)), 0, 6)]); $viewer = User::factory()->create(['username' => 'vwr-'.substr(bin2hex(random_bytes(4)), 0, 6)]); $video = Video::create([ 'user_id' => $owner->id, 'title' => 'test', 'description' => '', 'filename' => 'users/x/videos/y/video.mp4', 'path' => 'users/x/videos/y/video.mp4', 'type' => 'generic', 'visibility' => 'public', 'status' => 'ready', 'share_token' => bin2hex(random_bytes(8)), ]); return [$owner, $viewer, $video]; } }