Files
scrum-poker-internal/tests/Feature/RoomBroadcastingTest.php
T

175 lines
5.4 KiB
PHP

<?php
use App\Enums\RoomStateChange;
use App\Events\RoomStateChanged;
use App\Jobs\CloseInactiveRooms;
use App\Models\Room;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
function createBroadcastRoom(array $attributes = []): Room
{
return Room::create([
'title' => 'Realtime Planning',
'deck_config' => [
'name' => 'Fibonacci',
'cards' => [
['key' => '3', 'label' => '3'],
['key' => '5', 'label' => '5'],
],
],
...$attributes,
]);
}
test('room state broadcasts expose only invalidation metadata', function () {
$event = new RoomStateChanged(
'019b8f80-37c4-71a3-b415-5f2697332c16',
7,
RoomStateChange::VoteChanged,
);
expect($event->broadcastAs())->toBe('room.state.changed')
->and($event->broadcastOn()->name)->toBe('rooms.019b8f80-37c4-71a3-b415-5f2697332c16')
->and($event->broadcastWith())->toBe([
'version' => 7,
'change' => 'vote_changed',
])
->and($event->broadcastWith())->not->toHaveKeys([
'player_name',
'vote',
]);
});
test('joining a room broadcasts the new room version', function () {
Event::fake([RoomStateChanged::class]);
$room = createBroadcastRoom();
$this->post(route('room.code'), [
'join_code' => $room->join_code,
'player_name' => 'Anna',
])->assertRedirect(route('room.show', $room));
Event::assertDispatched(RoomStateChanged::class, fn (RoomStateChanged $event): bool => $event->roomId === $room->id
&& $event->version === $room->fresh()->state_version
&& $event->change === RoomStateChange::PlayerJoined
);
});
test('voting and round actions broadcast their state changes', function () {
Event::fake([RoomStateChanged::class]);
$room = createBroadcastRoom();
$moderator = $room->players()->create([
'name' => 'Eric',
'is_moderator' => true,
]);
$session = ["room_players.{$room->id}" => $moderator->id];
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '5'])
->assertRedirect();
$this->withSession($session)
->post(route('room.reveal', $room))
->assertRedirect();
$this->withSession($session)
->post(route('room.next-round', $room))
->assertRedirect();
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '3'])
->assertRedirect();
$this->withSession($session)
->post(route('room.reset', $room))
->assertRedirect();
foreach ([
RoomStateChange::VoteChanged,
RoomStateChange::CardsRevealed,
RoomStateChange::NextRound,
RoomStateChange::VotesReset,
] as $change) {
Event::assertDispatched(
RoomStateChanged::class,
fn (RoomStateChanged $event): bool => $event->change === $change,
);
}
Event::assertDispatchedTimes(RoomStateChanged::class, 5);
});
test('failed room actions do not broadcast updates', function () {
Event::fake([RoomStateChanged::class]);
$room = createBroadcastRoom();
$player = $room->players()->create([
'name' => 'Anna',
'is_moderator' => false,
]);
$this->withSession(["room_players.{$room->id}" => $player->id])
->post(route('room.reveal', $room))
->assertForbidden();
$this->withSession(["room_players.{$room->id}" => $player->id])
->post(route('room.vote.store', $room), ['value' => '100'])
->assertSessionHasErrors('value');
Event::assertNotDispatched(RoomStateChanged::class);
});
test('reopening and deleting a room broadcast lifecycle changes', function () {
Event::fake([RoomStateChanged::class]);
$owner = User::factory()->create();
$room = createBroadcastRoom([
'owner_id' => $owner->id,
'closed_at' => now(),
]);
$room->players()->create([
'user_id' => $owner->id,
'name' => 'Eric',
'is_moderator' => true,
]);
$this->actingAs($owner)
->post(route('room.reopen', $room))
->assertRedirect(route('room.show', $room));
$this->actingAs($owner)
->delete(route('room.destroy', $room))
->assertRedirect(route('dashboard'));
Event::assertDispatched(
RoomStateChanged::class,
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::RoomReopened,
);
Event::assertDispatched(
RoomStateChanged::class,
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::RoomDeleted,
);
Event::assertDispatchedTimes(RoomStateChanged::class, 2);
});
test('the cleanup job broadcasts only rooms it actually closes', function () {
Carbon::setTestNow('2026-08-16 12:00:00');
Event::fake([RoomStateChanged::class]);
$expiredRoom = createBroadcastRoom([
'last_activity_at' => now()->subDays(14),
]);
createBroadcastRoom([
'last_activity_at' => now()->subDays(14)->addSecond(),
]);
$job = new CloseInactiveRooms;
$job->handle();
$job->handle();
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
Event::assertDispatched(RoomStateChanged::class, fn (RoomStateChanged $event): bool => $event->roomId === $expiredRoom->id
&& $event->version === 2
&& $event->change === RoomStateChange::RoomClosed
);
});