mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 17:12:08 +00:00
kick forever
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\RoomStateChange;
|
||||
use App\Events\RoomStateChanged;
|
||||
use App\Jobs\RemoveAbsentPlayers;
|
||||
use App\Models\Room;
|
||||
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('the migration removes old inactive and absent players while keeping present players', 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(),
|
||||
]);
|
||||
|
||||
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();
|
||||
|
||||
$this->assertModelMissing($previouslyInactive);
|
||||
$this->assertModelMissing($absent);
|
||||
$this->assertModelExists($present);
|
||||
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',
|
||||
'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);
|
||||
|
||||
Event::assertDispatched(
|
||||
RoomStateChanged::class,
|
||||
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerKicked
|
||||
&& $event->affectedPlayerId === $absentPlayer->id
|
||||
&& collect($event->state['players'])->doesntContain('id', $absentPlayer->id),
|
||||
);
|
||||
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);
|
||||
});
|
||||
@@ -46,7 +46,6 @@ test('embedded guest access reuses the session player and applies a changed disp
|
||||
$room = createEmbeddedRoom();
|
||||
$player = $room->players()->create([
|
||||
'name' => 'Old name',
|
||||
'inactive_at' => now(),
|
||||
]);
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $player->id])
|
||||
@@ -57,8 +56,7 @@ test('embedded guest access reuses the session player and applies a changed disp
|
||||
->assertOk();
|
||||
|
||||
expect($room->players()->count())->toBe(1)
|
||||
->and($player->fresh()->name)->toBe('New name')
|
||||
->and($player->fresh()->inactive_at)->toBeNull();
|
||||
->and($player->fresh()->name)->toBe('New name');
|
||||
});
|
||||
|
||||
test('an authenticated user automatically joins the embedded room', function () {
|
||||
@@ -92,7 +90,6 @@ test('embedded access reuses the user player and applies a changed display name'
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Old name',
|
||||
'inactive_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
@@ -103,8 +100,7 @@ test('embedded access reuses the user player and applies a changed display name'
|
||||
->assertOk();
|
||||
|
||||
expect($room->players()->count())->toBe(1)
|
||||
->and($player->fresh()->name)->toBe('New name')
|
||||
->and($player->fresh()->inactive_at)->toBeNull();
|
||||
->and($player->fresh()->name)->toBe('New name');
|
||||
});
|
||||
|
||||
test('an embedded guest keeps the same player after logging in separately', function () {
|
||||
|
||||
@@ -20,11 +20,6 @@ test('favorited rooms are presented on the dashboard with player counts', functi
|
||||
'name' => 'Active player',
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
$room->players()->create([
|
||||
'name' => 'Inactive player',
|
||||
'last_seen_at' => now()->subHour(),
|
||||
'inactive_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('room.favorite', $room))
|
||||
@@ -39,8 +34,7 @@ test('favorited rooms are presented on the dashboard with player counts', functi
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->has('favoriteRooms', 1)
|
||||
->where('favoriteRooms.0.id', $room->id)
|
||||
->where('favoriteRooms.0.players_count', 2)
|
||||
->where('favoriteRooms.0.active_player_count', 1));
|
||||
->where('favoriteRooms.0.players_count', 1));
|
||||
});
|
||||
|
||||
test('the room resource exposes favorite and owner state', function () {
|
||||
|
||||
@@ -216,15 +216,16 @@ test('an active guest with a room session skips the join form', function () {
|
||||
->assertRedirect(route('room.show', $room));
|
||||
});
|
||||
|
||||
test('an inactive guest is sent through join and reuses the existing player', function () {
|
||||
test('a removed guest joins again with a new player identity', function () {
|
||||
$room = createLifecycleRoom();
|
||||
$player = $room->players()->create([
|
||||
'name' => 'Returning guest',
|
||||
'last_seen_at' => now()->subHours(2),
|
||||
'inactive_at' => now()->subHour(),
|
||||
]);
|
||||
$session = ["room_players.{$room->id}" => $player->id];
|
||||
|
||||
$player->delete();
|
||||
|
||||
$this->withSession($session)
|
||||
->get(route('room.show', $room))
|
||||
->assertRedirect(route('room.join', ['join_code' => $room->join_code]));
|
||||
@@ -233,7 +234,7 @@ test('an inactive guest is sent through join and reuses the existing player', fu
|
||||
->get(route('room.join', ['join_code' => $room->join_code]))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('rooms/Join')
|
||||
->where('defaultPlayerName', 'Returning guest'));
|
||||
->where('defaultPlayerName', null));
|
||||
|
||||
$this->withSession($session)
|
||||
->post(route('room.code'), [
|
||||
@@ -243,7 +244,7 @@ test('an inactive guest is sent through join and reuses the existing player', fu
|
||||
->assertRedirect(route('room.show', $room));
|
||||
|
||||
expect($room->players()->count())->toBe(1)
|
||||
->and($player->fresh()->inactive_at)->toBeNull();
|
||||
->and($room->players()->sole()->id)->not->toBe($player->id);
|
||||
});
|
||||
|
||||
test('showing or polling a room does not count as room activity', function () {
|
||||
|
||||
@@ -44,7 +44,6 @@ test('the room owner sees active room players on the dashboard', function () {
|
||||
$room = createRoomForPlayerKick($owner);
|
||||
$moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
||||
$participant = addPlayerToKickRoom($room, 'Anna');
|
||||
addPlayerToKickRoom($room, 'Inactive')->update(['inactive_at' => now()]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get(route('dashboard'))
|
||||
@@ -77,8 +76,8 @@ test('the room owner can kick an active participant', function () {
|
||||
->delete(route('room.player.destroy', [$room, $participant]))
|
||||
->assertRedirect();
|
||||
|
||||
expect($participant->fresh()->inactive_at)->not->toBeNull()
|
||||
->and($room->fresh()->state_version)->toBe($previousVersion + 1);
|
||||
$this->assertModelMissing($participant);
|
||||
expect($room->fresh()->state_version)->toBe($previousVersion + 1);
|
||||
|
||||
Event::assertDispatched(
|
||||
RoomStateChanged::class,
|
||||
@@ -104,6 +103,36 @@ test('the room owner can kick an active participant', function () {
|
||||
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]);
|
||||
@@ -116,7 +145,7 @@ test('the room moderator can kick an active participant without shared managemen
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertRedirect();
|
||||
|
||||
expect($target->fresh()->inactive_at)->not->toBeNull();
|
||||
$this->assertModelMissing($target);
|
||||
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
||||
});
|
||||
|
||||
@@ -135,7 +164,7 @@ test('a participant cannot kick another player when participant management is di
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($target->fresh()->inactive_at)->toBeNull();
|
||||
$this->assertModelExists($target);
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
@@ -154,7 +183,7 @@ test('a participant can kick another player when participant management is enabl
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertRedirect();
|
||||
|
||||
expect($target->fresh()->inactive_at)->not->toBeNull();
|
||||
$this->assertModelMissing($target);
|
||||
Event::assertDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
@@ -171,7 +200,7 @@ test('a participant cannot kick themselves when participant management is enable
|
||||
->delete(route('room.player.destroy', [$room, $participant]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($participant->fresh()->inactive_at)->toBeNull();
|
||||
$this->assertModelExists($participant);
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
@@ -189,7 +218,7 @@ test('an anonymous visitor without a room session cannot kick from an ownerless
|
||||
$this->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($target->fresh()->inactive_at)->toBeNull();
|
||||
$this->assertModelExists($target);
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
@@ -204,7 +233,7 @@ test('the room owner cannot kick their own player', function () {
|
||||
->delete(route('room.player.destroy', [$room, $moderator]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($moderator->fresh()->inactive_at)->toBeNull();
|
||||
$this->assertModelExists($moderator);
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
@@ -221,6 +250,6 @@ test('the room owner cannot kick a player from another room', function () {
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($target->fresh()->inactive_at)->toBeNull();
|
||||
$this->assertModelExists($target);
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user