diff --git a/app/Http/Controllers/RoomController.php b/app/Http/Controllers/RoomController.php index 34f1603..aa28c24 100644 --- a/app/Http/Controllers/RoomController.php +++ b/app/Http/Controllers/RoomController.php @@ -1,11 +1,150 @@ user(); + + $decks = Deck::query() + ->where(function ($query) use ($user) { + $query->where('is_system', true); + + if ($user !== null) { + $query->orWhere('user_id', $user->id); + } + }) + ->orderBy('name') + ->get([ + 'id', + 'name', + 'cards', + ]); + + return Inertia::render('rooms/Create', [ + 'decks' => $decks, + 'defaultPlayerName' => $user?->name, + ]); + } + + public function store(Request $request, StoreRoomRequest $roomStoreRequest) + { + $validated = $roomStoreRequest->validated(); + $user = $request->user(); + + $deck = Deck::query() + ->whereKey($validated['deck_id']) + ->where(function ($query) use ($user) { + $query->where('is_system', true); + + if ($user !== null) { + $query->orWhere('user_id', $user->id); + } + }) + ->firstOrFail(); + + [$room, $player] = DB::transaction(function () use ( + $validated, + $deck, + $user, + ) { + $room = Room::create([ + 'title' => $validated['title'] ?? null, + 'deck_config' => [ + 'id' => $deck->id, + 'name' => $deck->name, + 'cards' => $deck->cards, + ], + ]); + + $player = $room->players()->create([ + 'user_id' => $user?->id, + 'name' => $validated['player_name'], + 'is_moderator' => true, + 'last_seen_at' => now(), + ]); + + return [$room, $player]; + }); + + $request->session()->put( + "room_players.{$room->id}", + $player->id, + ); + + return to_route('room.show', $room); + } + + public function show(Room $room, Request $request) + { + $currentPlayer = $request->attributes->get('currentPlayer'); + + abort_unless($currentPlayer instanceof Player, 403); + + $room->load([ + 'players' => fn ($query) => $query + ->select(['id', 'room_id', 'name', 'is_moderator', 'created_at']) + ->orderByDesc('is_moderator') + ->orderBy('created_at'), + 'players.votes' => fn ($query) => $query + ->where('room_id', $room->id) + ->where('round', $room->current_round), + ]); + + $players = $room->players->map(function (Player $player) use ($room) { + $vote = $player->votes->first(); + + return [ + 'id' => $player->id, + 'name' => $player->name, + 'is_moderator' => $player->is_moderator, + 'has_voted' => $vote !== null, + 'vote' => $room->revealed_at !== null ? $vote?->value : null, + ]; + }); + + $currentVote = $room->players + ->firstWhere('id', $currentPlayer->id) + ?->votes + ->first(); + + return Inertia::render('rooms/Show', [ + 'room' => [ + 'id' => $room->id, + 'title' => $room->title, + 'join_code' => $room->join_code, + 'current_round' => $room->current_round, + 'revealed' => $room->revealed_at !== null, + 'state_version' => $room->state_version, + 'deck' => [ + 'name' => $room->deck_config['name'], + 'cards' => $room->deck_config['cards'], + ], + 'players' => $players, + ], + 'currentPlayer' => [ + 'id' => $currentPlayer->id, + 'name' => $currentPlayer->name, + 'is_moderator' => $currentPlayer->is_moderator, + 'current_vote' => $currentVote?->value, + ], + ]); + } + + public function join(Request $request) + { + return Inertia::render('rooms/Join', [ + 'join_code' => $request->query('join_code'), + ]); } } diff --git a/app/Http/Controllers/RoomPlayerController.php b/app/Http/Controllers/RoomPlayerController.php new file mode 100644 index 0000000..5861208 --- /dev/null +++ b/app/Http/Controllers/RoomPlayerController.php @@ -0,0 +1,32 @@ +validated(); + $user = $request->user(); + + $room = Room::where('join_code', '=', $validated['join_code'])->firstOrFail(); + + $player = $room->players()->create([ + 'user_id' => $user?->id, + 'name' => $validated['player_name'], + 'is_moderator' => false, + 'last_seen_at' => now(), + ]); + + $request->session()->put( + "room_players.{$room->id}", + $player->id, + ); + + return to_route('room.show', $room); + + } +} diff --git a/app/Http/Controllers/RoundController.php b/app/Http/Controllers/RoundController.php new file mode 100644 index 0000000..93d8280 --- /dev/null +++ b/app/Http/Controllers/RoundController.php @@ -0,0 +1,113 @@ +attributes->get('currentPlayer'); + + abort_unless( + $player instanceof Player + && $player->room_id === $room->id + && $player->is_moderator, + 403, + ); + + DB::transaction(function () use ($room) { + $lockedRoom = Room::query() + ->lockForUpdate() + ->findOrFail($room->id); + + Vote::query() + ->where('room_id', $lockedRoom->id) + ->where('round', $lockedRoom->current_round) + ->delete(); + + $lockedRoom->update([ + 'revealed_at' => null, + 'state_version' => $lockedRoom->state_version + 1, + ]); + }); + + return back(); + } + + public function reveal( + Request $request, + Room $room, + ): RedirectResponse { + $player = $request->attributes->get('currentPlayer'); + + abort_unless( + $player instanceof Player + && $player->room_id === $room->id + && $player->is_moderator, + 403, + ); + + DB::transaction(function () use ($room) { + $lockedRoom = Room::query() + ->lockForUpdate() + ->findOrFail($room->id); + + if ($lockedRoom->revealed_at !== null) { + throw ValidationException::withMessages([ + 'room' => 'Die Karten sind bereits aufgedeckt.', + ]); + } + + $lockedRoom->update([ + 'revealed_at' => now(), + 'state_version' => $lockedRoom->state_version + 1, + ]); + }); + + return back(); + } + + public function nextRound( + Request $request, + Room $room, + ): RedirectResponse { + $player = $request->attributes->get('currentPlayer'); + + abort_unless( + $player instanceof Player + && $player->room_id === $room->id + && $player->is_moderator, + 403, + ); + + DB::transaction(function () use ($room) { + $lockedRoom = Room::query() + ->lockForUpdate() + ->findOrFail($room->id); + + if ($lockedRoom->revealed_at === null) { + throw ValidationException::withMessages([ + 'room' => 'Die aktuelle Runde muss zuerst aufgedeckt werden.', + ]); + } + + $lockedRoom->update([ + 'current_round' => $lockedRoom->current_round + 1, + 'revealed_at' => null, + 'state_version' => $lockedRoom->state_version + 1, + ]); + }); + + return back(); + } +} diff --git a/app/Http/Controllers/VoteController.php b/app/Http/Controllers/VoteController.php index 6d117b8..f059ddb 100644 --- a/app/Http/Controllers/VoteController.php +++ b/app/Http/Controllers/VoteController.php @@ -2,9 +2,55 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; +use App\Http\Requests\SubmitVoteRequest; +use App\Models\Player; +use App\Models\Room; +use App\Models\Vote; +use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; class VoteController extends Controller { - // + public function store( + SubmitVoteRequest $request, + Room $room, + ): RedirectResponse { + $player = $request->attributes->get('currentPlayer'); + + abort_unless( + $player instanceof Player + && $player->room_id === $room->id, + 403, + ); + + $value = $request->validated('value'); + + DB::transaction(function () use ($room, $player, $value) { + $lockedRoom = Room::query() + ->lockForUpdate() + ->findOrFail($room->id); + + if ($lockedRoom->revealed_at !== null) { + throw ValidationException::withMessages([ + 'value' => 'Die Karten wurden bereits aufgedeckt.', + ]); + } + + Vote::updateOrCreate( + [ + 'room_id' => $lockedRoom->id, + 'player_id' => $player->id, + 'round' => $lockedRoom->current_round, + ], + [ + 'value' => $value, + ], + ); + + $lockedRoom->increment('state_version'); + }); + + return back(); + } } diff --git a/app/Http/Middleware/EnsureRoomParticipant.php b/app/Http/Middleware/EnsureRoomParticipant.php new file mode 100644 index 0000000..4c7c952 --- /dev/null +++ b/app/Http/Middleware/EnsureRoomParticipant.php @@ -0,0 +1,50 @@ +route('room'); + + if (! $room instanceof Room) { + abort(404); + } + + $sessionKey = "room_players.{$room->id}"; + $playerId = $request->session()->get($sessionKey); + + if (! $playerId) { + return to_route('room.join', [ + 'join_code' => $room->join_code, + ])->withErrors([ + 'join_code' => 'Du musst diesen Raum erst beitreten.', + ]); + } + + $player = $room->players()->find($playerId); + + if (! $player) { + $request->session()->forget($sessionKey); + + return to_route('room.join', [ + 'join_code' => $room->join_code, + ]); + } + + $request->attributes->set('currentPlayer', $player); + + return $next($request); + } +} diff --git a/app/Http/Requests/JoinRoomRequest.php b/app/Http/Requests/JoinRoomRequest.php new file mode 100644 index 0000000..4e5b317 --- /dev/null +++ b/app/Http/Requests/JoinRoomRequest.php @@ -0,0 +1,54 @@ +merge([ + 'join_code' => strtoupper(trim((string) $this->input('join_code'))), + 'player_name' => trim((string) $this->input('player_name')), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + $roomId = Room::query() + ->where('join_code', $this->input('join_code')) + ->value('id'); + + return [ + 'join_code' => ['bail', 'string', 'required', 'size:6', Rule::exists(Room::class, 'join_code')], + 'player_name' => ['required', 'string', 'max:50', Rule::unique(Player::class, 'name')->where('room_id', $roomId)], + ]; + } + + public function messages(): array + { + return [ + 'join_code.exists' => 'Dieser Raumcode existiert nicht.', + 'join_code.size' => 'Der Raumcode muss aus sechs Zeichen bestehen.', + 'player_name.unique' => 'Dieser Name wird in dem Raum bereits verwendet.', + ]; + } +} diff --git a/app/Http/Requests/StoreRoomRequest.php b/app/Http/Requests/StoreRoomRequest.php new file mode 100644 index 0000000..05e7cb7 --- /dev/null +++ b/app/Http/Requests/StoreRoomRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => ['nullable', 'string', 'max:100'], + 'player_name' => ['required', 'string', 'max:50'], + 'deck_id' => ['required', 'integer'], + ]; + } +} diff --git a/app/Http/Requests/SubmitVoteRequest.php b/app/Http/Requests/SubmitVoteRequest.php new file mode 100644 index 0000000..f1dba08 --- /dev/null +++ b/app/Http/Requests/SubmitVoteRequest.php @@ -0,0 +1,40 @@ +|string> + */ + public function rules(): array + { + $room = $this->route('room'); + + return [ + 'value' => ['bail', 'required', 'string', Rule::in(array_column($room->deck_config['cards'], 'key'))], + ]; + } + + public function messages(): array + { + return [ + 'value.required' => 'Du musst eine Karte auswählen.', + 'value.in' => 'Diese Karte gehört nicht zum verwendeten Deck.', + ]; + } +} diff --git a/app/Models/Deck.php b/app/Models/Deck.php index 965cceb..893fbc5 100644 --- a/app/Models/Deck.php +++ b/app/Models/Deck.php @@ -16,7 +16,7 @@ class Deck extends Model { return [ 'cards' => 'array', - 'is_system' => 'boolean' + 'is_system' => 'boolean', ]; } diff --git a/app/Models/Player.php b/app/Models/Player.php index d9039a7..9a0a7ff 100644 --- a/app/Models/Player.php +++ b/app/Models/Player.php @@ -5,12 +5,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use phpDocumentor\Reflection\Types\Boolean; class Player extends Model { protected $fillable = [ - 'room_id', + 'room_id', + 'user_id', 'name', 'is_moderator', 'last_seen_at', @@ -58,7 +58,7 @@ class Player extends Model return $this->is_moderator; } - public function markAsSeen(): void + public function markAsSeen(): void { $this->update([ 'last_seen_at' => now(), diff --git a/app/Models/Room.php b/app/Models/Room.php index c1a85c9..c8afc44 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -2,11 +2,9 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -use Override; class Room extends Model { @@ -20,7 +18,7 @@ class Room extends Model 'state_version', ]; - protected function casts() :array + protected function casts(): array { return [ 'current_round' => 'integer', @@ -37,15 +35,15 @@ class Room extends Model }); } - private static function generateJoinCode(): string + private static function generateJoinCode(): string { $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; do { $code = ''; - for($i = 0; $i < 6; $i++) { - $code .= $alphabet[random_int(0,strlen($alphabet)-1)]; + for ($i = 0; $i < 6; $i++) { + $code .= $alphabet[random_int(0, strlen($alphabet) - 1)]; } } while (static::where('join_code', $code)->exists()); @@ -53,14 +51,13 @@ class Room extends Model } - public function players(): HasMany { return $this->hasMany(Player::class); } - public function vote(): HasMany + public function votes(): HasMany { return $this->hasMany(Vote::class); } diff --git a/app/Models/Vote.php b/app/Models/Vote.php index ea9ff46..bb082e0 100644 --- a/app/Models/Vote.php +++ b/app/Models/Vote.php @@ -11,22 +11,23 @@ class Vote extends Model 'round', 'value', 'room_id', - 'player_id' + 'player_id', ]; - protected function casts(): array { return [ - 'round' => 'integer', + 'round' => 'integer', ]; } - public function room(): BelongsTo { + public function room(): BelongsTo + { return $this->belongsTo(Room::class); } - public function player(): BelongsTo { + public function player(): BelongsTo + { return $this->belongsTo(Player::class); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index ba5cf4c..7e9df36 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { $middleware->encryptCookies(except: ['appearance', 'sidebar_state']); + $middleware->alias([ + 'room.player' => EnsureRoomParticipant::class, + ]); $middleware->web(append: [ HandleAppearance::class, diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3a634af..99d8f1a 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -6,7 +6,6 @@ use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; - class DatabaseSeeder extends Seeder { use WithoutModelEvents; @@ -17,7 +16,7 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call([ - DeckSeeder::class, + DeckSeeder::class, ]); User::factory()->create([ diff --git a/database/seeders/DeckSeeder.php b/database/seeders/DeckSeeder.php index 2422f52..fc4cecd 100644 --- a/database/seeders/DeckSeeder.php +++ b/database/seeders/DeckSeeder.php @@ -58,7 +58,7 @@ class DeckSeeder extends Seeder ->whereNull('user_id') ->where('is_system', true) ->where('name', $definition['name']) - ->first() ?? new Deck(); + ->first() ?? new Deck; $deck->forceFill([ 'user_id' => null, @@ -68,4 +68,4 @@ class DeckSeeder extends Seeder ])->save(); } } -} \ No newline at end of file +} diff --git a/resources/js/layouts/PublicLayout.vue b/resources/js/layouts/PublicLayout.vue index 1e9b6fc..d202e62 100644 --- a/resources/js/layouts/PublicLayout.vue +++ b/resources/js/layouts/PublicLayout.vue @@ -38,7 +38,7 @@ -
+
diff --git a/resources/js/pages/Landingpage.vue b/resources/js/pages/Landingpage.vue new file mode 100644 index 0000000..2aa64d4 --- /dev/null +++ b/resources/js/pages/Landingpage.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/js/pages/Welcome.vue b/resources/js/pages/Welcome.vue deleted file mode 100644 index 638f743..0000000 --- a/resources/js/pages/Welcome.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/resources/js/pages/rooms/Create.vue b/resources/js/pages/rooms/Create.vue new file mode 100644 index 0000000..1b942c6 --- /dev/null +++ b/resources/js/pages/rooms/Create.vue @@ -0,0 +1,119 @@ + + + \ No newline at end of file diff --git a/resources/js/pages/rooms/CreateRoom.vue b/resources/js/pages/rooms/CreateRoom.vue deleted file mode 100644 index 88781d7..0000000 --- a/resources/js/pages/rooms/CreateRoom.vue +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/resources/js/pages/rooms/Join.vue b/resources/js/pages/rooms/Join.vue new file mode 100644 index 0000000..a420d5a --- /dev/null +++ b/resources/js/pages/rooms/Join.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/resources/js/pages/rooms/Show.vue b/resources/js/pages/rooms/Show.vue new file mode 100644 index 0000000..985671d --- /dev/null +++ b/resources/js/pages/rooms/Show.vue @@ -0,0 +1,663 @@ + + + + + diff --git a/routes/room.php b/routes/room.php index e0323dc..6f2cb87 100644 --- a/routes/room.php +++ b/routes/room.php @@ -1,6 +1,24 @@ name('room.create'); \ No newline at end of file +Route::get('/room', [RoomController::class, 'create'])->name('room.create'); +Route::get('/room/join', [RoomController::class, 'join'])->name('room.join'); +Route::post('room/join', [RoomPlayerController::class, 'join'])->name('room.code'); +Route::post('/room', [RoomController::class, 'store'])->name('room.store'); + +Route::middleware('room.player')->group(function () { + Route::get('/rooms/{room}', [RoomController::class, 'show'])->name('room.show'); + Route::post('/rooms/{room}/votes', [VoteController::class, 'store']) + ->name('room.vote.store'); + Route::post('/rooms/{room}/reveal', [RoundController::class, 'reveal']) + ->name('room.reveal'); + Route::post('/rooms/{room}/reset', [RoundController::class, 'reset']) + ->name('room.reset'); + Route::post('/rooms/{room}/next-round', [RoundController::class, 'nextRound']) + ->name('room.next-round'); +}); diff --git a/routes/web.php b/routes/web.php index a7eb7f0..d1320c2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,10 +1,11 @@ name('home'); Route::middleware(['auth', 'verified'])->group(function () { Route::inertia('dashboard', 'Dashboard')->name('dashboard'); }); - diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index c404ceb..d1b9ed2 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -74,4 +74,4 @@ test('users are rate limited', function () { ]); $response->assertTooManyRequests(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 6428476..81eacd8 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -98,4 +98,4 @@ test('already verified user visiting verification link is redirected without fir Event::assertNotDispatched(Verified::class); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php index a1279d7..8dfc9a7 100644 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -19,4 +19,4 @@ test('password confirmation requires authentication', function () { $response = $this->get(route('password.confirm')); $response->assertRedirect(route('login')); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index 5ce8fd7..a2b8010 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -75,4 +75,4 @@ test('password cannot be reset with invalid token', function () { ]); $response->assertSessionHasErrors('email'); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index abd8ab2..35ec7ca 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -22,4 +22,4 @@ test('new users can register', function () { $this->assertAuthenticated(); $response->assertRedirect(route('dashboard', absolute: false)); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php index 8d1f1da..560e715 100644 --- a/tests/Feature/Auth/TwoFactorChallengeTest.php +++ b/tests/Feature/Auth/TwoFactorChallengeTest.php @@ -32,4 +32,4 @@ test('two factor challenge can be rendered', function () { ->assertInertia(fn (Assert $page) => $page ->component('auth/TwoFactorChallenge'), ); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/VerificationNotificationTest.php b/tests/Feature/Auth/VerificationNotificationTest.php index c6648ee..2e7c33f 100644 --- a/tests/Feature/Auth/VerificationNotificationTest.php +++ b/tests/Feature/Auth/VerificationNotificationTest.php @@ -31,4 +31,4 @@ test('does not send verification notification if email is verified', function () ->assertRedirect(route('dashboard', absolute: false)); Notification::assertNothingSent(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php index d59d2d7..95c6a8b 100644 --- a/tests/Feature/DashboardTest.php +++ b/tests/Feature/DashboardTest.php @@ -13,4 +13,4 @@ test('authenticated users can visit the dashboard', function () { $response = $this->get(route('dashboard')); $response->assertOk(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index a576279..ef7b533 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -4,4 +4,4 @@ test('returns a successful response', function () { $response = $this->get(route('home')); $response->assertOk(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/RoomVotingTest.php b/tests/Feature/RoomVotingTest.php new file mode 100644 index 0000000..fa52f36 --- /dev/null +++ b/tests/Feature/RoomVotingTest.php @@ -0,0 +1,160 @@ + 'Sprint Planning', + 'deck_config' => [ + 'name' => 'Fibonacci', + 'cards' => [ + ['key' => '3', 'label' => '3'], + ['key' => '5', 'label' => '5'], + ['key' => '8', 'label' => '8'], + ], + ], + ]); + + $moderator = $room->players()->create([ + 'name' => 'Eric', + 'is_moderator' => true, + ]); + + $player = $room->players()->create([ + 'name' => 'Anna', + 'is_moderator' => false, + ]); + + return [$room, $moderator, $player]; +} + +test('a participant can vote and change their vote in the current round', function () { + [$room, $moderator] = createVotingRoom(); + + $session = ["room_players.{$room->id}" => $moderator->id]; + + $this->withSession($session) + ->post(route('room.vote.store', $room), ['value' => '5']) + ->assertRedirect(); + + $this->assertDatabaseHas('votes', [ + 'room_id' => $room->id, + 'player_id' => $moderator->id, + 'round' => 1, + 'value' => '5', + ]); + + $this->withSession($session) + ->post(route('room.vote.store', $room), ['value' => '8']) + ->assertRedirect(); + + expect(Vote::query()->count())->toBe(1); + expect(Vote::query()->value('value'))->toBe('8'); +}); + +test('a participant cannot vote with a card outside the room deck', function () { + [$room, $moderator] = createVotingRoom(); + + $this->withSession(["room_players.{$room->id}" => $moderator->id]) + ->post(route('room.vote.store', $room), ['value' => '100']) + ->assertSessionHasErrors('value'); + + $this->assertDatabaseCount('votes', 0); +}); + +test('other votes stay hidden until the moderator reveals the round', function () { + [$room, $moderator, $player] = createVotingRoom(); + + $player->votes()->create([ + 'room_id' => $room->id, + 'round' => 1, + 'value' => '8', + ]); + + $session = ["room_players.{$room->id}" => $moderator->id]; + + $this->withSession($session) + ->get(route('room.show', $room)) + ->assertInertia(fn (Assert $page) => $page + ->component('rooms/Show') + ->where('room.revealed', false) + ->where('room.players.1.has_voted', true) + ->where('room.players.1.vote', null)); + + $this->withSession($session) + ->post(route('room.reveal', $room)) + ->assertRedirect(); + + $this->withSession($session) + ->get(route('room.show', $room)) + ->assertInertia(fn (Assert $page) => $page + ->where('room.revealed', true) + ->where('room.players.1.vote', '8')); +}); + +test('only the moderator may reveal cards', function () { + [$room, , $player] = createVotingRoom(); + + $this->withSession(["room_players.{$room->id}" => $player->id]) + ->post(route('room.reveal', $room)) + ->assertForbidden(); + + expect($room->fresh()->revealed_at)->toBeNull(); +}); + +test('starting the next round clears the revealed state', function () { + [$room, $moderator] = createVotingRoom(); + + $room->update(['revealed_at' => now()]); + + $this->withSession(["room_players.{$room->id}" => $moderator->id]) + ->post(route('room.next-round', $room)) + ->assertRedirect(); + + $room->refresh(); + + expect($room->current_round)->toBe(2) + ->and($room->revealed_at)->toBeNull(); +}); + +test('the moderator can reset only the votes from the current round', function () { + [$room, $moderator, $player] = createVotingRoom(); + + $room->update([ + 'current_round' => 2, + 'revealed_at' => now(), + ]); + + $player->votes()->create([ + 'room_id' => $room->id, + 'round' => 1, + 'value' => '3', + ]); + + $player->votes()->create([ + 'room_id' => $room->id, + 'round' => 2, + 'value' => '8', + ]); + + $this->withSession(["room_players.{$room->id}" => $moderator->id]) + ->post(route('room.reset', $room)) + ->assertRedirect(); + + $this->assertDatabaseHas('votes', [ + 'room_id' => $room->id, + 'player_id' => $player->id, + 'round' => 1, + 'value' => '3', + ]); + + $this->assertDatabaseMissing('votes', [ + 'room_id' => $room->id, + 'round' => 2, + ]); + + expect($room->fresh()->revealed_at)->toBeNull(); +}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index b4c4c37..9f49e25 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -82,4 +82,4 @@ test('correct password must be provided to delete account', function () { ->assertRedirect(route('profile.edit')); expect($user->fresh())->not->toBeNull(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Settings/SecurityTest.php b/tests/Feature/Settings/SecurityTest.php index 408e3a6..fef3096 100644 --- a/tests/Feature/Settings/SecurityTest.php +++ b/tests/Feature/Settings/SecurityTest.php @@ -101,4 +101,4 @@ test('correct password must be provided to update password', function () { $response ->assertSessionHasErrors('current_password') ->assertRedirect(route('security.edit')); -}); \ No newline at end of file +}); diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php index 27f3f87..44a4f33 100644 --- a/tests/Unit/ExampleTest.php +++ b/tests/Unit/ExampleTest.php @@ -2,4 +2,4 @@ test('that true is true', function () { expect(true)->toBeTrue(); -}); \ No newline at end of file +});