From 4dfe79ce5c4025c133dfa078d5ef6e9a07b7bbc4 Mon Sep 17 00:00:00 2001 From: Twyco Date: Thu, 24 Sep 2026 14:38:18 +0200 Subject: [PATCH] kick forever --- app/Http/Controllers/RoomJoinController.php | 1 - app/Http/Middleware/EnsureRoomParticipant.php | 6 -- app/Http/Requests/KickRoomPlayerRequest.php | 2 +- app/Http/Resources/DashboardRoomResource.php | 5 +- app/Jobs/MarkInactivePlayers.php | 22 ----- app/Jobs/RemoveAbsentPlayers.php | 76 +++++++++++++++ app/Models/Player.php | 14 --- app/Models/Room.php | 6 -- app/Services/Rooms/DashboardRoomService.php | 4 +- app/Services/Rooms/RoomPageService.php | 1 - .../Rooms/RoomParticipationService.php | 22 +---- .../Rooms/RoomRealtimeStateService.php | 1 - ...048_remove_previously_inactive_players.php | 42 +++++++++ .../dashboard/DashboardRoomCard.vue | 3 +- resources/js/pages/auth/Login.vue | 6 +- resources/js/types/dashboard.ts | 1 - routes/console.php | 4 +- tests/Feature/AbsentPlayerRemovalTest.php | 94 +++++++++++++++++++ tests/Feature/EmbeddedRoomTest.php | 8 +- tests/Feature/RoomFavoritesTest.php | 8 +- tests/Feature/RoomLifecycleTest.php | 9 +- tests/Feature/RoomPlayerKickTest.php | 49 ++++++++-- 22 files changed, 271 insertions(+), 113 deletions(-) delete mode 100644 app/Jobs/MarkInactivePlayers.php create mode 100644 app/Jobs/RemoveAbsentPlayers.php create mode 100644 database/migrations/2026_09_24_123048_remove_previously_inactive_players.php create mode 100644 tests/Feature/AbsentPlayerRemovalTest.php diff --git a/app/Http/Controllers/RoomJoinController.php b/app/Http/Controllers/RoomJoinController.php index b70167f..1a100e4 100644 --- a/app/Http/Controllers/RoomJoinController.php +++ b/app/Http/Controllers/RoomJoinController.php @@ -32,7 +32,6 @@ class RoomJoinController extends Controller $room !== null && ! $room->isClosed() && $existingPlayer instanceof Player - && $existingPlayer->inactive_at === null ) { return to_route('room.show', $room); } diff --git a/app/Http/Middleware/EnsureRoomParticipant.php b/app/Http/Middleware/EnsureRoomParticipant.php index 3307592..5f2c4e4 100644 --- a/app/Http/Middleware/EnsureRoomParticipant.php +++ b/app/Http/Middleware/EnsureRoomParticipant.php @@ -113,12 +113,6 @@ class EnsureRoomParticipant ]); } - if ($player->inactive_at !== null) { - return to_route('room.join', [ - 'join_code' => $room->join_code, - ]); - } - $player->markAsSeen(); $request->attributes->set('currentPlayer', $player); diff --git a/app/Http/Requests/KickRoomPlayerRequest.php b/app/Http/Requests/KickRoomPlayerRequest.php index 38b7193..07b4091 100644 --- a/app/Http/Requests/KickRoomPlayerRequest.php +++ b/app/Http/Requests/KickRoomPlayerRequest.php @@ -48,7 +48,7 @@ class KickRoomPlayerRequest extends FormRequest return null; } - return $room->activePlayers()->find((int) $playerId); + return $room->players()->find((int) $playerId); } /** diff --git a/app/Http/Resources/DashboardRoomResource.php b/app/Http/Resources/DashboardRoomResource.php index 373d405..a215fca 100644 --- a/app/Http/Resources/DashboardRoomResource.php +++ b/app/Http/Resources/DashboardRoomResource.php @@ -18,9 +18,8 @@ class DashboardRoomResource extends JsonResource 'join_code' => $this->resource->join_code, 'current_round' => $this->resource->current_round, 'players_count' => $this->resource->players_count, - 'active_player_count' => $this->resource->active_player_count, - 'players' => $this->resource->relationLoaded('activePlayers') - ? $this->resource->activePlayers + 'players' => $this->resource->relationLoaded('players') + ? $this->resource->players ->map(fn ($player): array => [ 'id' => $player->id, 'name' => $player->name, diff --git a/app/Jobs/MarkInactivePlayers.php b/app/Jobs/MarkInactivePlayers.php deleted file mode 100644 index 81b6881..0000000 --- a/app/Jobs/MarkInactivePlayers.php +++ /dev/null @@ -1,22 +0,0 @@ -whereNull('inactive_at') - ->where('last_seen_at', '<=', now()->subHour()) - ->update([ - 'inactive_at' => now(), - ]); - } -} diff --git a/app/Jobs/RemoveAbsentPlayers.php b/app/Jobs/RemoveAbsentPlayers.php new file mode 100644 index 0000000..7ae5fdb --- /dev/null +++ b/app/Jobs/RemoveAbsentPlayers.php @@ -0,0 +1,76 @@ +subHour(); + + Player::query() + ->where(function ($query) use ($cutoff): void { + $query->where('last_seen_at', '<=', $cutoff) + ->orWhere(function ($query) use ($cutoff): void { + $query->whereNull('last_seen_at') + ->where('created_at', '<=', $cutoff); + }); + }) + ->select(['id', 'room_id']) + ->chunkById(100, function ($players) use ($cutoff, $realtimeState): void { + foreach ($players as $player) { + $room = DB::transaction(function () use ($player, $cutoff): ?Room { + $room = Room::withTrashed() + ->lockForUpdate() + ->find($player->room_id); + + if ($room === null) { + return null; + } + + $currentPlayer = $room->players() + ->lockForUpdate() + ->find($player->id); + + if ($currentPlayer === null) { + return null; + } + + $lastActivity = $currentPlayer->last_seen_at ?? $currentPlayer->created_at; + + if ($lastActivity === null || $lastActivity->greaterThan($cutoff)) { + return null; + } + + $currentPlayer->delete(); + $room->update([ + 'state_version' => $room->state_version + 1, + ]); + + return $room; + }); + + if ($room !== null && ! $room->trashed()) { + RoomStateChanged::dispatch( + $room->id, + $room->state_version, + RoomStateChange::PlayerKicked, + $realtimeState->for($room), + $player->id, + ); + } + } + }); + } +} diff --git a/app/Models/Player.php b/app/Models/Player.php index 585bd58..f3f383b 100644 --- a/app/Models/Player.php +++ b/app/Models/Player.php @@ -2,7 +2,6 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -16,7 +15,6 @@ use Illuminate\Support\Carbon; * @property string $name * @property bool $is_moderator * @property Carbon|null $last_seen_at - * @property Carbon|null $inactive_at * @property Carbon|null $created_at * @property Carbon|null $updated_at * @property-read Room $room @@ -30,7 +28,6 @@ class Player extends Model 'name', 'is_moderator', 'last_seen_at', - 'inactive_at', ]; protected function casts(): array @@ -38,7 +35,6 @@ class Player extends Model return [ 'is_moderator' => 'boolean', 'last_seen_at' => 'datetime', - 'inactive_at' => 'datetime', ]; } @@ -85,14 +81,4 @@ class Player extends Model 'last_seen_at' => now(), ]); } - - /** - * @param Builder $query - * @return Builder - */ - public function scopeActive(Builder $query): Builder - { - return $query->whereNull('inactive_at'); - - } } diff --git a/app/Models/Room.php b/app/Models/Room.php index 32d3a75..3790f1e 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -93,12 +93,6 @@ class Room extends Model } - /** @return HasMany */ - public function activePlayers(): HasMany - { - return $this->hasMany(Player::class)->whereNull('inactive_at'); - } - /** @return BelongsTo */ public function owner(): BelongsTo { diff --git a/app/Services/Rooms/DashboardRoomService.php b/app/Services/Rooms/DashboardRoomService.php index d5a3319..2c43765 100644 --- a/app/Services/Rooms/DashboardRoomService.php +++ b/app/Services/Rooms/DashboardRoomService.php @@ -16,13 +16,12 @@ class DashboardRoomService $ownedRooms = $user ->ownedRooms() ->with([ - 'activePlayers' => fn ($query) => $query + 'players' => fn ($query) => $query ->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at']) ->orderByDesc('is_moderator') ->orderBy('created_at'), ]) ->withCount('players') - ->withCount(['activePlayers as active_player_count']) ->orderByRaw('closed_at IS NOT NULL') ->orderByDesc('last_activity_at') ->get(); @@ -30,7 +29,6 @@ class DashboardRoomService $favoriteRooms = $user ->favoriteRooms() ->withCount('players') - ->withCount(['activePlayers as active_player_count']) ->orderByRaw('closed_at IS NOT NULL') ->orderByDesc('last_activity_at') ->get(); diff --git a/app/Services/Rooms/RoomPageService.php b/app/Services/Rooms/RoomPageService.php index 5ee61ee..bbb852e 100644 --- a/app/Services/Rooms/RoomPageService.php +++ b/app/Services/Rooms/RoomPageService.php @@ -20,7 +20,6 @@ class RoomPageService $room->load([ 'players' => fn ($query) => $query - ->active() ->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at']) ->orderByDesc('is_moderator') ->orderBy('created_at'), diff --git a/app/Services/Rooms/RoomParticipationService.php b/app/Services/Rooms/RoomParticipationService.php index 14008d1..f00d80f 100644 --- a/app/Services/Rooms/RoomParticipationService.php +++ b/app/Services/Rooms/RoomParticipationService.php @@ -80,7 +80,6 @@ class RoomParticipationService } else { $player->update([ 'last_seen_at' => now(), - 'inactive_at' => null, ]); } @@ -150,16 +149,13 @@ class RoomParticipationService ]); $change = RoomStateChange::PlayerJoined; } else { - if ($player->inactive_at !== null) { - $change = RoomStateChange::PlayerJoined; - } elseif ($player->name !== $playerName) { + if ($player->name !== $playerName) { $change = RoomStateChange::PlayerUpdated; } $updates = [ 'name' => $playerName, 'last_seen_at' => now(), - 'inactive_at' => null, ]; if ( @@ -282,14 +278,14 @@ class RoomParticipationService ?User $user, ?Player $currentPlayer, ): void { - $stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): ?int { + $stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): int { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); $lockedCurrentPlayer = $currentPlayer === null ? null - : $lockedRoom->activePlayers() + : $lockedRoom->players() ->lockForUpdate() ->find($currentPlayer->id); $isOwner = $user !== null && $lockedRoom->owner_id === $user->id; @@ -312,13 +308,7 @@ class RoomParticipationService 403, ); - if ($lockedPlayer->inactive_at !== null) { - return null; - } - - $lockedPlayer->update([ - 'inactive_at' => now(), - ]); + $lockedPlayer->delete(); $lockedRoom->update([ 'last_activity_at' => now(), 'state_version' => $lockedRoom->state_version + 1, @@ -327,10 +317,6 @@ class RoomParticipationService return $lockedRoom->state_version; }); - if ($stateVersion === null) { - return; - } - RoomStateChanged::dispatch( $room->id, $stateVersion, diff --git a/app/Services/Rooms/RoomRealtimeStateService.php b/app/Services/Rooms/RoomRealtimeStateService.php index 2b120f6..8ccb206 100644 --- a/app/Services/Rooms/RoomRealtimeStateService.php +++ b/app/Services/Rooms/RoomRealtimeStateService.php @@ -32,7 +32,6 @@ class RoomRealtimeStateService $room->refresh(); $room->load([ 'players' => fn ($query) => $query - ->active() ->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at']) ->orderByDesc('is_moderator') ->orderBy('created_at'), diff --git a/database/migrations/2026_09_24_123048_remove_previously_inactive_players.php b/database/migrations/2026_09_24_123048_remove_previously_inactive_players.php new file mode 100644 index 0000000..d31d84c --- /dev/null +++ b/database/migrations/2026_09_24_123048_remove_previously_inactive_players.php @@ -0,0 +1,42 @@ +subHour(); + + DB::table('players') + ->where(function ($query) use ($cutoff): void { + $query->whereNotNull('inactive_at') + ->orWhere('last_seen_at', '<=', $cutoff) + ->orWhere(function ($query) use ($cutoff): void { + $query->whereNull('last_seen_at') + ->where('created_at', '<=', $cutoff); + }); + }) + ->delete(); + + Schema::table('players', function (Blueprint $table): void { + $table->dropColumn('inactive_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('players', function (Blueprint $table): void { + $table->timestamp('inactive_at')->nullable()->after('last_seen_at'); + }); + } +}; diff --git a/resources/js/components/dashboard/DashboardRoomCard.vue b/resources/js/components/dashboard/DashboardRoomCard.vue index 602611d..60a8615 100644 --- a/resources/js/components/dashboard/DashboardRoomCard.vue +++ b/resources/js/components/dashboard/DashboardRoomCard.vue @@ -47,8 +47,7 @@ function formatDate(value: string | null): string {
- {{ room.players_count }} Spieler / - {{ room.active_player_count }} aktiv + {{ room.players_count }} Spieler Runde {{ room.current_round }} diff --git a/resources/js/pages/auth/Login.vue b/resources/js/pages/auth/Login.vue index 5e4288d..ead9e38 100644 --- a/resources/js/pages/auth/Login.vue +++ b/resources/js/pages/auth/Login.vue @@ -85,11 +85,7 @@ defineProps<{
diff --git a/resources/js/types/dashboard.ts b/resources/js/types/dashboard.ts index 1e4a2b3..233a366 100644 --- a/resources/js/types/dashboard.ts +++ b/resources/js/types/dashboard.ts @@ -11,7 +11,6 @@ export type DashboardRoom = { join_code: string; current_round: number; players_count: number; - active_player_count: number; players: DashboardRoomPlayer[]; last_activity_at: string | null; closed_at: string | null; diff --git a/routes/console.php b/routes/console.php index b0148e7..59d9c07 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,7 +1,7 @@ weeklyOn(1, '02:00') ->withoutOverlapping(); -Schedule::job(new MarkInactivePlayers) +Schedule::job(new RemoveAbsentPlayers) ->everyFiveMinutes() ->withoutOverlapping(); diff --git a/tests/Feature/AbsentPlayerRemovalTest.php b/tests/Feature/AbsentPlayerRemovalTest.php new file mode 100644 index 0000000..acfcc81 --- /dev/null +++ b/tests/Feature/AbsentPlayerRemovalTest.php @@ -0,0 +1,94 @@ +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); +}); diff --git a/tests/Feature/EmbeddedRoomTest.php b/tests/Feature/EmbeddedRoomTest.php index ff10a2b..d74355d 100644 --- a/tests/Feature/EmbeddedRoomTest.php +++ b/tests/Feature/EmbeddedRoomTest.php @@ -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 () { diff --git a/tests/Feature/RoomFavoritesTest.php b/tests/Feature/RoomFavoritesTest.php index 6ade311..41e00f8 100644 --- a/tests/Feature/RoomFavoritesTest.php +++ b/tests/Feature/RoomFavoritesTest.php @@ -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 () { diff --git a/tests/Feature/RoomLifecycleTest.php b/tests/Feature/RoomLifecycleTest.php index af6bc1f..b41cbee 100644 --- a/tests/Feature/RoomLifecycleTest.php +++ b/tests/Feature/RoomLifecycleTest.php @@ -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 () { diff --git a/tests/Feature/RoomPlayerKickTest.php b/tests/Feature/RoomPlayerKickTest.php index fb574c0..153e684 100644 --- a/tests/Feature/RoomPlayerKickTest.php +++ b/tests/Feature/RoomPlayerKickTest.php @@ -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); });