mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 19:12:10 +00:00
185 lines
6.5 KiB
PHP
185 lines
6.5 KiB
PHP
<?php
|
|
|
|
use App\Enums\RoomStateChange;
|
|
use App\Events\RoomStateChanged;
|
|
use App\Jobs\RemoveAbsentPlayers;
|
|
use App\Models\Room;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
test('the player table no longer stores an inactive status', function () {
|
|
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
|
});
|
|
|
|
test('migrations remove absent players and reconcile guest rooms', function () {
|
|
Schema::table('players', function (Blueprint $table): void {
|
|
$table->timestamp('inactive_at')->nullable();
|
|
});
|
|
|
|
$room = Room::create([
|
|
'title' => 'Planning',
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
$previouslyInactive = $room->players()->create([
|
|
'name' => 'Inactive',
|
|
'last_seen_at' => now(),
|
|
]);
|
|
$absent = $room->players()->create([
|
|
'name' => 'Nico',
|
|
'last_seen_at' => now()->subHours(2),
|
|
]);
|
|
$present = $room->players()->create([
|
|
'name' => 'Anna',
|
|
'last_seen_at' => now(),
|
|
]);
|
|
$emptyGuestRoom = Room::create([
|
|
'title' => 'Empty guest room',
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
$owner = User::factory()->create();
|
|
$emptyAccountRoom = Room::create([
|
|
'owner_id' => $owner->id,
|
|
'is_persistent' => true,
|
|
'title' => 'Persistent account room',
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
|
|
DB::table('players')->where('id', $previouslyInactive->id)->update(['inactive_at' => now()]);
|
|
|
|
$migration = require database_path('migrations/2026_09_24_123048_remove_previously_inactive_players.php');
|
|
$migration->up();
|
|
$guestRoomMigration = require database_path('migrations/2026_09_24_125529_reconcile_guest_rooms_after_player_cleanup.php');
|
|
$guestRoomMigration->up();
|
|
|
|
$this->assertModelMissing($previouslyInactive);
|
|
$this->assertModelMissing($absent);
|
|
$this->assertModelExists($present);
|
|
$this->assertSoftDeleted($emptyGuestRoom);
|
|
$this->assertModelExists($emptyAccountRoom);
|
|
expect($present->fresh()->is_moderator)->toBeTrue();
|
|
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
|
});
|
|
|
|
test('the timeout removes absent players and their votes but keeps recent players', function () {
|
|
$room = Room::create([
|
|
'title' => 'Planning',
|
|
'current_round' => 1,
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
$absentPlayer = $room->players()->create([
|
|
'name' => 'Nico',
|
|
'is_moderator' => true,
|
|
'last_seen_at' => now()->subHour(),
|
|
]);
|
|
$recentPlayer = $room->players()->create([
|
|
'name' => 'Anna',
|
|
'last_seen_at' => now()->subHour()->addSecond(),
|
|
]);
|
|
$vote = $absentPlayer->votes()->create([
|
|
'room_id' => $room->id,
|
|
'round' => $room->current_round,
|
|
'value' => '3',
|
|
]);
|
|
$previousVersion = $room->fresh()->state_version;
|
|
|
|
Event::fake([RoomStateChanged::class]);
|
|
|
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
|
|
|
$this->assertModelMissing($absentPlayer);
|
|
$this->assertModelMissing($vote);
|
|
$this->assertModelExists($recentPlayer);
|
|
expect($room->fresh()->state_version)->toBe($previousVersion + 1);
|
|
expect($recentPlayer->fresh()->is_moderator)->toBeTrue();
|
|
|
|
Event::assertDispatched(
|
|
RoomStateChanged::class,
|
|
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerKicked
|
|
&& $event->affectedPlayerId === $absentPlayer->id
|
|
&& collect($event->state['players'])->doesntContain('id', $absentPlayer->id)
|
|
&& collect($event->state['players'])->firstWhere('id', $recentPlayer->id)['is_moderator'] === true,
|
|
);
|
|
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
|
|
|
$this->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($absentPlayer->id);
|
|
});
|
|
|
|
test('the timeout deletes a guest room when its last player is removed', function () {
|
|
$room = Room::create([
|
|
'title' => 'Guest planning',
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
$moderator = $room->players()->create([
|
|
'name' => 'Nico',
|
|
'is_moderator' => true,
|
|
'last_seen_at' => now()->subHour(),
|
|
]);
|
|
|
|
Event::fake([RoomStateChanged::class]);
|
|
|
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
|
|
|
$this->assertModelMissing($moderator);
|
|
$this->assertSoftDeleted($room);
|
|
Event::assertDispatched(
|
|
RoomStateChanged::class,
|
|
fn (RoomStateChanged $event): bool => $event->roomId === $room->id
|
|
&& $event->change === RoomStateChange::RoomDeleted,
|
|
);
|
|
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
|
|
|
$this->post(route('room.code'), [
|
|
'join_code' => $room->join_code,
|
|
'player_name' => 'Nico',
|
|
])->assertSessionHasErrors('join_code');
|
|
});
|
|
|
|
test('the owner regains moderation when joining a persistent room after a timeout', function () {
|
|
$owner = User::factory()->create();
|
|
$room = Room::create([
|
|
'owner_id' => $owner->id,
|
|
'is_persistent' => true,
|
|
'title' => 'Permanent planning',
|
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
|
]);
|
|
$oldModerator = $room->players()->create([
|
|
'user_id' => $owner->id,
|
|
'name' => 'Nico',
|
|
'is_moderator' => true,
|
|
'last_seen_at' => now()->subHour(),
|
|
]);
|
|
$guest = $room->players()->create([
|
|
'name' => 'Anna',
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
|
|
|
$this->assertModelMissing($oldModerator);
|
|
$this->assertModelExists($room);
|
|
expect($guest->fresh()->is_moderator)->toBeFalse();
|
|
|
|
$this->actingAs($owner)
|
|
->withSession(["room_players.{$room->id}" => $oldModerator->id])
|
|
->post(route('room.code'), [
|
|
'join_code' => $room->join_code,
|
|
'player_name' => 'Nico',
|
|
])
|
|
->assertRedirect(route('room.show', $room));
|
|
|
|
$newModerator = $room->players()->where('user_id', $owner->id)->sole();
|
|
|
|
expect($newModerator->id)->not->toBe($oldModerator->id)
|
|
->and($newModerator->is_moderator)->toBeTrue();
|
|
});
|