mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 19:12:10 +00:00
feat: some fixes to the ui, imlemented user and guest user, ans used the dahboard
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
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 Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function createLifecycleRoom(array $attributes = []): Room
|
||||
{
|
||||
return Room::create([
|
||||
'title' => 'Sprint Planning',
|
||||
'deck_config' => [
|
||||
'name' => 'Fibonacci',
|
||||
'cards' => [
|
||||
['key' => '3', 'label' => '3'],
|
||||
['key' => '5', 'label' => '5'],
|
||||
],
|
||||
],
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
function createSystemDeck(): Deck
|
||||
{
|
||||
return Deck::query()->forceCreate([
|
||||
'name' => 'Fibonacci',
|
||||
'cards' => [
|
||||
['key' => '3', 'label' => '3'],
|
||||
['key' => '5', 'label' => '5'],
|
||||
],
|
||||
'is_system' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
test('an authenticated room creator owns the room and sees it on the dashboard', function () {
|
||||
$user = User::factory()->create();
|
||||
$deck = createSystemDeck();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('room.store'), [
|
||||
'title' => 'Team Alpha',
|
||||
'player_name' => 'Eric',
|
||||
'deck_id' => $deck->id,
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
$room = Room::query()->sole();
|
||||
|
||||
expect($room->owner_id)->toBe($user->id)
|
||||
->and($room->last_activity_at)->not->toBeNull();
|
||||
|
||||
$this->assertDatabaseHas('players', [
|
||||
'room_id' => $room->id,
|
||||
'user_id' => $user->id,
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('Dashboard')
|
||||
->has('rooms', 1)
|
||||
->where('rooms.0.id', $room->id)
|
||||
->where('rooms.0.title', 'Team Alpha'));
|
||||
});
|
||||
|
||||
test('a guest creator receives the one-time account notice', function () {
|
||||
$deck = createSystemDeck();
|
||||
|
||||
$this->post(route('room.store'), [
|
||||
'title' => 'Guest room',
|
||||
'player_name' => 'Guest',
|
||||
'deck_id' => $deck->id,
|
||||
])->assertRedirect();
|
||||
|
||||
$room = Room::query()->sole();
|
||||
|
||||
$this->get(route('room.show', $room))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('roomNotice.guest_retention_days', 14)
|
||||
->where('roomNotice.account_retention_days', 30));
|
||||
|
||||
$this->get(route('room.show', $room))
|
||||
->assertInertia(fn (Assert $page) => $page->where('roomNotice', null));
|
||||
});
|
||||
|
||||
test('guest rooms from the current browser are claimed after login', function () {
|
||||
$user = User::factory()->create([
|
||||
'email' => 'eric@example.com',
|
||||
]);
|
||||
$room = createLifecycleRoom();
|
||||
$moderator = $room->players()->create([
|
||||
'name' => 'Eric als Gast',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->post(route('login.store'), [
|
||||
'email' => 'eric@example.com',
|
||||
'password' => 'password',
|
||||
])
|
||||
->assertRedirect();
|
||||
|
||||
expect($room->fresh()->owner_id)->toBe($user->id)
|
||||
->and($moderator->fresh()->user_id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('joined rooms are not listed as owned rooms', function () {
|
||||
$user = User::factory()->create();
|
||||
$owner = User::factory()->create();
|
||||
$ownedRoom = createLifecycleRoom(['owner_id' => $user->id, 'title' => 'Owned']);
|
||||
$joinedRoom = createLifecycleRoom(['owner_id' => $owner->id, 'title' => 'Joined']);
|
||||
|
||||
$ownedRoom->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Owner',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
$joinedRoom->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Participant',
|
||||
'is_moderator' => false,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->has('rooms', 1)
|
||||
->where('rooms.0.id', $ownedRoom->id));
|
||||
});
|
||||
|
||||
test('an authenticated player rejoins with the existing room identity', function () {
|
||||
$user = User::factory()->create();
|
||||
$room = createLifecycleRoom();
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Room nickname',
|
||||
'is_moderator' => false,
|
||||
]);
|
||||
$room->update(['last_activity_at' => now()->subDay()]);
|
||||
$previousActivity = $room->last_activity_at;
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->post(route('room.code'), [
|
||||
'join_code' => $room->join_code,
|
||||
'player_name' => 'Ignored new name',
|
||||
])
|
||||
->assertRedirect(route('room.show', $room));
|
||||
|
||||
expect($room->players()->count())->toBe(1)
|
||||
->and($player->fresh()->name)->toBe('Room nickname')
|
||||
->and($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue();
|
||||
|
||||
$response->assertSessionHas("room_players.{$room->id}", $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]);
|
||||
$moderator = $room->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Eric',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('room.show', $room))
|
||||
->assertOk()
|
||||
->assertSessionHas("room_players.{$room->id}", $moderator->id);
|
||||
});
|
||||
|
||||
test('showing or polling a room does not count as room activity', function () {
|
||||
$room = createLifecycleRoom(['last_activity_at' => now()->subDays(5)]);
|
||||
$moderator = $room->players()->create([
|
||||
'name' => 'Eric',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
$activity = $room->last_activity_at;
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->get(route('room.show', $room))
|
||||
->assertOk();
|
||||
|
||||
expect($room->fresh()->last_activity_at->equalTo($activity))->toBeTrue();
|
||||
});
|
||||
|
||||
test('meaningful game actions update the explicit activity timestamp', function () {
|
||||
Carbon::setTestNow('2026-08-16 12:00:00');
|
||||
$room = createLifecycleRoom(['last_activity_at' => now()->subDay()]);
|
||||
$moderator = $room->players()->create([
|
||||
'name' => 'Eric',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->post(route('room.vote.store', $room), ['value' => '5'])
|
||||
->assertRedirect();
|
||||
|
||||
expect($room->fresh()->last_activity_at->equalTo(now()))->toBeTrue();
|
||||
});
|
||||
|
||||
test('the cleanup job closes expired guest and account rooms only once', function () {
|
||||
Carbon::setTestNow('2026-08-16 12:00:00');
|
||||
$owner = User::factory()->create();
|
||||
|
||||
$expiredGuest = createLifecycleRoom(['last_activity_at' => now()->subDays(14)]);
|
||||
$freshGuest = createLifecycleRoom(['last_activity_at' => now()->subDays(14)->addSecond()]);
|
||||
$expiredAccount = createLifecycleRoom([
|
||||
'owner_id' => $owner->id,
|
||||
'last_activity_at' => now()->subDays(30),
|
||||
]);
|
||||
$freshAccount = createLifecycleRoom([
|
||||
'owner_id' => $owner->id,
|
||||
'last_activity_at' => now()->subDays(30)->addSecond(),
|
||||
]);
|
||||
$alreadyClosed = createLifecycleRoom([
|
||||
'last_activity_at' => now()->subDays(40),
|
||||
'closed_at' => now()->subDay(),
|
||||
'state_version' => 7,
|
||||
]);
|
||||
|
||||
$job = new CloseInactiveRooms;
|
||||
$job->handle();
|
||||
$job->handle();
|
||||
|
||||
expect($expiredGuest->fresh()->closed_at)->not->toBeNull()
|
||||
->and($expiredGuest->fresh()->state_version)->toBe(2)
|
||||
->and($expiredAccount->fresh()->closed_at)->not->toBeNull()
|
||||
->and($freshGuest->fresh()->closed_at)->toBeNull()
|
||||
->and($freshAccount->fresh()->closed_at)->toBeNull()
|
||||
->and($alreadyClosed->fresh()->state_version)->toBe(7);
|
||||
});
|
||||
|
||||
test('closed rooms reject joins and participant actions', function () {
|
||||
$room = createLifecycleRoom(['closed_at' => now()]);
|
||||
$moderator = $room->players()->create([
|
||||
'name' => 'Eric',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->post(route('room.code'), [
|
||||
'join_code' => $room->join_code,
|
||||
'player_name' => 'Anna',
|
||||
])->assertSessionHasErrors('join_code');
|
||||
|
||||
$this->get(route('room.join', ['join_code' => $room->join_code]))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('rooms/Join')
|
||||
->where('closedRoom.join_code', $room->join_code));
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->post(route('room.vote.store', $room), ['value' => '5'])
|
||||
->assertRedirect(route('room.join', ['join_code' => $room->join_code]));
|
||||
|
||||
expect(Vote::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('only the owner can reopen a room into a clean new round', function () {
|
||||
$owner = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$room = createLifecycleRoom([
|
||||
'owner_id' => $owner->id,
|
||||
'current_round' => 3,
|
||||
'revealed_at' => now()->subDay(),
|
||||
'closed_at' => now(),
|
||||
'last_activity_at' => now()->subDays(2),
|
||||
]);
|
||||
$moderator = $room->players()->create([
|
||||
'user_id' => $owner->id,
|
||||
'name' => 'Owner',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
$vote = $moderator->votes()->create([
|
||||
'room_id' => $room->id,
|
||||
'round' => 3,
|
||||
'value' => '5',
|
||||
]);
|
||||
$joinCode = $room->join_code;
|
||||
|
||||
$this->actingAs($otherUser)
|
||||
->post(route('room.reopen', $room))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('room.reopen', $room))
|
||||
->assertRedirect(route('room.show', $room));
|
||||
|
||||
$room->refresh();
|
||||
|
||||
expect($room->current_round)->toBe(4)
|
||||
->and($room->revealed_at)->toBeNull()
|
||||
->and($room->closed_at)->toBeNull()
|
||||
->and($room->last_activity_at->isToday())->toBeTrue()
|
||||
->and($room->join_code)->toBe($joinCode)
|
||||
->and($vote->fresh())->not->toBeNull()
|
||||
->and($room->votes()->where('round', 4)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('only the owner can delete a room', function () {
|
||||
$owner = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$room = createLifecycleRoom(['owner_id' => $owner->id]);
|
||||
$room->players()->create([
|
||||
'user_id' => $owner->id,
|
||||
'name' => 'Owner',
|
||||
'is_moderator' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($otherUser)
|
||||
->delete(route('room.destroy', $room))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->delete(route('room.destroy', $room))
|
||||
->assertRedirect(route('dashboard'));
|
||||
|
||||
$this->assertSoftDeleted('rooms', ['id' => $room->id]);
|
||||
|
||||
$this->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page->has('rooms', 0));
|
||||
});
|
||||
|
||||
test('participants are redirected when an open room is deleted', function () {
|
||||
$room = createLifecycleRoom();
|
||||
$player = $room->players()->create([
|
||||
'name' => 'Participant',
|
||||
'is_moderator' => false,
|
||||
]);
|
||||
$sessionKey = "room_players.{$room->id}";
|
||||
|
||||
$room->delete();
|
||||
|
||||
$this->withSession([$sessionKey => $player->id])
|
||||
->get(route('room.show', $room))
|
||||
->assertRedirect(route('home'))
|
||||
->assertSessionMissing($sessionKey);
|
||||
});
|
||||
@@ -33,6 +33,8 @@ function createVotingRoom(): array
|
||||
|
||||
test('a participant can vote and change their vote in the current round', function () {
|
||||
[$room, $moderator] = createVotingRoom();
|
||||
$room->update(['last_activity_at' => now()->subDay()]);
|
||||
$previousActivity = $room->last_activity_at;
|
||||
|
||||
$session = ["room_players.{$room->id}" => $moderator->id];
|
||||
|
||||
@@ -52,7 +54,8 @@ test('a participant can vote and change their vote in the current round', functi
|
||||
->assertRedirect();
|
||||
|
||||
expect(Vote::query()->count())->toBe(1);
|
||||
expect(Vote::query()->value('value'))->toBe('8');
|
||||
expect(Vote::query()->value('value'))->toBe('8')
|
||||
->and($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a participant cannot vote with a card outside the room deck', function () {
|
||||
@@ -67,6 +70,8 @@ test('a participant cannot vote with a card outside the room deck', function ()
|
||||
|
||||
test('other votes stay hidden until the moderator reveals the round', function () {
|
||||
[$room, $moderator, $player] = createVotingRoom();
|
||||
$room->update(['last_activity_at' => now()->subDay()]);
|
||||
$previousActivity = $room->last_activity_at;
|
||||
|
||||
$player->votes()->create([
|
||||
'room_id' => $room->id,
|
||||
@@ -88,6 +93,8 @@ test('other votes stay hidden until the moderator reveals the round', function (
|
||||
->post(route('room.reveal', $room))
|
||||
->assertRedirect();
|
||||
|
||||
expect($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue();
|
||||
|
||||
$this->withSession($session)
|
||||
->get(route('room.show', $room))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
@@ -108,7 +115,11 @@ test('only the moderator may reveal cards', function () {
|
||||
test('starting the next round clears the revealed state', function () {
|
||||
[$room, $moderator] = createVotingRoom();
|
||||
|
||||
$room->update(['revealed_at' => now()]);
|
||||
$room->update([
|
||||
'revealed_at' => now(),
|
||||
'last_activity_at' => now()->subDay(),
|
||||
]);
|
||||
$previousActivity = $room->last_activity_at;
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->post(route('room.next-round', $room))
|
||||
@@ -117,7 +128,8 @@ test('starting the next round clears the revealed state', function () {
|
||||
$room->refresh();
|
||||
|
||||
expect($room->current_round)->toBe(2)
|
||||
->and($room->revealed_at)->toBeNull();
|
||||
->and($room->revealed_at)->toBeNull()
|
||||
->and($room->last_activity_at->greaterThan($previousActivity))->toBeTrue();
|
||||
});
|
||||
|
||||
test('the moderator can reset only the votes from the current round', function () {
|
||||
@@ -126,7 +138,9 @@ test('the moderator can reset only the votes from the current round', function (
|
||||
$room->update([
|
||||
'current_round' => 2,
|
||||
'revealed_at' => now(),
|
||||
'last_activity_at' => now()->subDay(),
|
||||
]);
|
||||
$previousActivity = $room->last_activity_at;
|
||||
|
||||
$player->votes()->create([
|
||||
'room_id' => $room->id,
|
||||
@@ -156,5 +170,41 @@ test('the moderator can reset only the votes from the current round', function (
|
||||
'round' => 2,
|
||||
]);
|
||||
|
||||
expect($room->fresh()->revealed_at)->toBeNull();
|
||||
$room->refresh();
|
||||
|
||||
expect($room->revealed_at)->toBeNull()
|
||||
->and($room->last_activity_at->greaterThan($previousActivity))->toBeTrue();
|
||||
});
|
||||
|
||||
test('revealed rounds show the average and median of numeric votes', function () {
|
||||
[$room, $moderator, $player] = createVotingRoom();
|
||||
$thirdPlayer = $room->players()->create([
|
||||
'name' => 'Nina',
|
||||
'is_moderator' => false,
|
||||
]);
|
||||
$nonNumericPlayer = $room->players()->create([
|
||||
'name' => 'Coffee',
|
||||
'is_moderator' => false,
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
[$moderator, '3'],
|
||||
[$player, '5'],
|
||||
[$thirdPlayer, '8'],
|
||||
[$nonNumericPlayer, 'coffee'],
|
||||
] as [$votingPlayer, $value]) {
|
||||
$votingPlayer->votes()->create([
|
||||
'room_id' => $room->id,
|
||||
'round' => 1,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
$room->update(['revealed_at' => now()]);
|
||||
|
||||
$this->withSession(["room_players.{$room->id}" => $moderator->id])
|
||||
->get(route('room.show', $room))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('room.statistics.average', '5,33')
|
||||
->where('room.statistics.median', '5,00'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user