createMock(LoggerInterface::class); $logger->expects($this->exactly(2)) ->method('info') ->with( $this->logicalOr( $this->stringContains('AlfaBank Request'), $this->stringContains('AlfaBank Response') ), $this->callback(function (array $context): bool { return isset($context['host']) && $context['host'] === 'https://test.alfabank.ru'; }) ); $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true, logger: $logger); $connector->withMockClient($mockClient); $connector->send(new GetAccountsRequest); }); test('AlfaBankConnector logs response with host by default', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $calls = []; $logger->expects($this->exactly(2)) ->method('info') ->willReturnCallback(function (string $message, array $context) use (&$calls): void { $calls[] = $message; }); $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true, logger: $logger); $connector->withMockClient($mockClient); $connector->send(new GetAccountsRequest); expect($calls)->toHaveCount(2); expect($calls[0])->toContain('AlfaBank Request'); expect($calls[1])->toContain('AlfaBank Response'); }); test('AlfaBankConnector does not log when loggingEnabled is false', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->never())->method('info'); $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: false, logger: $logger); $connector->withMockClient($mockClient); $connector->send(new GetAccountsRequest); }); test('withoutLogging disables logging', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->never())->method('info'); $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true, logger: $logger); $connector->withoutLogging(); $connector->withMockClient($mockClient); $connector->send(new GetAccountsRequest); }); test('withLogging re-enables logging after withoutLogging', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->exactly(2))->method('info'); $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: false, logger: $logger); $connector->withLogging($logger); $connector->withMockClient($mockClient); $connector->send(new GetAccountsRequest); }); test('SbpConnector logs request with host by default', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->exactly(2)) ->method('info') ->with( $this->logicalOr( $this->stringContains('AlfaBank Request'), $this->stringContains('AlfaBank Response') ), $this->callback(function (array $context): bool { return isset($context['host']) && $context['host'] === 'https://test.alfabank.ru'; }) ); $mockClient = new MockClient([ '*' => MockResponse::make([], 200), ]); $config = createLoggingConfig(); $connector = new SbpConnector($config, 'alias', loggingEnabled: true, logger: $logger); $connector->withMockClient($mockClient); $connector->send(new class extends \Saloon\Http\Request { protected \Saloon\Enums\Method $method = \Saloon\Enums\Method::GET; public function resolveEndpoint(): string { return '/test'; } }); }); test('SbpConnector withoutLogging disables logging', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->never())->method('info'); $mockClient = new MockClient([ '*' => MockResponse::make([], 200), ]); $config = createLoggingConfig(); $connector = new SbpConnector($config, 'alias', loggingEnabled: true, logger: $logger); $connector->withoutLogging(); $connector->withMockClient($mockClient); $connector->send(new class extends \Saloon\Http\Request { protected \Saloon\Enums\Method $method = \Saloon\Enums\Method::GET; public function resolveEndpoint(): string { return '/test'; } }); }); test('LoggingRequestMiddleware masks sensitive headers', function (): void { $middleware = new LoggingRequestMiddleware( $this->createMock(LoggerInterface::class) ); $reflection = new ReflectionMethod($middleware, 'maskHeaders'); $result = $reflection->invoke($middleware, [ 'Authorization' => 'Bearer secret-token', 'Accept' => 'application/json', ]); expect($result['Authorization'])->toBe('Bearer *****'); expect($result['Accept'])->toBe('application/json'); }); test('LoggingRequestMiddleware masks sensitive body fields', function (): void { $middleware = new LoggingRequestMiddleware( $this->createMock(LoggerInterface::class) ); $reflection = new ReflectionMethod($middleware, 'maskBody'); $result = $reflection->invoke($middleware, [ 'pan' => '4276123456789012', 'cvc' => '123', 'amount' => 1000, 'nested' => [ 'client_secret' => 'super-secret', ], ]); expect($result['pan'])->toBe('*****'); expect($result['cvc'])->toBe('*****'); expect($result['amount'])->toBe(1000); expect($result['nested']['client_secret'])->toBe('*****'); }); test('AlfaBankConnector without logger uses NullLogger silently', function (): void { $mockClient = new MockClient([ '*' => MockResponse::make(['accounts' => []], 200), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true); $connector->withMockClient($mockClient); expect($connector->send(new GetAccountsRequest))->status()->toBe(200); }); test('logs non-JSON response body without throwing', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $loggedBody = null; $logger->expects($this->exactly(2)) ->method('info') ->willReturnCallback(function (string $message, array $context) use (&$loggedBody): void { if (str_contains($message, 'Response')) { $loggedBody = $context['body']; } }); $mockClient = new MockClient([ '*' => MockResponse::make('eyJhbGciOiJIUzI1NiJ9...', 200, ['Content-Type' => 'application/jwt']), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true, logger: $logger); $connector->withMockClient($mockClient); $response = $connector->send(new GetAccountsRequest); expect($response->status())->toBe(200); expect($loggedBody)->toBe('eyJhbGciOiJIUzI1NiJ9...'); }); test('logs plain text response without throwing', function (): void { /** @var LoggerInterface&MockObject */ $logger = $this->createMock(LoggerInterface::class); $loggedBody = null; $logger->expects($this->exactly(2)) ->method('info') ->willReturnCallback(function (string $message, array $context) use (&$loggedBody): void { if (str_contains($message, 'Response')) { $loggedBody = $context['body']; } }); $mockClient = new MockClient([ '*' => MockResponse::make('Internal Server Error', 200, ['Content-Type' => 'text/plain']), ]); $config = createLoggingConfig(); $connector = new AlfaBankConnector($config, loggingEnabled: true, logger: $logger); $connector->withMockClient($mockClient); $response = $connector->send(new GetAccountsRequest); expect($response->status())->toBe(200); expect($loggedBody)->toBe('Internal Server Error'); });