From a00899e444e4ff475ed2555f9a5632c71930ee85 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 18 Aug 2026 19:47:53 +0200 Subject: [PATCH] Route every Mercure publish through an event, log pushes per-player 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 --- src/Command/SendMainframeHintsCommand.php | 30 +++---- src/Game/Controller/GameAdminController.php | 60 ++++++++++++- src/Game/Controller/GameApiController.php | 17 ++-- src/Game/Event/PushMercureMessageEvent.php | 35 ++++++++ .../LogMercureMessageListener.php | 43 ++++++++++ .../PublishMercureMessageListener.php | 33 +++++++ src/Game/Service/GameDashboardService.php | 24 ++---- src/Game/Service/GameResponseService.php | 85 +++++++------------ src/Game/Service/SessionActivityLogger.php | 46 ++++++++++ templates/game/admin/sessions/view.html.twig | 3 +- tests/Game/GameDashboardServiceTest.php | 13 +-- .../GameResponseServiceChatVerifyCodeTest.php | 41 +++++++-- tests/Game/SessionLoggingTest.php | 21 +++-- 13 files changed, 333 insertions(+), 118 deletions(-) create mode 100644 src/Game/Event/PushMercureMessageEvent.php create mode 100644 src/Game/EventListener/LogMercureMessageListener.php create mode 100644 src/Game/EventListener/PublishMercureMessageListener.php create mode 100644 src/Game/Service/SessionActivityLogger.php diff --git a/src/Command/SendMainframeHintsCommand.php b/src/Command/SendMainframeHintsCommand.php index 572078f..e57335a 100644 --- a/src/Command/SendMainframeHintsCommand.php +++ b/src/Command/SendMainframeHintsCommand.php @@ -8,6 +8,7 @@ use App\Game\Entity\Session; use App\Game\Enum\GameSettingType; use App\Game\Enum\SessionSettingType; use App\Game\Enum\SessionStatus; +use App\Game\Event\PushMercureMessageEvent; use App\Game\Repository\GameSettingRepository; use App\Game\Repository\SessionRepository; use App\Game\Repository\SessionSettingRepository; @@ -17,8 +18,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; #[AsCommand( name: 'app:hints:check', @@ -46,7 +46,7 @@ final class SendMainframeHintsCommand extends Command private readonly GameSettingRepository $gameSettingRepository, private readonly PlayerService $playerService, private readonly EntityManagerInterface $entityManager, - private readonly HubInterface $hub, + private readonly EventDispatcherInterface $eventDispatcher, ) { parent::__construct(); } @@ -100,12 +100,11 @@ final class SendMainframeHintsCommand extends Command $this->entityManager->persist($session); $this->entityManager->flush(); - try { - $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode(['type' => 'game_finished', 'status' => 'lost']))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + ['type' => 'game_finished', 'status' => 'lost'], + 'game_finished', + )); return true; } @@ -303,11 +302,12 @@ final class SendMainframeHintsCommand extends Command private function publishHint(Session $session, string $message, Player $player): void { - try { - $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode([$player->getScreen() ?? 0, $message, 'hint']))); - } catch (\Exception $e) { - // Mercure might be down, but we don't want to crash the game - } + $screen = $player->getScreen() ?? 0; + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + [$screen, $message, 'hint'], + 'hint', + $screen, + )); } } diff --git a/src/Game/Controller/GameAdminController.php b/src/Game/Controller/GameAdminController.php index 20efaa3..3a221f4 100644 --- a/src/Game/Controller/GameAdminController.php +++ b/src/Game/Controller/GameAdminController.php @@ -58,7 +58,7 @@ final class GameAdminController extends AbstractController $playersLogs[] = [ 'username' => $player->getUser()->getUsername(), - 'logs' => file_exists($logFile) ? file_get_contents($logFile) : '', + 'entries' => file_exists($logFile) ? $this->parseActivityLog($logFile) : [], ]; } @@ -68,4 +68,62 @@ final class GameAdminController extends AbstractController 'lobbyMessages' => $lobbyMessageRepository->findForSession($session), ]); } + + /** + * @return array + */ + private function parseActivityLog(string $logFile): array + { + $lines = []; + + foreach (file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $rawLine) { + $entry = json_decode($rawLine, true); + + if (!is_array($entry) || !isset($entry['ts'], $entry['source'])) { + // Fall back to showing whatever couldn't be parsed, rather than dropping it silently. + $lines[] = ['time' => '', 'line' => $rawLine]; + continue; + } + + $time = (\DateTimeImmutable::createFromFormat(DATE_ATOM, $entry['ts']) ?: null)?->format('Y-m-d H:i:s') ?? $entry['ts']; + + $lines[] = ['time' => $time, 'line' => $this->formatActivityLogLine($entry)]; + } + + return $lines; + } + + private function formatActivityLogLine(array $entry): string + { + $source = $entry['source'] ?? ''; + + if ($source === 'player') { + return 'PLAYER: ' . (string)($entry['content'] ?? ''); + } + + if ($source === 'server') { + return 'SERVER: ' . (string)($entry['content'] ?? ''); + } + + if ($source === 'push') { + return $this->formatPushLine((string)($entry['eventType'] ?? ''), $entry['payload'] ?? null); + } + + return json_encode($entry); + } + + private function formatPushLine(string $eventType, mixed $payload): string + { + return match ($eventType) { + 'chat' => is_array($payload) && ($payload[0] ?? 0) !== 0 + ? 'CHAT (private): ' . ($payload[1] ?? '') + : 'CHAT: ' . (is_array($payload) ? ($payload[1] ?? '') : ''), + 'hint' => 'HINT: ' . (is_array($payload) ? ($payload[1] ?? '') : ''), + 'security_alert' => 'SECURITY ALERT: ' . (is_array($payload) ? ($payload[1] ?? '') : ''), + 'virus_alert' => 'VIRUS: ' . (is_array($payload) ? ($payload[1] ?? '') : ''), + 'help_modus_activated' => 'MAINFRAME: ' . (is_array($payload) ? ($payload[1] ?? '') : ''), + 'game_finished' => 'GAME FINISHED: ' . (is_array($payload) ? ($payload['status'] ?? '') : ''), + default => 'PUSH (' . $eventType . '): ' . json_encode($payload), + }; + } } diff --git a/src/Game/Controller/GameApiController.php b/src/Game/Controller/GameApiController.php index a5da0af..c03fb2c 100644 --- a/src/Game/Controller/GameApiController.php +++ b/src/Game/Controller/GameApiController.php @@ -5,6 +5,7 @@ namespace App\Game\Controller; use App\Game\Entity\Session; use App\Game\Enum\SessionStatus; +use App\Game\Event\PushMercureMessageEvent; use App\Game\Service\GameResponseService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -12,8 +13,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; #[Route('/game/api', name: 'game_api_')] final class GameApiController extends AbstractController @@ -22,7 +22,7 @@ final class GameApiController extends AbstractController public function __construct( protected GameResponseService $gameResponseService, private EntityManagerInterface $entityManager, - private HubInterface $hub, + private EventDispatcherInterface $eventDispatcher, ) { } @@ -50,12 +50,11 @@ final class GameApiController extends AbstractController $this->entityManager->persist($session); $this->entityManager->flush(); - try { - $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode(['type' => 'game_finished', 'status' => 'lost']))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + ['type' => 'game_finished', 'status' => 'lost'], + 'game_finished', + )); $isFinished = true; } diff --git a/src/Game/Event/PushMercureMessageEvent.php b/src/Game/Event/PushMercureMessageEvent.php new file mode 100644 index 0000000..9691dcd --- /dev/null +++ b/src/Game/Event/PushMercureMessageEvent.php @@ -0,0 +1,35 @@ +session->getPlayers() as $player) { + if ($event->targetScreen !== null && $player->getScreen() !== $event->targetScreen) { + continue; + } + + $this->sessionActivityLogger->log($player, 'push', [ + 'eventType' => $event->eventType, + 'payload' => $event->payload, + ]); + } + } catch (\Throwable $e) { + // A logging hiccup should never break gameplay + } + } +} diff --git a/src/Game/EventListener/PublishMercureMessageListener.php b/src/Game/EventListener/PublishMercureMessageListener.php new file mode 100644 index 0000000..055be26 --- /dev/null +++ b/src/Game/EventListener/PublishMercureMessageListener.php @@ -0,0 +1,33 @@ +publish() call site. + */ +#[AsEventListener] +final class PublishMercureMessageListener +{ + public function __construct( + private readonly HubInterface $hub, + ) { + } + + public function __invoke(PushMercureMessageEvent $event): void + { + try { + $topic = '/game/hub/' . $event->session->getId(); + $this->hub->publish(new Update($topic, json_encode($event->payload))); + } catch (\Exception $e) { + // Mercure might be down, but we don't want to crash the game + } + } +} diff --git a/src/Game/Service/GameDashboardService.php b/src/Game/Service/GameDashboardService.php index 9b30131..95917cc 100644 --- a/src/Game/Service/GameDashboardService.php +++ b/src/Game/Service/GameDashboardService.php @@ -11,14 +11,14 @@ use App\Game\Entity\SessionSetting; use App\Game\Enum\GameStatus; use App\Game\Enum\SessionSettingType; use App\Game\Enum\SessionStatus; +use App\Game\Event\PushMercureMessageEvent; use App\Game\Repository\GameRepository; use App\Game\Repository\LobbyMessageRepository; use App\Game\Repository\SessionRepository; use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; final class GameDashboardService { @@ -31,7 +31,7 @@ final class GameDashboardService private readonly SessionRepository $sessionRepository, private readonly LobbyMessageRepository $lobbyMessageRepository, private readonly EntityManagerInterface $entityManager, - private readonly HubInterface $hub, + private readonly EventDispatcherInterface $eventDispatcher, ) { } @@ -480,12 +480,11 @@ final class GameDashboardService private function publishLobbyEvent(Session $session, string $type, array $extra = []): void { - try { - $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode(array_merge(['type' => $type], $extra)))); - } catch (\Exception $e) { - // Mercure might be down, but we don't want to crash the game - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + array_merge(['type' => $type], $extra), + $type, + )); } public function checkAllPlayersReady(Session $session): void @@ -559,12 +558,7 @@ final class GameDashboardService $this->entityManager->flush(); - try { - $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode(['type' => 'all_ready']))); - } catch (\Exception $e) { - // Mercure might be down, but we don't want to crash the game - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent($session, ['type' => 'all_ready'], 'all_ready')); } } diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index 15e8c06..337b772 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -8,12 +8,12 @@ use App\Game\Enum\SessionStatus; use App\Game\Entity\Player; use App\Game\Entity\Session; use App\Game\Entity\SessionSetting; +use App\Game\Event\PushMercureMessageEvent; use App\Game\Repository\SessionSettingRepository; use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class GameResponseService { @@ -25,8 +25,9 @@ class GameResponseService private Security $security, private PlayerService $playerService, private SessionSettingRepository $sessionSettingRepository, - private HubInterface $hub, private EntityManagerInterface $entityManager, + private EventDispatcherInterface $eventDispatcher, + private SessionActivityLogger $sessionActivityLogger, private string $projectDir, ) { } @@ -72,7 +73,7 @@ class GameResponseService $this->enforceLockedFilesRemovalDeadline($player->getSession()); - $this->logSessionActivity($player, 'PLAYER: ' . $message); + $this->sessionActivityLogger->log($player, 'player', ['content' => $message]); $data = $this->handleLockedPlayer($message, $player); @@ -98,28 +99,12 @@ class GameResponseService } if ($responseLog !== '') { - $this->logSessionActivity($player, 'SERVER: ' . trim($responseLog)); + $this->sessionActivityLogger->log($player, 'server', ['content' => trim($responseLog)]); } return $data; } - private function logSessionActivity(Player $player, string $content): void - { - $sessionId = $player->getSession()->getId(); - $logDir = $this->projectDir . '/var/log/sessions/' . $sessionId; - - if (!is_dir($logDir)) { - mkdir($logDir, 0777, true); - } - - $logFile = $logDir . '/' . $player->getLogFileBasename() . '.txt'; - $timestamp = date('Y-m-d H:i:s'); - $logMessage = sprintf("[%s] %s\n", $timestamp, $content); - - file_put_contents($logFile, $logMessage, FILE_APPEND); - } - private function getRechten(Player $player): array { $settingName = SessionSettingType::tryFrom('RightsForPlayer' . $player->getScreen()); @@ -430,17 +415,19 @@ class GameResponseService $message = trim($message); - $activeGame = $player->getSession()?->getId(); + $session = $player->getSession(); - if(is_null($activeGame)) + if(is_null($session)) return false; - $topic = '/game/hub/' . $activeGame; - try { - $this->hub->publish(new Update($topic, json_encode([$sendTo, $message]))); - } catch (\Exception $e) { - // Mercure might be down - } + // A /chat targeted at one agent (e.g. "/chat 3 ...") must only ever be + // logged for that agent, not broadcast into everyone's transcript. + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + [$sendTo, $message], + 'chat', + (int)$sendTo === 0 ? null : (int)$sendTo, + )); $this->updateChatTracking($player, (int)$sendTo); @@ -480,15 +467,14 @@ class GameResponseService $this->entityManager->persist($setting); $this->entityManager->flush(); - // Notify the player that their codes have changed - $topic = '/game/hub/' . $session->getId(); + // Notify the player that their codes have changed - only this one (screen) $notification = "Security Alert: One of your verify codes was shared and has been regenerated."; - // We send it only to this player (screen) - try { - $this->hub->publish(new Update($topic, json_encode([$screen, $notification]))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + [$screen, $notification], + 'security_alert', + $screen, + )); } } @@ -963,13 +949,8 @@ class GameResponseService $this->entityManager->persist($everyoneVerifiedSetting); $this->entityManager->flush(); - $topic = '/game/hub/' . $session->getId(); $message = "Mainframe Help Modus: Agents Doyle, Vega and Lennox rapports have been updated with coded messages."; - try { - $this->hub->publish(new Update($topic, json_encode([0, $message]))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent($session, [0, $message], 'help_modus_activated')); } } @@ -1155,12 +1136,11 @@ class GameResponseService $this->entityManager->persist($session); $this->entityManager->flush(); - $topic = '/game/hub/' . $session->getId(); - try { - $this->hub->publish(new Update($topic, json_encode(['type' => 'game_finished', 'status' => 'won']))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent( + $session, + ['type' => 'game_finished', 'status' => 'won'], + 'game_finished', + )); return true; } @@ -1245,13 +1225,8 @@ class GameResponseService } if (!empty($stillLocked)) { - $topic = '/game/hub/' . $session->getId(); $message = 'AI VIRUS: Integrity check complete. Restored files that were not fully purged in time.'; - try { - $this->hub->publish(new Update($topic, json_encode([0, $message, 'virus']))); - } catch (\Exception $e) { - // Mercure might be down - } + $this->eventDispatcher->dispatch(new PushMercureMessageEvent($session, [0, $message, 'virus'], 'virus_alert')); } } } diff --git a/src/Game/Service/SessionActivityLogger.php b/src/Game/Service/SessionActivityLogger.php new file mode 100644 index 0000000..65b70fe --- /dev/null +++ b/src/Game/Service/SessionActivityLogger.php @@ -0,0 +1,46 @@ +getSession(); + if ($session === null) { + return; + } + + $logDir = $this->projectDir . '/var/log/sessions/' . $session->getId(); + if (!is_dir($logDir)) { + mkdir($logDir, 0777, true); + } + + $logFile = $logDir . '/' . $player->getLogFileBasename() . '.txt'; + + $entry = array_merge([ + 'ts' => (new \DateTimeImmutable())->format(DATE_ATOM), + 'source' => $source, + ], $data); + + file_put_contents($logFile, json_encode($entry) . "\n", FILE_APPEND | LOCK_EX); + } +} diff --git a/templates/game/admin/sessions/view.html.twig b/templates/game/admin/sessions/view.html.twig index 589965b..48df90a 100644 --- a/templates/game/admin/sessions/view.html.twig +++ b/templates/game/admin/sessions/view.html.twig @@ -98,7 +98,8 @@ font-size: 0.85rem; line-height: 1.5; margin: 0; - ">{{ playerLog.logs ?: 'No logs found for this player.' }} + ">{% if playerLog.entries is empty %}No logs found for this player.{% else %}{% for entry in playerLog.entries %}{% if entry.time %}[{{ entry.time }}] {% endif %}{{ entry.line }} +{% endfor %}{% endif %} {% endfor %} {% endblock %} diff --git a/tests/Game/GameDashboardServiceTest.php b/tests/Game/GameDashboardServiceTest.php index 87cc1c4..5801981 100644 --- a/tests/Game/GameDashboardServiceTest.php +++ b/tests/Game/GameDashboardServiceTest.php @@ -16,7 +16,7 @@ use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; use App\Game\Repository\GameRepository; use App\Game\Repository\SessionRepository; -use Symfony\Component\Mercure\HubInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use PHPUnit\Framework\TestCase; class GameDashboardServiceTest extends TestCase @@ -25,7 +25,7 @@ class GameDashboardServiceTest extends TestCase private $gameRepository; private $sessionRepository; private $lobbyMessageRepository; - private $hub; + private $eventDispatcher; private $service; protected function setUp(): void @@ -34,14 +34,14 @@ class GameDashboardServiceTest extends TestCase $this->gameRepository = $this->createMock(GameRepository::class); $this->sessionRepository = $this->createMock(SessionRepository::class); $this->lobbyMessageRepository = $this->createMock(LobbyMessageRepository::class); - $this->hub = $this->createMock(HubInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->service = new GameDashboardService( $this->gameRepository, $this->sessionRepository, $this->lobbyMessageRepository, $this->entityManager, - $this->hub, + $this->eventDispatcher, ); } @@ -316,8 +316,9 @@ class GameDashboardServiceTest extends TestCase $this->entityManager->expects($this->atLeastOnce()) ->method('flush'); - $this->hub->expects($this->once()) - ->method('publish'); + $this->eventDispatcher->expects($this->once()) + ->method('dispatch') + ->willReturnArgument(0); $this->service->checkAllPlayersReady($session); diff --git a/tests/Game/GameResponseServiceChatVerifyCodeTest.php b/tests/Game/GameResponseServiceChatVerifyCodeTest.php index 1b4b0f0..38f558f 100644 --- a/tests/Game/GameResponseServiceChatVerifyCodeTest.php +++ b/tests/Game/GameResponseServiceChatVerifyCodeTest.php @@ -11,42 +11,62 @@ use App\Game\Enum\SessionSettingType; use App\Game\Repository\SessionSettingRepository; use App\Game\Service\GameResponseService; use App\Game\Service\PlayerService; +use App\Game\Service\SessionActivityLogger; use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\Mercure\HubInterface; -use Symfony\Component\Mercure\Update; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class GameResponseServiceChatVerifyCodeTest extends TestCase { + private string $tempDir; private $security; private $playerService; private $sessionSettingRepository; - private $hub; + private $eventDispatcher; private $entityManager; private $service; protected function setUp(): void { + $this->tempDir = sys_get_temp_dir() . '/escapepage_test_' . uniqid(); + mkdir($this->tempDir, 0777, true); + $this->security = $this->createMock(Security::class); $this->playerService = $this->createMock(PlayerService::class); $this->sessionSettingRepository = $this->createMock(SessionSettingRepository::class); - $this->hub = $this->createMock(HubInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->entityManager = $this->createMock(EntityManagerInterface::class); $this->service = new GameResponseService( $this->security, $this->playerService, $this->sessionSettingRepository, - $this->hub, $this->entityManager, - 'H:\escapepage' + $this->eventDispatcher, + new SessionActivityLogger($this->tempDir), + $this->tempDir ); $_ENV['MERCURE_TOPIC_BASE'] = 'http://test'; } + protected function tearDown(): void + { + $this->removeDir($this->tempDir); + } + + private function removeDir(string $dir): void + { + if (!is_dir($dir)) return; + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? $this->removeDir("$dir/$file") : unlink("$dir/$file"); + } + rmdir($dir); + } + public function testChatRegeneratesVerifyCodesIfShared(): void { $user = new User(); @@ -63,6 +83,7 @@ class GameResponseServiceChatVerifyCodeTest extends TestCase $player->method('getUser')->willReturn($user); $player->method('getScreen')->willReturn(1); $player->method('getSession')->willReturn($session); + $player->method('getLogFileBasename')->willReturn('testuser'); $this->security->method('getUser')->willReturn($user); $this->playerService->method('GetCurrentlyActiveAsPlayer')->willReturn($player); @@ -88,9 +109,11 @@ class GameResponseServiceChatVerifyCodeTest extends TestCase return null; }); - // Expect Mercure updates: 1 for chat, 1 for notification - $this->hub->expects($this->exactly(2)) - ->method('publish'); + // Expect 2 PushMercureMessageEvents dispatched: 1 for chat, 1 for the + // regenerated-code notification + $this->eventDispatcher->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnArgument(0); $this->entityManager->expects($this->once()) ->method('flush'); diff --git a/tests/Game/SessionLoggingTest.php b/tests/Game/SessionLoggingTest.php index 4e7e5fa..e27eb54 100644 --- a/tests/Game/SessionLoggingTest.php +++ b/tests/Game/SessionLoggingTest.php @@ -10,11 +10,12 @@ use App\Game\Enum\SessionSettingType; use App\Game\Repository\SessionSettingRepository; use App\Game\Service\GameResponseService; use App\Game\Service\PlayerService; +use App\Game\Service\SessionActivityLogger; use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\Mercure\HubInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; class SessionLoggingTest extends TestCase { @@ -22,7 +23,6 @@ class SessionLoggingTest extends TestCase private $security; private $playerService; private $sessionSettingRepository; - private $hub; private $entityManager; private $service; @@ -34,15 +34,15 @@ class SessionLoggingTest extends TestCase $this->security = $this->createMock(Security::class); $this->playerService = $this->createMock(PlayerService::class); $this->sessionSettingRepository = $this->createMock(SessionSettingRepository::class); - $this->hub = $this->createMock(HubInterface::class); $this->entityManager = $this->createMock(EntityManagerInterface::class); $this->service = new GameResponseService( $this->security, $this->playerService, $this->sessionSettingRepository, - $this->hub, $this->entityManager, + new EventDispatcher(), + new SessionActivityLogger($this->tempDir), $this->tempDir ); } @@ -96,8 +96,15 @@ class SessionLoggingTest extends TestCase $logFilePath = $this->tempDir . '/var/log/sessions/456/player1.txt'; $this->assertFileExists($logFilePath); - $logContent = file_get_contents($logFilePath); - $this->assertStringContainsString('PLAYER: help', $logContent); - $this->assertStringContainsString('SERVER:', $logContent); + $lines = array_filter(explode("\n", file_get_contents($logFilePath))); + $entries = array_map(fn(string $line) => json_decode($line, true), $lines); + + $playerEntry = current(array_filter($entries, fn(array $e) => $e['source'] === 'player')); + $this->assertNotFalse($playerEntry); + $this->assertSame('help', $playerEntry['content']); + + $serverEntry = current(array_filter($entries, fn(array $e) => $e['source'] === 'server')); + $this->assertNotFalse($serverEntry); + $this->assertNotEmpty($serverEntry['content']); } }