mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 18:12:09 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dfe79ce5c | ||
|
|
b190ff719e | ||
|
|
54d23c3660 |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class KickRoomPlayerRequest extends FormRequest
|
||||
return null;
|
||||
}
|
||||
|
||||
return $room->activePlayers()->find((int) $playerId);
|
||||
return $room->players()->find((int) $playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Player;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class MarkInactivePlayers implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Player::query()
|
||||
->whereNull('inactive_at')
|
||||
->where('last_seen_at', '<=', now()->subHour())
|
||||
->update([
|
||||
'inactive_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\RoomStateChange;
|
||||
use App\Events\RoomStateChanged;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\RoomRealtimeStateService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RemoveAbsentPlayers implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function handle(RoomRealtimeStateService $realtimeState): void
|
||||
{
|
||||
$cutoff = now()->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,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<Player> $query
|
||||
* @return Builder<Player>
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNull('inactive_at');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,6 @@ class Room extends Model
|
||||
|
||||
}
|
||||
|
||||
/** @return HasMany<Player, $this> */
|
||||
public function activePlayers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Player::class)->whereNull('inactive_at');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$cutoff = now()->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
Generated
+4
-3
@@ -4,6 +4,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrum-poker-internal",
|
||||
"dependencies": {
|
||||
"@inertiajs/vite": "^3.0.0",
|
||||
"@inertiajs/vue3": "^3.0.0",
|
||||
@@ -4959,9 +4960,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -47,8 +47,7 @@ function formatDate(value: string | null): string {
|
||||
<div class="mt-5 grid grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<Users class="size-4" />
|
||||
{{ room.players_count }} Spieler /
|
||||
{{ room.active_player_count }} aktiv
|
||||
{{ room.players_count }} Spieler
|
||||
</span>
|
||||
<span>Runde {{ room.current_round }}</span>
|
||||
<span class="col-span-2 inline-flex items-center gap-2">
|
||||
|
||||
@@ -85,11 +85,7 @@ defineProps<{
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="remember" class="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
name="remember"
|
||||
:tabindex="3"
|
||||
/>
|
||||
<Checkbox id="remember" name="remember" :tabindex="3" />
|
||||
<span>Remember me</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\CloseInactiveRooms;
|
||||
use App\Jobs\MarkInactivePlayers;
|
||||
use App\Jobs\RemoveAbsentPlayers;
|
||||
use Illuminate\Foundation\DevCommands;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
@@ -11,6 +11,6 @@ Schedule::job(new CloseInactiveRooms)
|
||||
->weeklyOn(1, '02:00')
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::job(new MarkInactivePlayers)
|
||||
Schedule::job(new RemoveAbsentPlayers)
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -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