Handle authenticated room instance takeover
CI/CD / test_and_check_app (push) Failing after 2m7s
CI/CD / Publish immutable image (push) Skipped

This commit is contained in:
2026-09-10 16:33:13 +02:00
parent 2ab95cebdf
commit 640a2970c3
18 changed files with 351 additions and 22 deletions
+74 -6
View File
@@ -1,7 +1,10 @@
<?php
use App\Enums\RoomStateChange;
use App\Events\RoomStateChanged;
use App\Models\Room;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Inertia\Testing\AssertableInertia as Assert;
function createEmbeddedRoom(array $attributes = []): Room
@@ -130,17 +133,81 @@ test('an embedded guest keeps the same player after logging in separately', func
});
test('embedded guest access shows a dedicated error when the display name is taken', function () {
$user = User::factory()->create([
'email' => 'nico@example.com',
]);
$room = createEmbeddedRoom();
$room->players()->create(['name' => 'Nico']);
$this->get(route('embedded.room', [
$room->players()->create([
'user_id' => $user->id,
'name' => 'Nico',
]);
$url = route('embedded.room', [
'join_code' => $room->join_code,
'player_name' => 'Nico',
]))
]);
$this->get($url)
->assertUnprocessable()
->assertInertia(fn (Assert $page) => $page
->component('embedded/Error')
->where('message', 'Dieser Anzeigename ist in dem Raum bereits vorhanden.'));
->where('message', 'Dieser Anzeigename ist in dem Raum bereits vorhanden.')
->where('showLoginAction', true))
->assertSessionHas('url.intended', $url);
$this->post(route('login.store'), [
'email' => 'nico@example.com',
'password' => 'password',
])->assertRedirect($url);
$this->get($url)
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('embedded/Room')
->where('currentPlayer.name', 'Nico'));
});
test('a new account instance takes over the embedded player and kicks the previous instance', function () {
Event::fake([RoomStateChanged::class]);
$user = User::factory()->create();
$room = createEmbeddedRoom();
$player = $room->players()->create([
'user_id' => $user->id,
'name' => 'Nico',
'active_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c16',
]);
$url = route('embedded.room', [
'join_code' => $room->join_code,
'player_name' => 'Nico',
]);
$this->actingAs($user)
->withSession([
'room_player_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c17',
])
->get($url)
->assertOk()
->assertInertia(fn (Assert $page) => $page
->where('currentPlayer.id', $player->id)
->where('currentPlayer.instance_id', '019b8f80-37c4-71a3-b415-5f2697332c17'));
expect($player->fresh()->active_instance_id)
->toBe('019b8f80-37c4-71a3-b415-5f2697332c17');
Event::assertDispatched(
RoomStateChanged::class,
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerInstanceReplaced
&& $event->affectedPlayerId === $player->id
&& $event->affectedInstanceId === '019b8f80-37c4-71a3-b415-5f2697332c16',
);
$this->withSession([
"room_players.{$room->id}" => $player->id,
'room_player_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c16',
])
->post(route('room.vote.store', $room), ['value' => '5'])
->assertRedirect(route('room.join', ['join_code' => $room->join_code]));
$this->assertDatabaseCount('votes', 0);
});
test('embedded access shows a dedicated error for an unknown room code', function () {
@@ -151,7 +218,8 @@ test('embedded access shows a dedicated error for an unknown room code', functio
->assertUnprocessable()
->assertInertia(fn (Assert $page) => $page
->component('embedded/Error')
->where('message', 'Dieser Raumcode existiert nicht.'));
->where('message', 'Dieser Raumcode existiert nicht.')
->where('showLoginAction', false));
});
test('embedded logout returns to the same room as a guest without duplicating the player', function () {
+19
View File
@@ -45,6 +45,25 @@ test('room state broadcasts synchronously expose sanitized state', function () {
]);
});
test('player instance replacement broadcasts the displaced instance', function () {
$event = new RoomStateChanged(
'019b8f80-37c4-71a3-b415-5f2697332c16',
8,
RoomStateChange::PlayerInstanceReplaced,
null,
42,
'019b8f80-37c4-71a3-b415-5f2697332c17',
);
expect($event->broadcastWith())->toBe([
'version' => 8,
'change' => 'player_instance_replaced',
'state' => null,
'affected_player_id' => 42,
'affected_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c17',
]);
});
test('joining a room broadcasts the new room version', function () {
Event::fake([RoomStateChanged::class]);
$room = createBroadcastRoom();
+33
View File
@@ -1,11 +1,14 @@
<?php
use App\Enums\RoomStateChange;
use App\Events\RoomStateChanged;
use App\Jobs\CloseInactiveRooms;
use App\Models\Deck;
use App\Models\Room;
use App\Models\User;
use App\Models\Vote;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
use Inertia\Testing\AssertableInertia as Assert;
function createLifecycleRoom(array $attributes = []): Room
@@ -156,6 +159,36 @@ test('an authenticated player rejoins with the existing room identity', function
$response->assertSessionHas("room_players.{$room->id}", $player->id);
});
test('a new account instance takes over the player during a regular room join', function () {
$user = User::factory()->create();
$room = createLifecycleRoom();
$player = $room->players()->create([
'user_id' => $user->id,
'name' => 'Room nickname',
'active_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c16',
]);
Event::fake([RoomStateChanged::class]);
$this->actingAs($user)
->withSession([
'room_player_instance_id' => '019b8f80-37c4-71a3-b415-5f2697332c17',
])
->post(route('room.code'), [
'join_code' => $room->join_code,
'player_name' => 'Room nickname',
])
->assertRedirect(route('room.show', $room));
expect($player->fresh()->active_instance_id)
->toBe('019b8f80-37c4-71a3-b415-5f2697332c17');
Event::assertDispatched(
RoomStateChanged::class,
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerInstanceReplaced
&& $event->affectedPlayerId === $player->id,
);
});
test('an authenticated player can restore room access without the previous session', function () {
$user = User::factory()->create();
$room = createLifecycleRoom(['owner_id' => $user->id]);