diff --git a/.env b/.env new file mode 100644 index 0000000..ad8bd15 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +DOCKER_HOST_APP_PORT=8000 +DOCKER_HOST_NEO4J_HTTP_PORT=7474 +DOCKER_HOST_NEO4J_BOLT_PORT=7687 + +NEO4J_HOST=neo4j+s://bb79fe35.databases.neo4j.io +NEO4J_DATABASE=neo4j +NEO4J_PORT=7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=your_secure_password diff --git a/.gitignore b/.gitignore index a18abad..30011a7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ composer.lock .php-cs-fixer.cache composer.origin.json + +coverage +phpunitCoverage.xml \ No newline at end of file diff --git a/composer.json b/composer.json index 32202f9..86f9233 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "psalm": "APP_ENV=dev php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true", "fix-cs": "vendor/bin/php-cs-fixer fix", "check-cs": "vendor/bin/php-cs-fixer fix --dry-run", - "ci-symfony-install-version": "./.github/scripts/setup-symfony-env.bash" + "ci-symfony-install-version": "./.github/scripts/setup-symfony-env.bash", + "phpunit-with-coverage" : "XDEBUG_MODE=coverage php -d memory_limit=-1 vendor/bin/phpunit --configuration=phpunitCoverage.xml --testsuite=All --coverage-filter=src tests" } } diff --git a/docker-compose.yml b/docker-compose.yml index c28d0d8..09c28b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,11 +11,11 @@ services: volumes: - ./:/opt/project environment: - - NEO4J_HOST=neo4j + - NEO4J_HOST=neo4j+s://bb79fe35.databases.neo4j.io - NEO4J_DATABASE=neo4j - NEO4J_PORT=7687 - NEO4J_USER=neo4j - - NEO4J_PASSWORD=testtest + - NEO4J_PASSWORD=OXDRMgdWFKMcBRCBrIwXnKkwLgDlmFxipnywT6t_AK0 - XDEBUG_CONFIG="client_host=host.docker.internal log=/tmp/xdebug.log" working_dir: /opt/project extra_hosts: diff --git a/src/Event/FailureEvent.php b/src/Event/FailureEvent.php index 1aa2966..d2b939f 100644 --- a/src/Event/FailureEvent.php +++ b/src/Event/FailureEvent.php @@ -15,53 +15,19 @@ class FailureEvent extends Event protected bool $shouldThrowException = true; public function __construct( - private readonly ?string $alias, - private readonly ?Statement $statement, - private readonly Neo4jException $exception, - private readonly \DateTimeInterface $time, - private readonly ?string $scheme, - private readonly ?string $transactionId, + public readonly ?string $alias, + public readonly ?Statement $statement, + public readonly Neo4jException $exception, + public readonly \DateTimeInterface $time, + public readonly ?string $scheme, + public readonly ?string $transactionId, ) { } - public function getStatement(): ?Statement - { - return $this->statement; - } - - public function getException(): Neo4jException - { - return $this->exception; - } - /** @api */ public function disableException(): void { $this->shouldThrowException = false; } - public function shouldThrowException(): bool - { - return $this->shouldThrowException; - } - - public function getTime(): \DateTimeInterface - { - return $this->time; - } - - public function getAlias(): ?string - { - return $this->alias; - } - - public function getScheme(): ?string - { - return $this->scheme; - } - - public function getTransactionId(): ?string - { - return $this->transactionId; - } } diff --git a/src/Event/PostRunEvent.php b/src/Event/PostRunEvent.php index 26750f4..3dfa130 100644 --- a/src/Event/PostRunEvent.php +++ b/src/Event/PostRunEvent.php @@ -12,36 +12,13 @@ class PostRunEvent extends Event public const EVENT_ID = 'neo4j.post_run'; public function __construct( - private readonly ?string $alias, - private readonly ResultSummary $result, - private readonly \DateTimeInterface $time, - private readonly ?string $scheme, - private readonly ?string $transactionId, + public readonly ?string $alias, + public readonly ResultSummary $result, + public readonly \DateTimeInterface $time, + public readonly ?string $scheme, + public readonly ?string $transactionId ) { } - public function getResult(): ResultSummary - { - return $this->result; - } - - public function getTime(): \DateTimeInterface - { - return $this->time; - } - - public function getAlias(): ?string - { - return $this->alias; - } - - public function getScheme(): ?string - { - return $this->scheme; - } - public function getTransactionId(): ?string - { - return $this->transactionId; - } } diff --git a/src/Event/PreRunEvent.php b/src/Event/PreRunEvent.php index 360a942..d25b2f6 100644 --- a/src/Event/PreRunEvent.php +++ b/src/Event/PreRunEvent.php @@ -12,37 +12,13 @@ class PreRunEvent extends Event public const EVENT_ID = 'neo4j.pre_run'; public function __construct( - private readonly ?string $alias, - private readonly Statement $statement, - private readonly \DateTimeInterface $time, - private readonly ?string $scheme, - private readonly ?string $transactionId, + public readonly ?string $alias, + public readonly Statement $statement, + public readonly \DateTimeInterface $time, + public readonly ?string $scheme, + public readonly ?string $transactionId, ) { } - /** @api */ - public function getStatement(): Statement - { - return $this->statement; - } - - public function getTime(): \DateTimeInterface - { - return $this->time; - } - - public function getAlias(): ?string - { - return $this->alias; - } - - public function getScheme(): ?string - { - return $this->scheme; - } - public function getTransactionId(): ?string - { - return $this->transactionId; - } } diff --git a/src/EventListener/Neo4jProfileListener.php b/src/EventListener/Neo4jProfileListener.php index e9a8a29..2986090 100644 --- a/src/EventListener/Neo4jProfileListener.php +++ b/src/EventListener/Neo4jProfileListener.php @@ -12,7 +12,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\Service\ResetInterface; -final class Neo4jProfileListener implements EventSubscriberInterface, ResetInterface +class Neo4jProfileListener implements EventSubscriberInterface, ResetInterface { /** * @var listgetAlias(), $this->enabledProfiles)) { - $time = $event->getTime(); - $result = $event->getResult(); + if (in_array($event->alias, $this->enabledProfiles)) { + $time = $event->time; + $result = $event->result; $end_time = $time->getTimestamp() + $result->getResultAvailableAfter() + $result->getResultConsumedAfter(); + $this->profiledSummaries[] = [ - 'result' => $event->getResult(), - 'alias' => $event->getAlias(), + 'result' => $result, + 'alias' => $event->alias, + 'scheme' => $event->scheme, + 'transaction_id' => $event->transactionId, 'time' => $time->format('Y-m-d H:i:s'), 'start_time' => $time->getTimestamp(), 'end_time' => $end_time, @@ -69,12 +72,12 @@ public function onPostRun(PostRunEvent $event): void public function onFailure(FailureEvent $event): void { - if (in_array($event->getAlias(), $this->enabledProfiles)) { - $time = $event->getTime(); + if (in_array($event->alias, $this->enabledProfiles)) { + $time = $event->time; $this->profiledFailures[] = [ - 'exception' => $event->getException(), - 'statement' => $event->getStatement(), - 'alias' => $event->getAlias(), + 'exception' => $event->exception, + 'statement' => $event->statement, + 'alias' => $event->alias, 'time' => $time->format('Y-m-d H:i:s'), 'timestamp' => $time->getTimestamp(), ]; diff --git a/src/Factories/StopwatchEventNameFactory.php b/src/Factories/StopwatchEventNameFactory.php index a7002dd..89b214a 100644 --- a/src/Factories/StopwatchEventNameFactory.php +++ b/src/Factories/StopwatchEventNameFactory.php @@ -4,7 +4,7 @@ use Laudis\Neo4j\Enum\TransactionState; -final class StopwatchEventNameFactory +class StopwatchEventNameFactory { public function __construct( ) { diff --git a/tests/Unit/Collector/Neo4jDataCollectorTest.php b/tests/Unit/Collector/Neo4jDataCollectorTest.php new file mode 100644 index 0000000..5a2c30f --- /dev/null +++ b/tests/Unit/Collector/Neo4jDataCollectorTest.php @@ -0,0 +1,53 @@ +subscriber = $this->createMock(Neo4jProfileListener::class); + $this->collector = new Neo4jDataCollector($this->subscriber); + } + + public function testGetName(): void + { + $this->assertSame('neo4j', $this->collector->getName()); + } + + public function testGetQueryCount(): void + { + $this->subscriber->method('getProfiledSummaries')->willReturn([ + ['start_time' => 1000, 'query' => 'MATCH (n) RETURN n'], + ]); + $this->collector->collect(new Request(), new Response()); + + $this->assertSame(1, $this->collector->getQueryCount()); + } + + public function testRecursiveToArray(): void + { + $obj = new class { + public function toArray(): array + { + return ['key' => 'value']; + } + }; + $reflection = new \ReflectionClass($this->collector); + $method = $reflection->getMethod('recursiveToArray'); + + $result = $method->invoke($this->collector, $obj); + $this->assertSame(['key' => 'value'], $result); + } +} diff --git a/tests/Unit/Decorators/SymfonyClientTest.php b/tests/Unit/Decorators/SymfonyClientTest.php new file mode 100644 index 0000000..7e6d16b --- /dev/null +++ b/tests/Unit/Decorators/SymfonyClientTest.php @@ -0,0 +1,94 @@ +driverSetupManagerMock = $this->createMock(DriverSetupManager::class); + $this->driverFactoryMock = $this->createMock(SymfonyDriverFactory::class); + $this->sessionMock = $this->createMock(SymfonySession::class); + $this->transactionMock = $this->createMock(SymfonyTransaction::class); + + $this->sessionConfig = new SessionConfiguration(); + $this->transactionConfig = new TransactionConfiguration(); + + $this->driverSetupManagerMock + ->method('getDefaultAlias') + ->willReturn('default'); + + $this->client = new SymfonyClient( + $this->driverSetupManagerMock, + $this->sessionConfig, + $this->transactionConfig, + $this->driverFactoryMock + ); + } + + public function testRunStatement() + { + $statement = Statement::create('MATCH (n) RETURN n'); + $cypherMapMock = $this->createMock(CypherMap::class); + $summarizedResultMock = $this->createMock(SummarizedResult::class); + + $this->sessionMock + ->expects($this->once()) + ->method('runStatements') + ->with([$statement], $this->transactionConfig) + ->willReturn(new CypherList([$summarizedResultMock])); + + + $reflection = new \ReflectionClass($this->client); + $boundSessions = $reflection->getProperty('boundSessions'); + $boundSessions->setValue($this->client, ['default' => $this->sessionMock]); + + $result = $this->client->runStatement($statement); + + $this->assertInstanceOf(SummarizedResult::class, $result); + } + public function testWriteTransaction() + { + $expectedResult = 'transaction success'; + + $this->sessionMock + ->expects($this->once()) + ->method('writeTransaction') + ->willReturnCallback(function ($tsxHandler) { + return $tsxHandler($this->transactionMock); + }); + + $reflection = new \ReflectionClass($this->client); + $boundSessions = $reflection->getProperty('boundSessions'); + $boundSessions->setValue($this->client, ['default' => $this->sessionMock]); + + $result = $this->client->writeTransaction(fn ($tsx) => 'transaction success'); + + $this->assertEquals($expectedResult, $result); + } + + +} diff --git a/tests/Unit/Decorators/SymfonyDriverTest.php b/tests/Unit/Decorators/SymfonyDriverTest.php new file mode 100644 index 0000000..1e689a8 --- /dev/null +++ b/tests/Unit/Decorators/SymfonyDriverTest.php @@ -0,0 +1,67 @@ +driverMock = $this->createMock(Driver::class); + $this->factoryMock = $this->createMock(SymfonyDriverFactory::class); + + $this->symfonyDriver = new SymfonyDriver( + $this->driverMock, + $this->factoryMock, + $this->alias, + $this->schema + ); + } + + public function testCreateSession() + { + $sessionMock = $this->createMock(SymfonySession::class); + $configMock = $this->createMock(SessionConfiguration::class); + + $this->factoryMock + ->expects($this->once()) + ->method('createSession') + ->with($this->driverMock, $configMock, $this->alias, $this->schema) + ->willReturn($sessionMock); + + $session = $this->symfonyDriver->createSession($configMock); + $this->assertInstanceOf(SymfonySession::class, $session); + } + + public function testVerifyConnectivity() + { + $this->driverMock + ->expects($this->once()) + ->method('verifyConnectivity') + ->willReturn(true); + + $this->assertTrue($this->symfonyDriver->verifyConnectivity()); + } + + public function testCloseConnections() + { + $this->driverMock + ->expects($this->once()) + ->method('closeConnections'); + + $this->symfonyDriver->closeConnections(); + } +} diff --git a/tests/Unit/Decorators/SymfonySessionTest.php b/tests/Unit/Decorators/SymfonySessionTest.php new file mode 100644 index 0000000..e0aaa94 --- /dev/null +++ b/tests/Unit/Decorators/SymfonySessionTest.php @@ -0,0 +1,133 @@ +sessionMock = $this->createMock(Session::class); + $this->handlerMock = $this->createMock(EventHandler::class); + $this->factoryMock = $this->createMock(SymfonyDriverFactory::class); + + + $this->symfonySession = new SymfonySession( + $this->sessionMock, + $this->handlerMock, + $this->factoryMock, + $this->alias, + $this->schema + ); + } + + public function testRunStatement() + { + $statementMock = $this->createMock(Statement::class); + $resultMock = $this->createMock(SummarizedResult::class); + + + $this->handlerMock + ->expects($this->once()) + ->method('handleQuery') + ->with( + $this->callback(fn($callback) => is_callable($callback)), + $statementMock, + $this->alias, + $this->schema, + null + ) + ->willReturn($resultMock); + + $result = $this->symfonySession->runStatement($statementMock); + $this->assertInstanceOf(SummarizedResult::class, $result); + } + + public function testRunStatements() + { + $statementMock1 = $this->createMock(Statement::class); + $statementMock2 = $this->createMock(Statement::class); + $resultMock1 = $this->createMock(SummarizedResult::class); + $resultMock2 = $this->createMock(SummarizedResult::class); + + + $this->handlerMock + ->method('handleQuery') + ->willReturnOnConsecutiveCalls($resultMock1, $resultMock2); + + $result = $this->symfonySession->runStatements([$statementMock1, $statementMock2]); + $this->assertInstanceOf(CypherList::class, $result); + $this->assertCount(2, $result); + } + + + public function testBeginTransaction() + { + $transactionMock = $this->createMock(SymfonyTransaction::class); + + + $this->factoryMock + ->expects($this->once()) + ->method('createTransaction') + ->with( + $this->sessionMock, + $this->anything(), + $this->alias, + $this->schema + ) + ->willReturn($transactionMock); + + $transaction = $this->symfonySession->beginTransaction(); + + $this->assertInstanceOf(SymfonyTransaction::class, $transaction); + } + + public function testWriteTransaction() + { + $transactionMock = $this->createMock(SymfonyTransaction::class); + + $this->factoryMock + ->expects($this->once()) + ->method('createTransaction') + ->willReturn($transactionMock); + + $handler = function ($tsx) { + return 'transaction success'; + }; + + $result = $this->symfonySession->writeTransaction($handler); + + $this->assertEquals('transaction success', $result); + } + + public function testGetLastBookmark() + { + $bookmarkMock = $this->createMock(Bookmark::class); + + $this->sessionMock + ->expects($this->once()) + ->method('getLastBookmark') + ->willReturn($bookmarkMock); + + $bookmark = $this->symfonySession->getLastBookmark(); + $this->assertInstanceOf(Bookmark::class, $bookmark); + } +} diff --git a/tests/Unit/Decorators/SymfonyTransactionTest.php b/tests/Unit/Decorators/SymfonyTransactionTest.php new file mode 100644 index 0000000..96b4650 --- /dev/null +++ b/tests/Unit/Decorators/SymfonyTransactionTest.php @@ -0,0 +1,134 @@ +mockTransaction = $this->createMock(UnmanagedTransactionInterface::class); + $this->mockHandler = $this->createMock(EventHandler::class); + + $this->symfonyTransaction = new SymfonyTransaction( + $this->mockTransaction, + $this->mockHandler, + 'default', + 'bolt', + 'txn-123' + ); + } + + + public function testRun(): void + { + $statement = new Statement('MATCH (n) RETURN n', []); + $mockResult = $this->createMock(SummarizedResult::class); + + $this->mockTransaction + ->expects($this->once()) + ->method('runStatement') + ->with($this->equalTo($statement)) + ->willReturn($mockResult); + + $this->mockHandler + ->expects($this->once()) + ->method('handleQuery') + ->willReturnCallback(function ($callback) use ($statement, $mockResult) { + return $callback($statement); + }); + + $result = $this->symfonyTransaction->run($statement->getText(), $statement->getParameters()); + $this->assertInstanceOf(SummarizedResult::class, $result); + } + public function testCommit(): void + { + $this->mockTransaction + ->expects($this->once()) + ->method('commit') + ->willReturn(new CypherList([])); + $this->mockHandler + ->expects($this->once()) + ->method('handleTransactionAction') + ->with( + TransactionState::COMMITTED, + 'txn-123', + $this->isType('callable'), + 'default', + 'bolt' + ) + ->willReturnCallback(function ($state, $txnId, $callback) { + return $callback(); + }); + + $result = $this->symfonyTransaction->commit(); + + $this->assertInstanceOf(CypherList::class, $result); + $this->assertCount(0, $result); + } + + + public function testRollback(): void + { + $this->mockTransaction + ->expects($this->never()) + ->method('commit'); + + $this->mockHandler + ->expects($this->once()) + ->method('handleTransactionAction') + ->with( + TransactionState::ROLLED_BACK, + 'txn-123', + $this->isType('callable'), + 'default', + 'bolt' + ); + + $this->symfonyTransaction->rollback(); + } + + public function testIsRolledBack(): void + { + $this->mockTransaction + ->expects($this->once()) + ->method('isRolledBack') + ->willReturn(true); + + $this->assertTrue($this->symfonyTransaction->isRolledBack()); + } + + public function testIsCommitted(): void + { + $this->mockTransaction + ->expects($this->once()) + ->method('isCommitted') + ->willReturn(true); + + $this->assertTrue($this->symfonyTransaction->isCommitted()); + } + + public function testIsFinished(): void + { + $this->mockTransaction + ->expects($this->once()) + ->method('isFinished') + ->willReturn(true); + + $this->assertTrue($this->symfonyTransaction->isFinished()); + } +} diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..0e2d4f8 --- /dev/null +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,106 @@ +processConfiguration($configuration, [ + 'neo4j' => [ + 'default_driver' => 'neo4j_driver', + 'drivers' => [ + [ + 'alias' => 'custom_driver', + 'dsn' => 'bolt://custom-host:7687', + 'profiling' => true, + 'authentication' => [ + 'type' => 'basic', + 'username' => 'user', + 'password' => 'password', + ], + 'priority' => 10, + ], + ], + ], + ]); + + $this->assertSame('neo4j_driver', $config['default_driver']); + $this->assertCount(1, $config['drivers']); + $this->assertSame('custom_driver', $config['drivers'][0]['alias']); + $this->assertSame('bolt://custom-host:7687', $config['drivers'][0]['dsn']); + $this->assertTrue($config['drivers'][0]['profiling']); + $this->assertSame(10, $config['drivers'][0]['priority']); + + $this->assertSame('basic', $config['drivers'][0]['authentication']['type']); + $this->assertSame('user', $config['drivers'][0]['authentication']['username']); + $this->assertSame('password', $config['drivers'][0]['authentication']['password']); + } + + public function testSessionConfiguration(): void + { + $processor = new Processor(); + $configuration = new Configuration(); + + $config = $processor->processConfiguration($configuration, [ + 'neo4j' => [ + 'default_session_config' => [ + 'fetch_size' => 500, + 'access_mode' => 'read', + 'database' => 'testdb', + ], + ], + ]); + + $this->assertSame(500, $config['default_session_config']['fetch_size']); + $this->assertSame('read', $config['default_session_config']['access_mode']); + $this->assertSame('testdb', $config['default_session_config']['database']); + } + + public function testTransactionConfiguration(): void + { + $processor = new Processor(); + $configuration = new Configuration(); + + $config = $processor->processConfiguration($configuration, [ + 'neo4j' => [ + 'default_transaction_config' => [ + 'timeout' => 300, + ], + ], + ]); + + $this->assertSame(300, $config['default_transaction_config']['timeout']); + } + + public function testSslConfiguration(): void + { + $processor = new Processor(); + $configuration = new Configuration(); + + $config = $processor->processConfiguration($configuration, [ + 'neo4j' => [ + 'default_driver_config' => [ + 'ssl' => [ + 'mode' => 'enable', + 'verify_peer' => false, + ], + ], + ], + ]); + + $this->assertSame('enable', $config['default_driver_config']['ssl']['mode']); + $this->assertFalse($config['default_driver_config']['ssl']['verify_peer']); + } +} diff --git a/tests/Unit/DependencyInjection/Neo4jExtensionTest.php b/tests/Unit/DependencyInjection/Neo4jExtensionTest.php new file mode 100644 index 0000000..f3deb6e --- /dev/null +++ b/tests/Unit/DependencyInjection/Neo4jExtensionTest.php @@ -0,0 +1,83 @@ +setParameter('kernel.debug', false); + + $extension = new Neo4jExtension(); + $configs = [ + 'default_driver' => 'neo4j_default', + 'min_log_level' => 'error', + 'drivers' => [ + ['alias' => 'default', 'dsn' => 'bolt://localhost:7687', 'profiling' => null], + ], + ]; + + $extension->load([$configs], $container); + + $this->assertTrue($container->hasDefinition('neo4j.event_handler')); + $this->assertTrue($container->hasDefinition('neo4j.client_factory')); + $this->assertTrue($container->hasDefinition('neo4j.driver.default')); + + // Check event handler argument + $eventHandlerDefinition = $container->getDefinition('neo4j.event_handler'); + $this->assertEquals('neo4j_default', $eventHandlerDefinition->getArgument(1)); + + // Check client factory arguments + $clientFactoryDefinition = $container->getDefinition('neo4j.client_factory'); + $this->assertEquals('error', $clientFactoryDefinition->getArgument('$logLevel')); + } + + public function testDriversAreRegistered(): void + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + + $extension = new Neo4jExtension(); + $configs = [ + 'drivers' => [ + ['alias' => 'main', 'dsn' => 'bolt://localhost:7687', 'profiling' => true], + ['alias' => 'backup', 'dsn' => 'bolt://backup:7687', 'profiling' => false], + ], + ]; + + $extension->load([$configs], $container); + + $this->assertTrue($container->hasDefinition('neo4j.driver.main')); + $this->assertTrue($container->hasDefinition('neo4j.driver.backup')); + } + + public function testProfilingEnabledForDebug(): void + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + + $extension = new Neo4jExtension(); + $configs = [ + 'drivers' => [ + ['alias' => 'default', 'dsn' => 'bolt://localhost:7687', 'profiling' => null], + ], + ]; + + $extension->load([$configs], $container); + + $this->assertTrue($container->hasDefinition('neo4j.data_collector')); + $this->assertTrue($container->hasDefinition('neo4j.subscriber')); + + $subscriberDefinition = $container->getDefinition('neo4j.subscriber'); + $this->assertEquals(['default'], $subscriberDefinition->getArgument(0)); + } +} diff --git a/tests/Unit/Event/FailureEventTest.php b/tests/Unit/Event/FailureEventTest.php new file mode 100644 index 0000000..f4c13a8 --- /dev/null +++ b/tests/Unit/Event/FailureEventTest.php @@ -0,0 +1,65 @@ +createMock(Statement::class); + $exception = $this->createMock(Neo4jException::class); + $time = new \DateTimeImmutable(); + $scheme = 'bolt'; + $transactionId = '1234'; + + $event = new FailureEvent( + alias: $alias, + statement: $statement, + exception: $exception, + time: $time, + scheme: $scheme, + transactionId: $transactionId + ); + + $this->assertSame($alias, $event->alias); + $this->assertSame($statement, $event->statement); + $this->assertSame($exception, $event->exception); + $this->assertSame($time, $event->time); + $this->assertSame($scheme, $event->scheme); + $this->assertSame($transactionId, $event->transactionId); + } + + public function testDisableException(): void + { + $event = new FailureEvent( + alias: 'test_alias', + statement: null, + exception: $this->createMock(Neo4jException::class), + time: new \DateTimeImmutable(), + scheme: 'bolt', + transactionId: '1234' + ); + + $this->assertTrue($this->getShouldThrowException($event)); + + $event->disableException(); + + $this->assertFalse($this->getShouldThrowException($event)); + } + + private function getShouldThrowException(FailureEvent $event): bool + { + $reflection = new \ReflectionClass(FailureEvent::class); + $property = $reflection->getProperty('shouldThrowException'); + + return $property->getValue($event); + } +} diff --git a/tests/Unit/EventListener/Neo4jProfileListenerTest.php b/tests/Unit/EventListener/Neo4jProfileListenerTest.php new file mode 100644 index 0000000..3a4822d --- /dev/null +++ b/tests/Unit/EventListener/Neo4jProfileListenerTest.php @@ -0,0 +1,90 @@ +createMock(ResultSummary::class); + $resultMock->method('getResultAvailableAfter')->willReturn(10.0); + $resultMock->method('getResultConsumedAfter')->willReturn(5.0); + + $time = new \DateTimeImmutable(); + $scheme = 'bolt'; + $transactionId = 'tx123'; + + $event = new PostRunEvent($alias, $resultMock, $time, $scheme, $transactionId); + + $listener->onPostRun($event); + + $profiledSummaries = $listener->getProfiledSummaries(); + + $this->assertCount(1, $profiledSummaries); + $this->assertSame($resultMock, $profiledSummaries[0]['result']); + $this->assertSame($alias, $profiledSummaries[0]['alias']); + $this->assertSame($scheme, $profiledSummaries[0]['scheme']); + $this->assertSame($transactionId, $profiledSummaries[0]['transaction_id']); + $this->assertSame($time->format('Y-m-d H:i:s'), $profiledSummaries[0]['time']); + } + + + public function testOnFailure(): void + { + $enabledProfiles = ['default']; + $listener = new Neo4jProfileListener($enabledProfiles); + + $alias = 'default'; + $statementMock = $this->createMock(Statement::class); + $exceptionMock = $this->createMock(Neo4jException::class); + $time = new \DateTimeImmutable(); + + $event = new FailureEvent($alias, $statementMock, $exceptionMock, $time, 'bolt', 'tx123'); + + $listener->onFailure($event); + + $profiledFailures = $listener->getProfiledFailures(); + + $this->assertCount(1, $profiledFailures); + $this->assertSame($exceptionMock, $profiledFailures[0]['exception']); + $this->assertSame($statementMock, $profiledFailures[0]['statement']); + $this->assertSame($alias, $profiledFailures[0]['alias']); + $this->assertSame($time->format('Y-m-d H:i:s'), $profiledFailures[0]['time']); + } + + public function testReset(): void + { + $enabledProfiles = ['default']; + $listener = new Neo4jProfileListener($enabledProfiles); + + $resultMock = $this->createMock(ResultSummary::class); + $exceptionMock = $this->createMock(Neo4jException::class); + $statementMock = $this->createMock(Statement::class); + $time = new \DateTimeImmutable(); + + $listener->onPostRun(new PostRunEvent('default', $resultMock, $time, 'bolt', 'tx123')); + $listener->onFailure(new FailureEvent('default', $statementMock, $exceptionMock, $time, 'bolt', 'tx123')); + + $this->assertNotEmpty($listener->getProfiledSummaries()); + $this->assertNotEmpty($listener->getProfiledFailures()); + + $listener->reset(); + + $this->assertEmpty($listener->getProfiledSummaries()); + $this->assertEmpty($listener->getProfiledFailures()); + } +} diff --git a/tests/Unit/EventhandlerTest.php b/tests/Unit/EventhandlerTest.php new file mode 100644 index 0000000..d410dbc --- /dev/null +++ b/tests/Unit/EventhandlerTest.php @@ -0,0 +1,78 @@ +dispatcher = $this->createMock(EventDispatcherInterface::class); + $this->stopwatch = $this->createMock(Stopwatch::class); + $this->nameFactory = $this->createMock(StopwatchEventNameFactory::class); + + $this->eventHandler = new EventHandler( + dispatcher: $this->dispatcher, + stopwatch: $this->stopwatch, + nameFactory: $this->nameFactory + ); + } + + public function testHandleQuery(): void + { + $statement = $this->createMock(Statement::class); + $summary = $this->createMock(SummarizedResult::class); + $resultSummary = $this->createMock(\Laudis\Neo4j\Databags\ResultSummary::class); + + $summary->method('getSummary')->willReturn($resultSummary); + + $runHandler = fn() => $summary; + + $this->nameFactory->method('createQueryEventName')->willReturn('query_event'); + + $this->dispatcher->expects($this->exactly(2))->method('dispatch')->withConsecutive( + [$this->isInstanceOf(PreRunEvent::class), PreRunEvent::EVENT_ID], + [$this->isInstanceOf(PostRunEvent::class), PostRunEvent::EVENT_ID] + ); + + $this->stopwatch->expects($this->once())->method('start'); + $this->stopwatch->expects($this->once())->method('stop'); + + $result = $this->eventHandler->handleQuery($runHandler, $statement, 'alias', 'scheme', 'txId'); + + $this->assertSame($summary, $result); + } + + + public function testHandleTransactionAction(): void + { + $runHandler = fn() => 'result'; + + $this->nameFactory->method('createTransactionEventName')->willReturn('tx_event'); + + $this->dispatcher->expects($this->exactly(2))->method('dispatch'); + + $result = $this->eventHandler->handleTransactionAction(TransactionState::COMMITTED, 'txId', $runHandler, 'alias', 'scheme'); + + $this->assertSame('result', $result); + } +} diff --git a/tests/Unit/Factories/StopwatchEventNameFactoryTest.php b/tests/Unit/Factories/StopwatchEventNameFactoryTest.php new file mode 100644 index 0000000..81bbeca --- /dev/null +++ b/tests/Unit/Factories/StopwatchEventNameFactoryTest.php @@ -0,0 +1,57 @@ +factory = new StopwatchEventNameFactory(); + } + + public function testCreateQueryEventNameWithoutTransaction(): void + { + $alias = 'database1'; + $transactionId = null; + $expected = 'neo4j.database1.query'; + + $this->assertSame($expected, $this->factory->createQueryEventName($alias, $transactionId)); + } + + public function testCreateQueryEventNameWithTransaction(): void + { + $alias = 'database1'; + $transactionId = 'tx123'; + $expected = 'neo4j.database1.transaction.tx123.query'; + + $this->assertSame($expected, $this->factory->createQueryEventName($alias, $transactionId)); + } + + /** + * @dataProvider transactionEventNameProvider + */ + public function testCreateTransactionEventName(TransactionState $state, string $expectedAction): void + { + $alias = 'database1'; + $transactionId = 'tx123'; + $expected = sprintf('neo4j.%s.transaction.%s.%s', $alias, $transactionId, $expectedAction); + + $this->assertSame($expected, $this->factory->createTransactionEventName($alias, $transactionId, $state)); + } + + public function transactionEventNameProvider(): array + { + return [ + [TransactionState::COMMITTED, 'commit'], + [TransactionState::ACTIVE, 'begin'], + [TransactionState::ROLLED_BACK, 'rollback'], + [TransactionState::TERMINATED, 'error'], + ]; + } +} diff --git a/tests/Unit/Factories/SymfonyDriverFactoryTest.php b/tests/Unit/Factories/SymfonyDriverFactoryTest.php new file mode 100644 index 0000000..b554db2 --- /dev/null +++ b/tests/Unit/Factories/SymfonyDriverFactoryTest.php @@ -0,0 +1,109 @@ +eventHandler = $this->createMock(EventHandler::class); + $this->uuidFactory = $this->createMock(UuidFactory::class); + $this->factory = new SymfonyDriverFactory($this->eventHandler, $this->uuidFactory); + } + + public function testCreateTransaction(): void + { + $session = $this->createMock(Session::class); + $transactionConfig = $this->createMock(TransactionConfiguration::class); + $alias = 'test_alias'; + $schema = 'test_schema'; + + $symfonyTransaction = $this->createMock(SymfonyTransaction::class); + + $this->eventHandler + ->expects($this->once()) + ->method('handleTransactionAction') + ->with(TransactionState::ACTIVE) + ->willReturn($symfonyTransaction); + + $transaction = $this->factory->createTransaction($session, $transactionConfig, $alias, $schema); + + $this->assertInstanceOf(SymfonyTransaction::class, $transaction); + } + + public function testCreateSession(): void + { + $driver = $this->createMock(Driver::class); + $sessionConfig = $this->createMock(SessionConfiguration::class); + $alias = 'test_alias'; + $schema = 'test_schema'; + + $driver->expects($this->once()) + ->method('createSession') + ->with($sessionConfig) + ->willReturn($this->createMock(Session::class)); + + $session = $this->factory->createSession($driver, $sessionConfig, $alias, $schema); + + $this->assertInstanceOf(SymfonySession::class, $session); + } + + public function testCreateDriver(): void + { + $driver = $this->createMock(Driver::class); + $alias = 'test_alias'; + $schema = 'test_schema'; + + $symfonyDriver = $this->factory->createDriver($driver, $alias, $schema); + + $this->assertInstanceOf(SymfonyDriver::class, $symfonyDriver); + } + + public function testGenerateTransactionIdWithUuidFactory(): void + { + $uuid = $this->createMock(Uuid::class); + $uuid->method('toRfc4122')->willReturn('test-uuid'); + + $this->uuidFactory->method('create')->willReturn($uuid); + + $reflection = new \ReflectionClass(SymfonyDriverFactory::class); + $method = $reflection->getMethod('generateTransactionId'); + + $id = $method->invoke($this->factory); + + $this->assertSame('test-uuid', $id); + } + + /** + * @throws \ReflectionException + */ + public function testGenerateTransactionIdWithoutUuidFactory(): void + { + $factory = new SymfonyDriverFactory($this->eventHandler, null); + + $reflection = new \ReflectionClass(SymfonyDriverFactory::class); + $method = $reflection->getMethod('generateTransactionId'); + + $id = $method->invoke($factory); + + $this->assertMatchesRegularExpression('/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/', $id); + } +}