mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 23:32:09 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33df9e49ab | ||
|
|
4dfe79ce5c |
@@ -32,7 +32,6 @@ class RoomJoinController extends Controller
|
|||||||
$room !== null
|
$room !== null
|
||||||
&& ! $room->isClosed()
|
&& ! $room->isClosed()
|
||||||
&& $existingPlayer instanceof Player
|
&& $existingPlayer instanceof Player
|
||||||
&& $existingPlayer->inactive_at === null
|
|
||||||
) {
|
) {
|
||||||
return to_route('room.show', $room);
|
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();
|
$player->markAsSeen();
|
||||||
$request->attributes->set('currentPlayer', $player);
|
$request->attributes->set('currentPlayer', $player);
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class KickRoomPlayerRequest extends FormRequest
|
|||||||
return null;
|
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,
|
'join_code' => $this->resource->join_code,
|
||||||
'current_round' => $this->resource->current_round,
|
'current_round' => $this->resource->current_round,
|
||||||
'players_count' => $this->resource->players_count,
|
'players_count' => $this->resource->players_count,
|
||||||
'active_player_count' => $this->resource->active_player_count,
|
'players' => $this->resource->relationLoaded('players')
|
||||||
'players' => $this->resource->relationLoaded('activePlayers')
|
? $this->resource->players
|
||||||
? $this->resource->activePlayers
|
|
||||||
->map(fn ($player): array => [
|
->map(fn ($player): array => [
|
||||||
'id' => $player->id,
|
'id' => $player->id,
|
||||||
'name' => $player->name,
|
'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,111 @@
|
|||||||
|
<?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) {
|
||||||
|
$result = DB::transaction(function () use ($player, $cutoff): ?array {
|
||||||
|
$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();
|
||||||
|
$roomDeleted = false;
|
||||||
|
|
||||||
|
if ($room->owner_id === null && ! $room->trashed()) {
|
||||||
|
if (! $room->players()->exists()) {
|
||||||
|
$room->delete();
|
||||||
|
$roomDeleted = true;
|
||||||
|
} elseif (! $room->players()->where('is_moderator', true)->exists()) {
|
||||||
|
$successor = $room->players()
|
||||||
|
->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);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('created_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$successor?->update(['is_moderator' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$room->update([
|
||||||
|
'state_version' => $room->state_version + 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [$room, $roomDeleted];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($result === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$room, $roomDeleted] = $result;
|
||||||
|
|
||||||
|
if ($roomDeleted) {
|
||||||
|
RoomStateChanged::dispatch(
|
||||||
|
$room->id,
|
||||||
|
$room->state_version,
|
||||||
|
RoomStateChange::RoomDeleted,
|
||||||
|
);
|
||||||
|
} elseif (! $room->trashed()) {
|
||||||
|
RoomStateChanged::dispatch(
|
||||||
|
$room->id,
|
||||||
|
$room->state_version,
|
||||||
|
RoomStateChange::PlayerKicked,
|
||||||
|
$realtimeState->for($room),
|
||||||
|
$player->id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -16,7 +15,6 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property string $name
|
* @property string $name
|
||||||
* @property bool $is_moderator
|
* @property bool $is_moderator
|
||||||
* @property Carbon|null $last_seen_at
|
* @property Carbon|null $last_seen_at
|
||||||
* @property Carbon|null $inactive_at
|
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Room $room
|
* @property-read Room $room
|
||||||
@@ -30,7 +28,6 @@ class Player extends Model
|
|||||||
'name',
|
'name',
|
||||||
'is_moderator',
|
'is_moderator',
|
||||||
'last_seen_at',
|
'last_seen_at',
|
||||||
'inactive_at',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -38,7 +35,6 @@ class Player extends Model
|
|||||||
return [
|
return [
|
||||||
'is_moderator' => 'boolean',
|
'is_moderator' => 'boolean',
|
||||||
'last_seen_at' => 'datetime',
|
'last_seen_at' => 'datetime',
|
||||||
'inactive_at' => 'datetime',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,14 +81,4 @@ class Player extends Model
|
|||||||
'last_seen_at' => now(),
|
'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> */
|
/** @return BelongsTo<User, $this> */
|
||||||
public function owner(): BelongsTo
|
public function owner(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ class DashboardRoomService
|
|||||||
$ownedRooms = $user
|
$ownedRooms = $user
|
||||||
->ownedRooms()
|
->ownedRooms()
|
||||||
->with([
|
->with([
|
||||||
'activePlayers' => fn ($query) => $query
|
'players' => fn ($query) => $query
|
||||||
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
||||||
->orderByDesc('is_moderator')
|
->orderByDesc('is_moderator')
|
||||||
->orderBy('created_at'),
|
->orderBy('created_at'),
|
||||||
])
|
])
|
||||||
->withCount('players')
|
->withCount('players')
|
||||||
->withCount(['activePlayers as active_player_count'])
|
|
||||||
->orderByRaw('closed_at IS NOT NULL')
|
->orderByRaw('closed_at IS NOT NULL')
|
||||||
->orderByDesc('last_activity_at')
|
->orderByDesc('last_activity_at')
|
||||||
->get();
|
->get();
|
||||||
@@ -30,7 +29,6 @@ class DashboardRoomService
|
|||||||
$favoriteRooms = $user
|
$favoriteRooms = $user
|
||||||
->favoriteRooms()
|
->favoriteRooms()
|
||||||
->withCount('players')
|
->withCount('players')
|
||||||
->withCount(['activePlayers as active_player_count'])
|
|
||||||
->orderByRaw('closed_at IS NOT NULL')
|
->orderByRaw('closed_at IS NOT NULL')
|
||||||
->orderByDesc('last_activity_at')
|
->orderByDesc('last_activity_at')
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class RoomPageService
|
|||||||
|
|
||||||
$room->load([
|
$room->load([
|
||||||
'players' => fn ($query) => $query
|
'players' => fn ($query) => $query
|
||||||
->active()
|
|
||||||
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
||||||
->orderByDesc('is_moderator')
|
->orderByDesc('is_moderator')
|
||||||
->orderBy('created_at'),
|
->orderBy('created_at'),
|
||||||
|
|||||||
@@ -69,18 +69,19 @@ class RoomParticipationService
|
|||||||
$user,
|
$user,
|
||||||
$sessionPlayerIds[$room->id] ?? null,
|
$sessionPlayerIds[$room->id] ?? null,
|
||||||
);
|
);
|
||||||
|
$shouldModerate = $this->shouldModerate($room, $user);
|
||||||
|
|
||||||
if ($player === null) {
|
if ($player === null) {
|
||||||
$player = $room->players()->create([
|
$player = $room->players()->create([
|
||||||
'user_id' => $user?->id,
|
'user_id' => $user?->id,
|
||||||
'name' => $data['player_name'],
|
'name' => $data['player_name'],
|
||||||
'is_moderator' => false,
|
'is_moderator' => $shouldModerate,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$player->update([
|
$player->update([
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
'inactive_at' => null,
|
'is_moderator' => $player->is_moderator || $shouldModerate,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ class RoomParticipationService
|
|||||||
$user,
|
$user,
|
||||||
$sessionPlayerId,
|
$sessionPlayerId,
|
||||||
);
|
);
|
||||||
|
$shouldModerate = $this->shouldModerate($lockedRoom, $user);
|
||||||
|
|
||||||
$nameIsTaken = $lockedRoom->players()
|
$nameIsTaken = $lockedRoom->players()
|
||||||
->where('name', $playerName)
|
->where('name', $playerName)
|
||||||
@@ -145,21 +147,19 @@ class RoomParticipationService
|
|||||||
$player = $lockedRoom->players()->create([
|
$player = $lockedRoom->players()->create([
|
||||||
'user_id' => $user?->id,
|
'user_id' => $user?->id,
|
||||||
'name' => $playerName,
|
'name' => $playerName,
|
||||||
'is_moderator' => false,
|
'is_moderator' => $shouldModerate,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
$change = RoomStateChange::PlayerJoined;
|
$change = RoomStateChange::PlayerJoined;
|
||||||
} else {
|
} else {
|
||||||
if ($player->inactive_at !== null) {
|
if ($player->name !== $playerName || ($shouldModerate && ! $player->is_moderator)) {
|
||||||
$change = RoomStateChange::PlayerJoined;
|
|
||||||
} elseif ($player->name !== $playerName) {
|
|
||||||
$change = RoomStateChange::PlayerUpdated;
|
$change = RoomStateChange::PlayerUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
$updates = [
|
$updates = [
|
||||||
'name' => $playerName,
|
'name' => $playerName,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
'inactive_at' => null,
|
'is_moderator' => $player->is_moderator || $shouldModerate,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -200,6 +200,16 @@ class RoomParticipationService
|
|||||||
return $player;
|
return $player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldModerate(Room $room, ?User $user): bool
|
||||||
|
{
|
||||||
|
if ($user !== null && $room->owner_id === $user->id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $room->owner_id === null
|
||||||
|
&& ! $room->players()->where('is_moderator', true)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
private function findExistingEmbeddedPlayer(
|
private function findExistingEmbeddedPlayer(
|
||||||
Room $room,
|
Room $room,
|
||||||
?User $user,
|
?User $user,
|
||||||
@@ -222,7 +232,10 @@ class RoomParticipationService
|
|||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($sessionPlayer !== null) {
|
if (
|
||||||
|
$sessionPlayer !== null
|
||||||
|
&& ($user === null || $sessionPlayer->user_id === null || $sessionPlayer->user_id === $user->id)
|
||||||
|
) {
|
||||||
return $sessionPlayer;
|
return $sessionPlayer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,14 +295,14 @@ class RoomParticipationService
|
|||||||
?User $user,
|
?User $user,
|
||||||
?Player $currentPlayer,
|
?Player $currentPlayer,
|
||||||
): void {
|
): void {
|
||||||
$stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): ?int {
|
$stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): int {
|
||||||
$lockedRoom = Room::query()
|
$lockedRoom = Room::query()
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->findOrFail($room->id);
|
->findOrFail($room->id);
|
||||||
|
|
||||||
$lockedCurrentPlayer = $currentPlayer === null
|
$lockedCurrentPlayer = $currentPlayer === null
|
||||||
? null
|
? null
|
||||||
: $lockedRoom->activePlayers()
|
: $lockedRoom->players()
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->find($currentPlayer->id);
|
->find($currentPlayer->id);
|
||||||
$isOwner = $user !== null && $lockedRoom->owner_id === $user->id;
|
$isOwner = $user !== null && $lockedRoom->owner_id === $user->id;
|
||||||
@@ -312,13 +325,7 @@ class RoomParticipationService
|
|||||||
403,
|
403,
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($lockedPlayer->inactive_at !== null) {
|
$lockedPlayer->delete();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lockedPlayer->update([
|
|
||||||
'inactive_at' => now(),
|
|
||||||
]);
|
|
||||||
$lockedRoom->update([
|
$lockedRoom->update([
|
||||||
'last_activity_at' => now(),
|
'last_activity_at' => now(),
|
||||||
'state_version' => $lockedRoom->state_version + 1,
|
'state_version' => $lockedRoom->state_version + 1,
|
||||||
@@ -327,10 +334,6 @@ class RoomParticipationService
|
|||||||
return $lockedRoom->state_version;
|
return $lockedRoom->state_version;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($stateVersion === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomStateChanged::dispatch(
|
RoomStateChanged::dispatch(
|
||||||
$room->id,
|
$room->id,
|
||||||
$stateVersion,
|
$stateVersion,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class RoomRealtimeStateService
|
|||||||
$room->refresh();
|
$room->refresh();
|
||||||
$room->load([
|
$room->load([
|
||||||
'players' => fn ($query) => $query
|
'players' => fn ($query) => $query
|
||||||
->active()
|
|
||||||
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
||||||
->orderByDesc('is_moderator')
|
->orderByDesc('is_moderator')
|
||||||
->orderBy('created_at'),
|
->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('rooms')
|
||||||
|
->whereNull('owner_id')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereNotIn('id', DB::table('players')->select('room_id'))
|
||||||
|
->update(['deleted_at' => now()]);
|
||||||
|
|
||||||
|
DB::table('rooms')
|
||||||
|
->whereNull('owner_id')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereNotIn('id', DB::table('players')->where('is_moderator', true)->select('room_id'))
|
||||||
|
->select('id')
|
||||||
|
->chunkById(100, function ($rooms): void {
|
||||||
|
foreach ($rooms as $room) {
|
||||||
|
$playerId = DB::table('players')
|
||||||
|
->where('room_id', $room->id)
|
||||||
|
->orderBy('created_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($playerId !== null) {
|
||||||
|
DB::table('players')
|
||||||
|
->where('id', $playerId)
|
||||||
|
->update(['is_moderator' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Deleted rooms and former moderator assignments cannot be reconstructed.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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">
|
<div class="mt-5 grid grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<Users class="size-4" />
|
<Users class="size-4" />
|
||||||
{{ room.players_count }} Spieler /
|
{{ room.players_count }} Spieler
|
||||||
{{ room.active_player_count }} aktiv
|
|
||||||
</span>
|
</span>
|
||||||
<span>Runde {{ room.current_round }}</span>
|
<span>Runde {{ room.current_round }}</span>
|
||||||
<span class="col-span-2 inline-flex items-center gap-2">
|
<span class="col-span-2 inline-flex items-center gap-2">
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ export function useRoomRealtime(
|
|||||||
...payload.state,
|
...payload.state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentRoomPlayer = room.value.players.find(
|
||||||
|
(player) => player.id === currentPlayer.value.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentRoomPlayer !== undefined) {
|
||||||
|
currentPlayer.value = {
|
||||||
|
...currentPlayer.value,
|
||||||
|
is_moderator: currentRoomPlayer.is_moderator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.change === 'votes_reset' ||
|
payload.change === 'votes_reset' ||
|
||||||
payload.change === 'next_round'
|
payload.change === 'next_round'
|
||||||
|
|||||||
@@ -85,11 +85,7 @@ defineProps<{
|
|||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Label for="remember" class="flex items-center space-x-3">
|
<Label for="remember" class="flex items-center space-x-3">
|
||||||
<Checkbox
|
<Checkbox id="remember" name="remember" :tabindex="3" />
|
||||||
id="remember"
|
|
||||||
name="remember"
|
|
||||||
:tabindex="3"
|
|
||||||
/>
|
|
||||||
<span>Remember me</span>
|
<span>Remember me</span>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type DashboardRoom = {
|
|||||||
join_code: string;
|
join_code: string;
|
||||||
current_round: number;
|
current_round: number;
|
||||||
players_count: number;
|
players_count: number;
|
||||||
active_player_count: number;
|
|
||||||
players: DashboardRoomPlayer[];
|
players: DashboardRoomPlayer[];
|
||||||
last_activity_at: string | null;
|
last_activity_at: string | null;
|
||||||
closed_at: string | null;
|
closed_at: string | null;
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Jobs\CloseInactiveRooms;
|
use App\Jobs\CloseInactiveRooms;
|
||||||
use App\Jobs\MarkInactivePlayers;
|
use App\Jobs\RemoveAbsentPlayers;
|
||||||
use Illuminate\Foundation\DevCommands;
|
use Illuminate\Foundation\DevCommands;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@ Schedule::job(new CloseInactiveRooms)
|
|||||||
->weeklyOn(1, '02:00')
|
->weeklyOn(1, '02:00')
|
||||||
->withoutOverlapping();
|
->withoutOverlapping();
|
||||||
|
|
||||||
Schedule::job(new MarkInactivePlayers)
|
Schedule::job(new RemoveAbsentPlayers)
|
||||||
->everyFiveMinutes()
|
->everyFiveMinutes()
|
||||||
->withoutOverlapping();
|
->withoutOverlapping();
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?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();
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Enums\RoomStateChange;
|
use App\Enums\RoomStateChange;
|
||||||
use App\Events\RoomStateChanged;
|
use App\Events\RoomStateChanged;
|
||||||
|
use App\Jobs\RemoveAbsentPlayers;
|
||||||
use App\Models\Room;
|
use App\Models\Room;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
@@ -37,7 +38,7 @@ test('a guest automatically joins the embedded room', function () {
|
|||||||
$player = $room->players()->sole();
|
$player = $room->players()->sole();
|
||||||
|
|
||||||
expect($player->user_id)->toBeNull()
|
expect($player->user_id)->toBeNull()
|
||||||
->and($player->is_moderator)->toBeFalse();
|
->and($player->is_moderator)->toBeTrue();
|
||||||
|
|
||||||
$response->assertSessionHas("room_players.{$room->id}", $player->id);
|
$response->assertSessionHas("room_players.{$room->id}", $player->id);
|
||||||
});
|
});
|
||||||
@@ -46,7 +47,6 @@ test('embedded guest access reuses the session player and applies a changed disp
|
|||||||
$room = createEmbeddedRoom();
|
$room = createEmbeddedRoom();
|
||||||
$player = $room->players()->create([
|
$player = $room->players()->create([
|
||||||
'name' => 'Old name',
|
'name' => 'Old name',
|
||||||
'inactive_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->withSession(["room_players.{$room->id}" => $player->id])
|
$this->withSession(["room_players.{$room->id}" => $player->id])
|
||||||
@@ -57,8 +57,7 @@ test('embedded guest access reuses the session player and applies a changed disp
|
|||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
expect($room->players()->count())->toBe(1)
|
expect($room->players()->count())->toBe(1)
|
||||||
->and($player->fresh()->name)->toBe('New name')
|
->and($player->fresh()->name)->toBe('New name');
|
||||||
->and($player->fresh()->inactive_at)->toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an authenticated user automatically joins the embedded room', function () {
|
test('an authenticated user automatically joins the embedded room', function () {
|
||||||
@@ -82,17 +81,70 @@ test('an authenticated user automatically joins the embedded room', function ()
|
|||||||
'room_id' => $room->id,
|
'room_id' => $room->id,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'name' => 'Nico',
|
'name' => 'Nico',
|
||||||
'is_moderator' => false,
|
'is_moderator' => true,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a persistent room owner regains moderation through the embedded room', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$room = createEmbeddedRoom([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
]);
|
||||||
|
$oldModerator = $room->players()->create([
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'name' => 'Nico',
|
||||||
|
'is_moderator' => true,
|
||||||
|
'last_seen_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->withSession(["room_players.{$room->id}" => $oldModerator->id])
|
||||||
|
->get(route('embedded.room', [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn (Assert $page) => $page
|
||||||
|
->where('currentPlayer.is_moderator', true));
|
||||||
|
|
||||||
|
expect($room->players()->where('user_id', $owner->id)->sole()->id)->not->toBe($oldModerator->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a persistent room owner does not take over another account session when becoming moderator', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$room = createEmbeddedRoom([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
]);
|
||||||
|
$otherPlayer = $room->players()->create([
|
||||||
|
'user_id' => $otherUser->id,
|
||||||
|
'name' => 'Anna',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->withSession(["room_players.{$room->id}" => $otherPlayer->id])
|
||||||
|
->get(route('embedded.room', [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn (Assert $page) => $page
|
||||||
|
->where('currentPlayer.is_moderator', true));
|
||||||
|
|
||||||
|
expect($otherPlayer->fresh()->is_moderator)->toBeFalse()
|
||||||
|
->and($room->players()->where('user_id', $owner->id)->sole()->is_moderator)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
test('embedded access reuses the user player and applies a changed display name', function () {
|
test('embedded access reuses the user player and applies a changed display name', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$room = createEmbeddedRoom();
|
$room = createEmbeddedRoom();
|
||||||
$player = $room->players()->create([
|
$player = $room->players()->create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'name' => 'Old name',
|
'name' => 'Old name',
|
||||||
'inactive_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@@ -103,8 +155,7 @@ test('embedded access reuses the user player and applies a changed display name'
|
|||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
expect($room->players()->count())->toBe(1)
|
expect($room->players()->count())->toBe(1)
|
||||||
->and($player->fresh()->name)->toBe('New name')
|
->and($player->fresh()->name)->toBe('New name');
|
||||||
->and($player->fresh()->inactive_at)->toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an embedded guest keeps the same player after logging in separately', function () {
|
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',
|
'name' => 'Active player',
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
$room->players()->create([
|
|
||||||
'name' => 'Inactive player',
|
|
||||||
'last_seen_at' => now()->subHour(),
|
|
||||||
'inactive_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->post(route('room.favorite', $room))
|
->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
|
->assertInertia(fn (Assert $page) => $page
|
||||||
->has('favoriteRooms', 1)
|
->has('favoriteRooms', 1)
|
||||||
->where('favoriteRooms.0.id', $room->id)
|
->where('favoriteRooms.0.id', $room->id)
|
||||||
->where('favoriteRooms.0.players_count', 2)
|
->where('favoriteRooms.0.players_count', 1));
|
||||||
->where('favoriteRooms.0.active_player_count', 1));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the room resource exposes favorite and owner state', function () {
|
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));
|
->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();
|
$room = createLifecycleRoom();
|
||||||
$player = $room->players()->create([
|
$player = $room->players()->create([
|
||||||
'name' => 'Returning guest',
|
'name' => 'Returning guest',
|
||||||
'last_seen_at' => now()->subHours(2),
|
'last_seen_at' => now()->subHours(2),
|
||||||
'inactive_at' => now()->subHour(),
|
|
||||||
]);
|
]);
|
||||||
$session = ["room_players.{$room->id}" => $player->id];
|
$session = ["room_players.{$room->id}" => $player->id];
|
||||||
|
|
||||||
|
$player->delete();
|
||||||
|
|
||||||
$this->withSession($session)
|
$this->withSession($session)
|
||||||
->get(route('room.show', $room))
|
->get(route('room.show', $room))
|
||||||
->assertRedirect(route('room.join', ['join_code' => $room->join_code]));
|
->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]))
|
->get(route('room.join', ['join_code' => $room->join_code]))
|
||||||
->assertInertia(fn (Assert $page) => $page
|
->assertInertia(fn (Assert $page) => $page
|
||||||
->component('rooms/Join')
|
->component('rooms/Join')
|
||||||
->where('defaultPlayerName', 'Returning guest'));
|
->where('defaultPlayerName', null));
|
||||||
|
|
||||||
$this->withSession($session)
|
$this->withSession($session)
|
||||||
->post(route('room.code'), [
|
->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));
|
->assertRedirect(route('room.show', $room));
|
||||||
|
|
||||||
expect($room->players()->count())->toBe(1)
|
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 () {
|
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);
|
$room = createRoomForPlayerKick($owner);
|
||||||
$moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
$moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
||||||
$participant = addPlayerToKickRoom($room, 'Anna');
|
$participant = addPlayerToKickRoom($room, 'Anna');
|
||||||
addPlayerToKickRoom($room, 'Inactive')->update(['inactive_at' => now()]);
|
|
||||||
|
|
||||||
$this->actingAs($owner)
|
$this->actingAs($owner)
|
||||||
->get(route('dashboard'))
|
->get(route('dashboard'))
|
||||||
@@ -77,8 +76,8 @@ test('the room owner can kick an active participant', function () {
|
|||||||
->delete(route('room.player.destroy', [$room, $participant]))
|
->delete(route('room.player.destroy', [$room, $participant]))
|
||||||
->assertRedirect();
|
->assertRedirect();
|
||||||
|
|
||||||
expect($participant->fresh()->inactive_at)->not->toBeNull()
|
$this->assertModelMissing($participant);
|
||||||
->and($room->fresh()->state_version)->toBe($previousVersion + 1);
|
expect($room->fresh()->state_version)->toBe($previousVersion + 1);
|
||||||
|
|
||||||
Event::assertDispatched(
|
Event::assertDispatched(
|
||||||
RoomStateChanged::class,
|
RoomStateChanged::class,
|
||||||
@@ -104,6 +103,36 @@ test('the room owner can kick an active participant', function () {
|
|||||||
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
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 () {
|
test('the room moderator can kick an active participant without shared management', function () {
|
||||||
$owner = User::factory()->create();
|
$owner = User::factory()->create();
|
||||||
$room = createRoomForPlayerKick($owner, ['owner_id' => null]);
|
$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]))
|
->delete(route('room.player.destroy', [$room, $target]))
|
||||||
->assertRedirect();
|
->assertRedirect();
|
||||||
|
|
||||||
expect($target->fresh()->inactive_at)->not->toBeNull();
|
$this->assertModelMissing($target);
|
||||||
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
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]))
|
->delete(route('room.player.destroy', [$room, $target]))
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
|
|
||||||
expect($target->fresh()->inactive_at)->toBeNull();
|
$this->assertModelExists($target);
|
||||||
Event::assertNotDispatched(RoomStateChanged::class);
|
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]))
|
->delete(route('room.player.destroy', [$room, $target]))
|
||||||
->assertRedirect();
|
->assertRedirect();
|
||||||
|
|
||||||
expect($target->fresh()->inactive_at)->not->toBeNull();
|
$this->assertModelMissing($target);
|
||||||
Event::assertDispatched(RoomStateChanged::class);
|
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]))
|
->delete(route('room.player.destroy', [$room, $participant]))
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
|
|
||||||
expect($participant->fresh()->inactive_at)->toBeNull();
|
$this->assertModelExists($participant);
|
||||||
Event::assertNotDispatched(RoomStateChanged::class);
|
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]))
|
$this->delete(route('room.player.destroy', [$room, $target]))
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
|
|
||||||
expect($target->fresh()->inactive_at)->toBeNull();
|
$this->assertModelExists($target);
|
||||||
Event::assertNotDispatched(RoomStateChanged::class);
|
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]))
|
->delete(route('room.player.destroy', [$room, $moderator]))
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
|
|
||||||
expect($moderator->fresh()->inactive_at)->toBeNull();
|
$this->assertModelExists($moderator);
|
||||||
Event::assertNotDispatched(RoomStateChanged::class);
|
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]))
|
->delete(route('room.player.destroy', [$room, $target]))
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
|
|
||||||
expect($target->fresh()->inactive_at)->toBeNull();
|
$this->assertModelExists($target);
|
||||||
Event::assertNotDispatched(RoomStateChanged::class);
|
Event::assertNotDispatched(RoomStateChanged::class);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user