$owner->id, 'title' => 'Team Planning', 'current_round' => 1, 'deck_config' => [ 'name' => 'Fibonacci', 'cards' => [ ['key' => '3', 'label' => '3'], ['key' => '5', 'label' => '5'], ], ], ...$attributes, ]); } function addPlayerToKickRoom( Room $room, string $name, ?User $user = null, bool $isModerator = false, ): Player { return $room->players()->create([ 'user_id' => $user?->id, 'name' => $name, 'is_moderator' => $isModerator, 'last_seen_at' => now(), ]); } test('the room owner sees active room players on the dashboard', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna'); $this->actingAs($owner) ->get(route('dashboard')) ->assertInertia(fn (Assert $page) => $page ->has('rooms.0.players', 2) ->where('rooms.0.players.0.id', $moderator->id) ->where('rooms.0.players.0.is_moderator', true) ->where('rooms.0.players.0.is_owner', true) ->where('rooms.0.players.1.id', $participant->id) ->where('rooms.0.players.1.is_moderator', false) ->where('rooms.0.players.1.is_owner', false)); }); test('the room owner can kick an active participant', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna'); $remainingPlayer = addPlayerToKickRoom($room, 'Ben'); $remainingPlayer->votes()->create([ 'room_id' => $room->id, 'round' => $room->current_round, 'value' => '5', ]); $previousVersion = $room->fresh()->state_version; Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $participant])) ->assertRedirect(); $this->assertModelMissing($participant); expect($room->fresh()->state_version)->toBe($previousVersion + 1); Event::assertDispatched( RoomStateChanged::class, function (RoomStateChanged $event) use ($participant, $remainingPlayer, $room): bool { if ( $event->roomId !== $room->id || $event->change !== RoomStateChange::PlayerKicked || $event->affectedPlayerId !== $participant->id || $event->state === null ) { return false; } $remainingPlayerState = collect($event->state['players']) ->firstWhere('id', $remainingPlayer->id); return collect($event->state['players'])->doesntContain('id', $participant->id) && $remainingPlayerState['has_voted'] === true && $remainingPlayerState['vote'] === null && $event->broadcastWith()['affected_player_id'] === $participant->id; }, ); Event::assertDispatchedTimes(RoomStateChanged::class, 1); }); test('a kicked guest loses their votes and their name becomes available', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); addPlayerToKickRoom($room, 'Moderator', $owner, true); $guest = addPlayerToKickRoom($room, 'Nico'); $vote = $guest->votes()->create([ 'room_id' => $room->id, 'round' => $room->current_round, 'value' => '3', ]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $guest])) ->assertRedirect(); $this->assertModelMissing($guest); $this->assertModelMissing($vote); auth()->logout(); $this->withSession(["room_players.{$room->id}" => $guest->id]) ->post(route('room.code'), [ 'join_code' => $room->join_code, 'player_name' => 'Nico', ]) ->assertRedirect(route('room.show', $room)); expect($room->players()->where('name', 'Nico')->sole()->id)->not->toBe($guest->id); }); test('the room moderator can kick an active participant without shared management', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner, ['owner_id' => null]); $moderator = addPlayerToKickRoom($room, 'Moderator', null, true); $target = addPlayerToKickRoom($room, 'Anna'); Event::fake([RoomStateChanged::class]); $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->delete(route('room.player.destroy', [$room, $target])) ->assertRedirect(); $this->assertModelMissing($target); Event::assertDispatchedTimes(RoomStateChanged::class, 1); }); test('a participant cannot kick another player when participant management is disabled', function () { $owner = User::factory()->create(); $participantUser = User::factory()->create(); $room = createRoomForPlayerKick($owner); addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna', $participantUser); $target = addPlayerToKickRoom($room, 'Ben'); Event::fake([RoomStateChanged::class]); $this->actingAs($participantUser) ->withSession(["room_players.{$room->id}" => $participant->id]) ->delete(route('room.player.destroy', [$room, $target])) ->assertForbidden(); $this->assertModelExists($target); Event::assertNotDispatched(RoomStateChanged::class); }); test('a participant can kick another player when participant management is enabled', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner, [ 'allow_participants_to_manage_rounds' => true, ]); addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna'); $target = addPlayerToKickRoom($room, 'Ben'); Event::fake([RoomStateChanged::class]); $this->withSession(["room_players.{$room->id}" => $participant->id]) ->delete(route('room.player.destroy', [$room, $target])) ->assertRedirect(); $this->assertModelMissing($target); Event::assertDispatched(RoomStateChanged::class); }); test('a participant cannot kick themselves when participant management is enabled', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner, [ 'allow_participants_to_manage_rounds' => true, ]); $participant = addPlayerToKickRoom($room, 'Anna'); Event::fake([RoomStateChanged::class]); $this->withSession(["room_players.{$room->id}" => $participant->id]) ->delete(route('room.player.destroy', [$room, $participant])) ->assertForbidden(); $this->assertModelExists($participant); Event::assertNotDispatched(RoomStateChanged::class); }); test('an anonymous visitor without a room session cannot kick from an ownerless room', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner, [ 'owner_id' => null, 'allow_participants_to_manage_rounds' => true, ]); addPlayerToKickRoom($room, 'Moderator', null, true); $target = addPlayerToKickRoom($room, 'Ben'); Event::fake([RoomStateChanged::class]); $this->delete(route('room.player.destroy', [$room, $target])) ->assertForbidden(); $this->assertModelExists($target); Event::assertNotDispatched(RoomStateChanged::class); }); test('the room owner cannot kick their own player', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true); Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $moderator])) ->assertForbidden(); $this->assertModelExists($moderator); Event::assertNotDispatched(RoomStateChanged::class); }); test('the room owner cannot kick a player from another room', function () { $owner = User::factory()->create(); $otherOwner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $otherRoom = createRoomForPlayerKick($otherOwner, ['title' => 'Other room']); $target = addPlayerToKickRoom($otherRoom, 'Anna'); Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $target])) ->assertForbidden(); $this->assertModelExists($target); Event::assertNotDispatched(RoomStateChanged::class); });