mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 14:52:09 +00:00
Add room participant moderation
This commit is contained in:
@@ -6,6 +6,7 @@ enum RoomStateChange: string
|
||||
{
|
||||
case PlayerJoined = 'player_joined';
|
||||
case PlayerUpdated = 'player_updated';
|
||||
case PlayerKicked = 'player_kicked';
|
||||
case VoteChanged = 'vote_changed';
|
||||
case CardsRevealed = 'cards_revealed';
|
||||
case VotesReset = 'votes_reset';
|
||||
|
||||
@@ -21,6 +21,7 @@ class RoomStateChanged implements ShouldBroadcastNow
|
||||
public readonly int $version,
|
||||
public readonly RoomStateChange $change,
|
||||
public readonly ?array $state = null,
|
||||
public readonly ?int $affectedPlayerId = null,
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): Channel
|
||||
@@ -34,14 +35,20 @@ class RoomStateChanged implements ShouldBroadcastNow
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{version: int, change: string, state: array<string, mixed>|null}
|
||||
* @return array{version: int, change: string, state: array<string, mixed>|null, affected_player_id?: int}
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
$payload = [
|
||||
'version' => $this->version,
|
||||
'change' => $this->change->value,
|
||||
'state' => $this->state,
|
||||
];
|
||||
|
||||
if ($this->affectedPlayerId !== null) {
|
||||
$payload['affected_player_id'] = $this->affectedPlayerId;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\KickRoomPlayerRequest;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use App\Services\Rooms\RoomParticipationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class RoomPlayerKickController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomParticipationService $roomParticipation,
|
||||
) {}
|
||||
|
||||
public function __invoke(KickRoomPlayerRequest $request, Room $room, Player $player): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless($user instanceof User, 403);
|
||||
|
||||
$this->roomParticipation->kick($room, $player, $user);
|
||||
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'success',
|
||||
'message' => "{$player->name} wurde aus dem Raum entfernt.",
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class KickRoomPlayerRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$room = $this->route('room');
|
||||
$player = $this->route('player');
|
||||
|
||||
return $room instanceof Room
|
||||
&& $player instanceof Player
|
||||
&& $this->user()?->id === $room->owner_id
|
||||
&& $player->room_id === $room->id
|
||||
&& $player->user_id !== $room->owner_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,17 @@ class DashboardRoomResource extends JsonResource
|
||||
'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
|
||||
->map(fn ($player): array => [
|
||||
'id' => $player->id,
|
||||
'name' => $player->name,
|
||||
'is_moderator' => $player->is_moderator,
|
||||
'is_owner' => $player->user_id === $this->resource->owner_id,
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
: [],
|
||||
'last_activity_at' => $this->resource->last_activity_at?->toIso8601String(),
|
||||
'closed_at' => $this->resource->closed_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
@@ -15,6 +15,12 @@ class DashboardRoomService
|
||||
{
|
||||
$ownedRooms = $user
|
||||
->ownedRooms()
|
||||
->with([
|
||||
'activePlayers' => 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')
|
||||
|
||||
@@ -179,4 +179,47 @@ class RoomParticipationService
|
||||
|
||||
return $player;
|
||||
}
|
||||
|
||||
public function kick(Room $room, Player $player, User $user): void
|
||||
{
|
||||
$stateVersion = DB::transaction(function () use ($room, $player, $user): ?int {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
abort_unless($lockedRoom->owner_id === $user->id, 403);
|
||||
|
||||
$lockedPlayer = $lockedRoom->players()
|
||||
->lockForUpdate()
|
||||
->findOrFail($player->id);
|
||||
|
||||
abort_if($lockedPlayer->user_id === $lockedRoom->owner_id, 403);
|
||||
|
||||
if ($lockedPlayer->inactive_at !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lockedPlayer->update([
|
||||
'inactive_at' => now(),
|
||||
]);
|
||||
$lockedRoom->update([
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
|
||||
return $lockedRoom->state_version;
|
||||
});
|
||||
|
||||
if ($stateVersion === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RoomStateChanged::dispatch(
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::PlayerKicked,
|
||||
$this->realtimeState->for($room),
|
||||
$player->id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, useForm } from '@inertiajs/vue3';
|
||||
import { DoorOpen, Plus, Settings as SettingsIcon, Trash2 } from '@lucide/vue';
|
||||
import {
|
||||
DoorOpen,
|
||||
Plus,
|
||||
Settings as SettingsIcon,
|
||||
Trash2,
|
||||
UserMinus,
|
||||
} from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import DashboardRoomCard from '@/components/dashboard/DashboardRoomCard.vue';
|
||||
@@ -13,13 +19,23 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { create, destroy, reopen, show } from '@/routes/room';
|
||||
import { destroy as kickRoomPlayer } from '@/routes/room/player';
|
||||
import { edit as roomSettings } from '@/routes/room/settings';
|
||||
import type { DashboardRoom, OwnedRoomListProps } from '@/types/dashboard';
|
||||
import type {
|
||||
DashboardRoom,
|
||||
DashboardRoomPlayer,
|
||||
OwnedRoomListProps,
|
||||
} from '@/types/dashboard';
|
||||
|
||||
defineProps<OwnedRoomListProps>();
|
||||
|
||||
const selectedRoom = ref<DashboardRoom | null>(null);
|
||||
const selectedPlayer = ref<{
|
||||
room: DashboardRoom;
|
||||
player: DashboardRoomPlayer;
|
||||
} | null>(null);
|
||||
const deleteForm = useForm({});
|
||||
const kickForm = useForm({});
|
||||
|
||||
function setDeleteDialogOpen(open: boolean): void {
|
||||
if (!open && !deleteForm.processing) {
|
||||
@@ -27,6 +43,12 @@ function setDeleteDialogOpen(open: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
function setKickDialogOpen(open: boolean): void {
|
||||
if (!open && !kickForm.processing) {
|
||||
selectedPlayer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRoom(): void {
|
||||
if (selectedRoom.value === null || deleteForm.processing) {
|
||||
return;
|
||||
@@ -39,6 +61,25 @@ function deleteRoom(): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function kickPlayer(): void {
|
||||
if (selectedPlayer.value === null || kickForm.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
kickForm.delete(
|
||||
kickRoomPlayer({
|
||||
room: selectedPlayer.value.room.id,
|
||||
player: selectedPlayer.value.player.id,
|
||||
}).url,
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedPlayer.value = null;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,6 +149,56 @@ function deleteRoom(): void {
|
||||
<Trash2 class="size-4" />
|
||||
Raum löschen
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="mt-2 w-full rounded-md border border-slate-700 bg-slate-900/40 p-3"
|
||||
>
|
||||
<h3 class="text-sm font-semibold">Teilnehmer</h3>
|
||||
|
||||
<ul
|
||||
v-if="room.players.length > 0"
|
||||
class="mt-2 flex flex-col gap-2"
|
||||
>
|
||||
<li
|
||||
v-for="player in room.players"
|
||||
:key="player.id"
|
||||
class="flex items-center justify-between gap-3 rounded-md bg-slate-800 px-3 py-2"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span
|
||||
class="block truncate text-sm"
|
||||
:class="
|
||||
player.is_moderator
|
||||
? 'text-amber-300'
|
||||
: 'text-slate-100'
|
||||
"
|
||||
>
|
||||
{{ player.name }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="player.is_owner"
|
||||
class="shrink-0 text-xs text-slate-400"
|
||||
>
|
||||
Du
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-semibold text-rose-300 transition hover:bg-rose-500/20 hover:text-rose-200"
|
||||
@click="selectedPlayer = { room, player }"
|
||||
>
|
||||
<UserMinus class="size-3.5" />
|
||||
Kicken
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="mt-2 text-sm text-slate-400">
|
||||
Aktuell sind keine Teilnehmer im Raum.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</DashboardRoomCard>
|
||||
</div>
|
||||
@@ -173,5 +264,42 @@ function deleteRoom(): void {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
:open="selectedPlayer !== null"
|
||||
@update:open="setKickDialogOpen"
|
||||
>
|
||||
<DialogContent
|
||||
class="border-slate-700 bg-slate-800 text-slate-100 sm:max-w-md"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Teilnehmer wirklich kicken?</DialogTitle>
|
||||
<DialogDescription class="text-slate-300">
|
||||
{{ selectedPlayer?.player.name }} wird aus „{{
|
||||
selectedPlayer?.room.title ?? 'Scrum Poker'
|
||||
}}“ entfernt und zur Startseite weitergeleitet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter class="gap-6 space-x-4 sm:gap-0">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-slate-600 px-4 py-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-700"
|
||||
:disabled="kickForm.processing"
|
||||
@click="selectedPlayer = null"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-rose-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-rose-500 disabled:opacity-50"
|
||||
:disabled="kickForm.processing"
|
||||
@click="kickPlayer"
|
||||
>
|
||||
{{ kickForm.processing ? 'Wird entfernt …' : 'Kicken' }}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleUserRound, Crown } from '@lucide/vue';
|
||||
import { CircleUserRound } from '@lucide/vue';
|
||||
|
||||
import type { RoomSeatProps } from '@/types/room';
|
||||
|
||||
@@ -75,22 +75,18 @@ function playerVote(): string | null {
|
||||
:class="{ 'player-details-above': player.isTopSeat }"
|
||||
>
|
||||
<p
|
||||
class="player-name max-w-32 text-center text-sm leading-tight font-semibold text-slate-100"
|
||||
class="player-name max-w-32 text-center text-sm leading-tight font-semibold"
|
||||
:class="
|
||||
player.is_moderator ? 'text-amber-300' : 'text-slate-100'
|
||||
"
|
||||
>
|
||||
{{ player.name }}
|
||||
</p>
|
||||
<div
|
||||
v-if="player.is_moderator || isCurrentPlayer()"
|
||||
v-if="isCurrentPlayer()"
|
||||
class="flex flex-wrap items-center justify-center gap-x-2 gap-y-0.5 text-[11px]"
|
||||
>
|
||||
<span
|
||||
v-if="player.is_moderator"
|
||||
class="inline-flex items-center gap-1 text-amber-300"
|
||||
>
|
||||
<Crown class="size-3 fill-amber-300" aria-hidden="true" />
|
||||
Moderator
|
||||
</span>
|
||||
<span v-if="isCurrentPlayer()" class="text-cyan-300">Du</span>
|
||||
<span class="text-cyan-300">Du</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ export function useRoomRealtime(
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.change === 'player_kicked' &&
|
||||
payload.affected_player_id === currentPlayer.value.id
|
||||
) {
|
||||
router.visit(home().url, { replace: true });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.change === 'room_closed') {
|
||||
router.visit(
|
||||
join({ query: { join_code: room.value.join_code } }).url,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export type DashboardRoomPlayer = {
|
||||
id: number;
|
||||
name: string;
|
||||
is_moderator: boolean;
|
||||
is_owner: boolean;
|
||||
};
|
||||
|
||||
export type DashboardRoom = {
|
||||
id: string;
|
||||
title: string | null;
|
||||
@@ -5,6 +12,7 @@ export type DashboardRoom = {
|
||||
current_round: number;
|
||||
players_count: number;
|
||||
active_player_count: number;
|
||||
players: DashboardRoomPlayer[];
|
||||
last_activity_at: string | null;
|
||||
closed_at: string | null;
|
||||
};
|
||||
|
||||
@@ -160,6 +160,7 @@ export type RoomVoteDeckEmits = {
|
||||
export type RoomStateChange =
|
||||
| 'player_joined'
|
||||
| 'player_updated'
|
||||
| 'player_kicked'
|
||||
| 'vote_changed'
|
||||
| 'cards_revealed'
|
||||
| 'votes_reset'
|
||||
@@ -173,6 +174,7 @@ export type RoomStateChangedPayload = {
|
||||
version: number;
|
||||
change: RoomStateChange;
|
||||
state: RoomRealtimeState | null;
|
||||
affected_player_id?: number;
|
||||
};
|
||||
|
||||
export type RoomRealtimeState = Pick<
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Controllers\RoomEmoteController;
|
||||
use App\Http\Controllers\RoomJoinController;
|
||||
use App\Http\Controllers\RoomLifecycleController;
|
||||
use App\Http\Controllers\RoomPlayerController;
|
||||
use App\Http\Controllers\RoomPlayerKickController;
|
||||
use App\Http\Controllers\RoomSettingsController;
|
||||
use App\Http\Controllers\RoundController;
|
||||
use App\Http\Controllers\VoteController;
|
||||
@@ -44,6 +45,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
->name('room.destroy');
|
||||
Route::post('/rooms/{room}/favorite', FavoriteRoomController::class)
|
||||
->name('room.favorite');
|
||||
Route::delete('/rooms/{room}/players/{player}', RoomPlayerKickController::class)
|
||||
->name('room.player.destroy');
|
||||
});
|
||||
|
||||
Route::middleware('room.player')->group(function () {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\RoomStateChange;
|
||||
use App\Events\RoomStateChanged;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function createRoomForPlayerKick(User $owner, array $attributes = []): Room
|
||||
{
|
||||
return Room::create([
|
||||
'owner_id' => $owner->id,
|
||||
'title' => 'Team Planning',
|
||||
'current_round' => 1,
|
||||
'deck_config' => [
|
||||
'name' => 'Fibonacci',
|
||||
'cards' => [
|
||||
['key' => '3', 'label' => '3'],
|
||||
['key' => '5', 'label' => '5'],
|
||||
],
|
||||
],
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
function addPlayerToKickRoom(
|
||||
Room $room,
|
||||
string $name,
|
||||
?User $user = null,
|
||||
bool $isModerator = false,
|
||||
): Player {
|
||||
return $room->players()->create([
|
||||
'user_id' => $user?->id,
|
||||
'name' => $name,
|
||||
'is_moderator' => $isModerator,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
test('the room owner sees active room players on the dashboard', function () {
|
||||
$owner = User::factory()->create();
|
||||
$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'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->has('rooms.0.players', 2)
|
||||
->where('rooms.0.players.0.id', $moderator->id)
|
||||
->where('rooms.0.players.0.is_moderator', true)
|
||||
->where('rooms.0.players.0.is_owner', true)
|
||||
->where('rooms.0.players.1.id', $participant->id)
|
||||
->where('rooms.0.players.1.is_moderator', false)
|
||||
->where('rooms.0.players.1.is_owner', false));
|
||||
});
|
||||
|
||||
test('the room owner can kick an active participant', function () {
|
||||
$owner = User::factory()->create();
|
||||
$room = createRoomForPlayerKick($owner);
|
||||
addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
||||
$participant = addPlayerToKickRoom($room, 'Anna');
|
||||
$remainingPlayer = addPlayerToKickRoom($room, 'Ben');
|
||||
$remainingPlayer->votes()->create([
|
||||
'room_id' => $room->id,
|
||||
'round' => $room->current_round,
|
||||
'value' => '5',
|
||||
]);
|
||||
$previousVersion = $room->fresh()->state_version;
|
||||
|
||||
Event::fake([RoomStateChanged::class]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('room.player.destroy', [$room, $participant]))
|
||||
->assertRedirect();
|
||||
|
||||
expect($participant->fresh()->inactive_at)->not->toBeNull()
|
||||
->and($room->fresh()->state_version)->toBe($previousVersion + 1);
|
||||
|
||||
Event::assertDispatched(
|
||||
RoomStateChanged::class,
|
||||
function (RoomStateChanged $event) use ($participant, $remainingPlayer, $room): bool {
|
||||
if (
|
||||
$event->roomId !== $room->id
|
||||
|| $event->change !== RoomStateChange::PlayerKicked
|
||||
|| $event->affectedPlayerId !== $participant->id
|
||||
|| $event->state === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$remainingPlayerState = collect($event->state['players'])
|
||||
->firstWhere('id', $remainingPlayer->id);
|
||||
|
||||
return collect($event->state['players'])->doesntContain('id', $participant->id)
|
||||
&& $remainingPlayerState['has_voted'] === true
|
||||
&& $remainingPlayerState['vote'] === null
|
||||
&& $event->broadcastWith()['affected_player_id'] === $participant->id;
|
||||
},
|
||||
);
|
||||
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
||||
});
|
||||
|
||||
test('a participant cannot kick another player', function () {
|
||||
$owner = User::factory()->create();
|
||||
$participantUser = User::factory()->create();
|
||||
$room = createRoomForPlayerKick($owner);
|
||||
addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
||||
addPlayerToKickRoom($room, 'Anna', $participantUser);
|
||||
$target = addPlayerToKickRoom($room, 'Ben');
|
||||
|
||||
Event::fake([RoomStateChanged::class]);
|
||||
|
||||
$this->actingAs($participantUser)
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($target->fresh()->inactive_at)->toBeNull();
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
test('the room owner cannot kick their own player', function () {
|
||||
$owner = User::factory()->create();
|
||||
$room = createRoomForPlayerKick($owner);
|
||||
$moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true);
|
||||
|
||||
Event::fake([RoomStateChanged::class]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('room.player.destroy', [$room, $moderator]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($moderator->fresh()->inactive_at)->toBeNull();
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
|
||||
test('the room owner cannot kick a player from another room', function () {
|
||||
$owner = User::factory()->create();
|
||||
$otherOwner = User::factory()->create();
|
||||
$room = createRoomForPlayerKick($owner);
|
||||
$otherRoom = createRoomForPlayerKick($otherOwner, ['title' => 'Other room']);
|
||||
$target = addPlayerToKickRoom($otherRoom, 'Anna');
|
||||
|
||||
Event::fake([RoomStateChanged::class]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('room.player.destroy', [$room, $target]))
|
||||
->assertForbidden();
|
||||
|
||||
expect($target->fresh()->inactive_at)->toBeNull();
|
||||
Event::assertNotDispatched(RoomStateChanged::class);
|
||||
});
|
||||
Reference in New Issue
Block a user