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']); } }