userManagementLogger = $this->createMock(LoggerInterface::class); $this->organizationManagementLogger = $this->createMock(LoggerInterface::class); $this->accessControlLogger = $this->createMock(LoggerInterface::class); $this->emailNotificationLogger = $this->createMock(LoggerInterface::class); $this->adminActionsLogger = $this->createMock(LoggerInterface::class); $this->securityLogger = $this->createMock(LoggerInterface::class); $this->errorLogger = $this->createMock(LoggerInterface::class); $this->awsLogger = $this->createMock(LoggerInterface::class); $this->requestStack = $this->createMock(RequestStack::class); $this->service = new LoggerService( $this->userManagementLogger, $this->organizationManagementLogger, $this->accessControlLogger, $this->emailNotificationLogger, $this->adminActionsLogger, $this->securityLogger, $this->errorLogger, $this->awsLogger, $this->requestStack ); } /** * Helper to simulate a request with a specific IP. */ private function mockRequestIp(?string $ip): void { if ($ip === null) { $this->requestStack->method('getCurrentRequest')->willReturn(null); } else { $request = $this->createMock(Request::class); $request->method('getClientIp')->willReturn($ip); $this->requestStack->method('getCurrentRequest')->willReturn($request); } } /** * Helper assertion to check context contains basic fields + specific data. */ private function assertContextContains(array $expectedSubset): \PHPUnit\Framework\Constraint\Callback { return $this->callback(function (array $context) use ($expectedSubset) { // Check Timestamp exists (we can't check exact value easily) if (!isset($context['timestamp'])) { return false; } // Check IP exists if (!isset($context['ip'])) { return false; } // Check specific keys foreach ($expectedSubset as $key => $value) { if (!array_key_exists($key, $context) || $context[$key] !== $value) { return false; } } return true; }); } // ========================================== // TESTS FOR USER MANAGEMENT LOGS // ========================================== public function testLogUserCreated(): void { $this->mockRequestIp('127.0.0.1'); $this->userManagementLogger->expects($this->once()) ->method('notice') ->with( "New user created: 10", $this->assertContextContains([ 'target_user_id' => 10, 'acting_user_id' => 99, 'ip' => '127.0.0.1' ]) ); $this->service->logUserCreated(10, 99); } public function testLogCGUAcceptanceLogsToTwoChannels(): void { $this->mockRequestIp('192.168.1.1'); $userId = 55; // Expect call on User Logger $this->userManagementLogger->expects($this->once()) ->method('info') ->with("User accepted CGU", $this->assertContextContains(['user_id' => $userId])); // Expect call on Security Logger $this->securityLogger->expects($this->once()) ->method('info') ->with("User accepted CGU", $this->assertContextContains(['user_id' => $userId])); $this->service->logCGUAcceptance($userId); } // ========================================== // TESTS FOR ORGANIZATION LOGS // ========================================== public function testLogUserOrganizationLinkCreated(): void { $this->mockRequestIp('10.0.0.1'); $this->organizationManagementLogger->expects($this->once()) ->method('notice') ->with( 'User-Organization link created', $this->assertContextContains([ 'target_user_id' => 1, 'organization_id' => 2, 'acting_user_id' => 3, 'uo_id' => 4 ]) ); $this->service->logUserOrganizationLinkCreated(1, 2, 3, 4); } // ========================================== // TESTS FOR ERROR LOGS // ========================================== public function testLogError(): void { $this->mockRequestIp('127.0.0.1'); $this->errorLogger->expects($this->once()) ->method('error') ->with( 'Something failed', $this->assertContextContains(['details' => 'foo']) ); $this->service->logError('Something failed', ['details' => 'foo']); } public function testLogEntityNotFoundHandlesGlobals(): void { $this->mockRequestIp('127.0.0.1'); // Simulate global server variable for REQUEST_URI $_SERVER['REQUEST_URI'] = '/some/path'; $this->errorLogger->expects($this->once()) ->method('error') ->with( 'Entity not found', $this->assertContextContains([ 'entity_type' => 'User', 'id' => 123, 'page_accessed' => '/some/path' ]) ); $this->service->logEntityNotFound('User', ['id' => 123], 1); // Cleanup global unset($_SERVER['REQUEST_URI']); } // ========================================== // TESTS FOR SECURITY LOGS // ========================================== public function testLogAccessDenied(): void { $this->mockRequestIp('10.10.10.10'); $this->securityLogger->expects($this->once()) ->method('warning') ->with( 'Access denied', $this->assertContextContains(['acting_user_id' => 5]) ); $this->service->logAccessDenied(5); } public function testLogTokenRevocation(): void { $this->mockRequestIp(null); // Test with NO REQUEST (e.g. CLI) $this->securityLogger->expects($this->once()) ->method('warning') ->with( 'Token revoked', $this->callback(function($context) { return $context['ip'] === 'unknown' && $context['reason'] === 'expired'; }) ); $this->service->logTokenRevocation('Token revoked', ['reason' => 'expired']); } // ========================================== // TESTS FOR ADMIN ACTIONS // ========================================== public function testLogSuperAdmin(): void { $this->mockRequestIp('1.2.3.4'); $this->adminActionsLogger->expects($this->once()) ->method('notice') ->with( 'Global reset', $this->assertContextContains([ 'target_user_id' => 10, 'acting_user_id' => 1, 'organization_id' => null ]) ); $this->service->logSuperAdmin(10, 1, 'Global reset'); } // ========================================== // TESTS FOR AWS LOGS // ========================================== public function testLogAWSAction(): void { $this->mockRequestIp('8.8.8.8'); $this->awsLogger->expects($this->once()) ->method('info') ->with( 'AWS action performed: Upload', $this->assertContextContains(['bucket' => 'my-bucket']) ); $this->service->logAWSAction('Upload', ['bucket' => 'my-bucket']); } // ========================================== // TESTS FOR ACCESS CONTROL // ========================================== public function testLogRoleEntityAssignment(): void { $this->mockRequestIp('127.0.0.1'); $this->accessControlLogger->expects($this->once()) ->method('info') ->with( 'Role Assigned', $this->assertContextContains([ 'target_user_id' => 2, 'organization_id' => 3, 'role_id' => 4, 'acting_user_id' => 1 ]) ); $this->service->logRoleEntityAssignment(2, 3, 4, 1, 'Role Assigned'); } }