Every $hub->publish() call site (chat, hints, security alerts, virus alerts, game_finished, and the pre-game lobby events) now dispatches a PushMercureMessageEvent instead of publishing directly. Two listeners handle it: PublishMercureMessageListener does the actual Mercure publish exactly as before (same wire format, no client changes needed), and LogMercureMessageListener appends it to the activity log of whichever player(s) it was actually delivered to. A private /chat to one agent only gets logged for that agent, never broadcast into everyone's transcript - the event carries an explicit targetScreen (null = everyone) rather than leaving listeners to guess from the payload shape. The per-player log format moved from flat text to JSON Lines (one timestamped, structured entry per line) via a new shared SessionActivityLogger service, since a flat string can't carry an event's type or exact payload - both needed for a future feature to replay a player's full session history, not just their own commands, on page reload. The admin session log viewer now parses and formats these entries back into readable lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
428 lines
14 KiB
PHP
428 lines
14 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Game;
|
|
|
|
use App\Game\Entity\Game;
|
|
use App\Game\Entity\Player;
|
|
use App\Game\Entity\Session;
|
|
use App\Game\Entity\SessionSetting;
|
|
use App\Game\Enum\GameStatus;
|
|
use App\Game\Enum\SessionStatus;
|
|
use App\Game\Enum\SessionSettingType;
|
|
use App\Game\Repository\LobbyMessageRepository;
|
|
use App\Game\Service\GameDashboardService;
|
|
use App\Tech\Entity\User;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use App\Game\Repository\GameRepository;
|
|
use App\Game\Repository\SessionRepository;
|
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class GameDashboardServiceTest extends TestCase
|
|
{
|
|
private $entityManager;
|
|
private $gameRepository;
|
|
private $sessionRepository;
|
|
private $lobbyMessageRepository;
|
|
private $eventDispatcher;
|
|
private $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
|
$this->gameRepository = $this->createMock(GameRepository::class);
|
|
$this->sessionRepository = $this->createMock(SessionRepository::class);
|
|
$this->lobbyMessageRepository = $this->createMock(LobbyMessageRepository::class);
|
|
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
|
|
|
$this->service = new GameDashboardService(
|
|
$this->gameRepository,
|
|
$this->sessionRepository,
|
|
$this->lobbyMessageRepository,
|
|
$this->entityManager,
|
|
$this->eventDispatcher,
|
|
);
|
|
}
|
|
|
|
public function testCreateSessionDoesNotInitializeSettings(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setStatus(GameStatus::OPEN);
|
|
|
|
$user = new User();
|
|
$user->setUsername('testuser');
|
|
|
|
$this->entityManager->expects($this->exactly(2))
|
|
->method('persist');
|
|
// 1. Session, 2. Player
|
|
|
|
$session = $this->service->createSession($game, $user, false);
|
|
|
|
$this->assertInstanceOf(Session::class, $session);
|
|
}
|
|
|
|
public function testJoinSessionDoesNotInitializeSettings(): void
|
|
{
|
|
$user = new User();
|
|
$user->setUsername('testuser');
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(3);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
|
|
$setting = new SessionSetting();
|
|
$setting->setSession($session);
|
|
$setting->setName(SessionSettingType::INVITE_CODE);
|
|
$setting->setValue('abc-123');
|
|
|
|
$repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
|
|
$this->entityManager->method('getRepository')
|
|
->willReturn($repo);
|
|
|
|
$repo->method('findOneBy')
|
|
->willReturn($setting);
|
|
|
|
$this->entityManager->expects($this->exactly(1))
|
|
->method('persist');
|
|
// 1. Player
|
|
|
|
$result = $this->service->joinSession('abc-123', $user);
|
|
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function testStartSessionAssignsScreensAndInitializesSettings(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(2);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(\App\Game\Enum\SessionStatus::CREATED);
|
|
|
|
$user1 = new User();
|
|
$user1->setUsername('user1');
|
|
$player1 = new Player();
|
|
$player1->setUser($user1);
|
|
$session->addPlayer($player1);
|
|
|
|
$user2 = new User();
|
|
$user2->setUsername('user2');
|
|
$player2 = new Player();
|
|
$player2->setUser($user2);
|
|
$session->addPlayer($player2);
|
|
|
|
// For each player (2):
|
|
// Player (persist)
|
|
// SessionSetting (rights)
|
|
// SessionSetting (pwd)
|
|
// SessionSetting (chat tracking)
|
|
// SessionSetting (verify codes)
|
|
// SessionSetting (verification progress)
|
|
// Total = 2 * 6 = 12
|
|
// PLUS Session (persist)
|
|
// Total = 13
|
|
|
|
$this->entityManager->expects($this->exactly(13))
|
|
->method('persist');
|
|
|
|
$result = $this->service->startSession($session);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
|
|
$this->assertNotNull($player1->getScreen());
|
|
$this->assertNotNull($player2->getScreen());
|
|
$this->assertNotEquals($player1->getScreen(), $player2->getScreen());
|
|
}
|
|
|
|
public function testStartSessionWithFourPlayers(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(4);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(\App\Game\Enum\SessionStatus::CREATED);
|
|
|
|
for ($i = 1; $i <= 4; $i++) {
|
|
$user = new User();
|
|
$user->setUsername('user' . $i);
|
|
$player = new Player();
|
|
$player->setUser($user);
|
|
$session->addPlayer($player);
|
|
}
|
|
|
|
// For each player (4):
|
|
// Player (persist) + 5 Settings = 6 persists
|
|
// Total = 4 * 6 = 24
|
|
// PLUS Session (persist)
|
|
// Total = 25
|
|
|
|
$this->entityManager->expects($this->exactly(25))
|
|
->method('persist');
|
|
|
|
$result = $this->service->startSession($session);
|
|
|
|
$this->assertTrue($result);
|
|
foreach ($session->getPlayers() as $player) {
|
|
$this->assertNotNull($player->getScreen());
|
|
$this->assertGreaterThanOrEqual(1, $player->getScreen());
|
|
$this->assertLessThanOrEqual(4, $player->getScreen());
|
|
}
|
|
}
|
|
|
|
public function testLeaveSession(): void
|
|
{
|
|
$user = new User();
|
|
$session = new Session();
|
|
$session->setStatus(\App\Game\Enum\SessionStatus::CREATED);
|
|
$session->setTimer(0);
|
|
|
|
$player1 = new Player();
|
|
$player1->setUser($user);
|
|
$player1->setSession($session);
|
|
$session->addPlayer($player1);
|
|
|
|
$player2 = new Player();
|
|
$player2->setUser(new User());
|
|
$player2->setSession($session);
|
|
$session->addPlayer($player2);
|
|
|
|
$setting = new SessionSetting();
|
|
$setting->setPlayer($player1);
|
|
$setting->setSession($session);
|
|
$session->addSetting($setting);
|
|
|
|
$this->entityManager->expects($this->exactly(2))
|
|
->method('remove');
|
|
// 1. SessionSetting, 2. Player
|
|
|
|
$result = $this->service->leaveSession($session, $user);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertCount(1, $session->getPlayers());
|
|
}
|
|
|
|
public function testToggleReady(): void
|
|
{
|
|
$user = new User();
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(1);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(SessionStatus::READY);
|
|
|
|
$player = new Player();
|
|
$player->setUser($user);
|
|
$player->setScreen(1);
|
|
$session->addPlayer($player);
|
|
|
|
$repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
|
|
$this->entityManager->method('getRepository')
|
|
->willReturn($repo);
|
|
|
|
// First call: toggle ON
|
|
$repo->expects($this->atLeastOnce())
|
|
->method('getSetting')
|
|
->willReturn(null); // No setting initially
|
|
|
|
$this->entityManager->expects($this->atLeastOnce())
|
|
->method('persist')
|
|
->with($this->callback(function($entity) {
|
|
return $entity instanceof SessionSetting && $entity->getName() === SessionSettingType::READY_AT_FOR_PLAYER1;
|
|
}));
|
|
|
|
$result = $this->service->toggleReady($session, $user);
|
|
$this->assertTrue($result);
|
|
}
|
|
|
|
public function testCheckAllPlayersReadyTransitionsStatus(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(2);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(SessionStatus::READY);
|
|
|
|
$player1 = new Player();
|
|
$player1->setScreen(1);
|
|
$session->addPlayer($player1);
|
|
|
|
$player2 = new Player();
|
|
$player2->setScreen(2);
|
|
$session->addPlayer($player2);
|
|
|
|
$repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
|
|
$this->entityManager->method('getRepository')
|
|
->willReturn($repo);
|
|
|
|
$setting1 = new SessionSetting();
|
|
$setting1->setName(SessionSettingType::READY_AT_FOR_PLAYER1);
|
|
$setting1->setValue((string)time());
|
|
$setting1->setPlayer($player1);
|
|
|
|
$setting2 = new SessionSetting();
|
|
$setting2->setName(SessionSettingType::READY_AT_FOR_PLAYER2);
|
|
$setting2->setValue((string)time());
|
|
$setting2->setPlayer($player2);
|
|
|
|
$repo->method('getSetting')
|
|
->willReturnCallback(function($s, $name, $p) use ($setting1, $setting2) {
|
|
if ($name === SessionSettingType::READY_AT_FOR_PLAYER1) return $setting1;
|
|
if ($name === SessionSettingType::READY_AT_FOR_PLAYER2) return $setting2;
|
|
return null;
|
|
});
|
|
|
|
$this->service->checkAllPlayersReady($session);
|
|
|
|
$this->assertEquals(SessionStatus::PLAYING, $session->getStatus());
|
|
}
|
|
|
|
public function testCheckAllPlayersReadyTimeouts(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(2);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(SessionStatus::READY);
|
|
|
|
$player1 = new Player();
|
|
$player1->setScreen(1);
|
|
$session->addPlayer($player1);
|
|
|
|
$player2 = new Player();
|
|
$player2->setScreen(2);
|
|
$session->addPlayer($player2);
|
|
|
|
$repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
|
|
$this->entityManager->method('getRepository')
|
|
->willReturn($repo);
|
|
|
|
$setting1 = new SessionSetting();
|
|
$setting1->setName(SessionSettingType::READY_AT_FOR_PLAYER1);
|
|
$setting1->setValue((string)(time() - 70)); // Timed out
|
|
$setting1->setPlayer($player1);
|
|
$session->addSetting($setting1);
|
|
|
|
$repo->method('getSetting')
|
|
->willReturnCallback(function($s, $name, $p) use ($setting1) {
|
|
if ($name === SessionSettingType::READY_AT_FOR_PLAYER1) return $setting1;
|
|
return null;
|
|
});
|
|
|
|
$this->entityManager->expects($this->once())
|
|
->method('remove')
|
|
->with($setting1);
|
|
|
|
$this->entityManager->expects($this->atLeastOnce())
|
|
->method('flush');
|
|
|
|
$this->eventDispatcher->expects($this->once())
|
|
->method('dispatch')
|
|
->willReturnArgument(0);
|
|
|
|
$this->service->checkAllPlayersReady($session);
|
|
|
|
$this->assertEquals(SessionStatus::READY, $session->getStatus());
|
|
}
|
|
|
|
public function testJoinSessionTriggersStartSessionWhenFull(): void
|
|
{
|
|
$user = new User();
|
|
$user->setUsername('testuser');
|
|
$game = new Game();
|
|
$game->setNumberOfPlayers(2);
|
|
$session = new Session();
|
|
$session->setGame($game);
|
|
$session->setStatus(\App\Game\Enum\SessionStatus::CREATED);
|
|
|
|
$existingUser = new User();
|
|
$existingUser->setUsername('existing');
|
|
$existingPlayer = new Player();
|
|
$existingPlayer->setUser($existingUser);
|
|
$session->addPlayer($existingPlayer);
|
|
|
|
$setting = new SessionSetting();
|
|
$setting->setSession($session);
|
|
$setting->setName(SessionSettingType::INVITE_CODE);
|
|
$setting->setValue('abc-123');
|
|
|
|
$repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
|
|
$this->entityManager->method('getRepository')
|
|
->willReturn($repo);
|
|
|
|
$repo->method('findOneBy')
|
|
->willReturn($setting);
|
|
|
|
// Expectation:
|
|
// 1. Persist new Player
|
|
// 2. Persist Player 1 (existing) screen update
|
|
// 3. Persist Player 2 (new) screen update
|
|
// 4. Persist Session status update
|
|
// 5. 5 Settings for Player 1
|
|
// 6. 5 Settings for Player 2
|
|
// Total = 1 + 1 + 1 + 1 + 5 + 5 = 14 persists
|
|
$this->entityManager->expects($this->exactly(14))
|
|
->method('persist');
|
|
|
|
$result = $this->service->joinSession('abc-123', $user);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
|
|
}
|
|
|
|
public function testLeaveSessionRevertsFromReady(): void
|
|
{
|
|
$user = new User();
|
|
$user->setUsername('testuser');
|
|
$session = new Session();
|
|
$session->setStatus(\App\Game\Enum\SessionStatus::READY);
|
|
$session->setTimer(0);
|
|
|
|
$player1 = new Player();
|
|
$player1->setUser($user);
|
|
$player1->setScreen(1);
|
|
$player1->setSession($session);
|
|
$session->addPlayer($player1);
|
|
|
|
$player2 = new Player();
|
|
$player2->setUser(new User());
|
|
$player2->setScreen(2);
|
|
$player2->setSession($session);
|
|
$session->addPlayer($player2);
|
|
|
|
$this->entityManager->expects($this->exactly(1))
|
|
->method('remove');
|
|
// 1. Player1, any settings (none in this test setup)
|
|
|
|
$result = $this->service->leaveSession($session, $user);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertEquals(\App\Game\Enum\SessionStatus::CREATED, $session->getStatus());
|
|
$this->assertNull($player2->getScreen());
|
|
}
|
|
|
|
public function testCreateSessionForOnePlayerGame(): void
|
|
{
|
|
$game = new Game();
|
|
$game->setStatus(GameStatus::OPEN);
|
|
$game->setNumberOfPlayers(1);
|
|
|
|
$user = new User();
|
|
$user->setUsername('testuser');
|
|
|
|
// 1. Session persist
|
|
// 2. Player persist
|
|
// 3. Player screen persist (during startSession)
|
|
// 4. Session status persist (during startSession)
|
|
// 5. 5 Settings for Player (during startSession)
|
|
// Total = 1 + 1 + 1 + 1 + 5 = 9 persists
|
|
$this->entityManager->expects($this->exactly(9))
|
|
->method('persist');
|
|
|
|
$session = $this->service->createSession($game, $user, false);
|
|
|
|
$this->assertInstanceOf(Session::class, $session);
|
|
$this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
|
|
}
|
|
}
|