diff --git a/.env.example b/.env.example index a9644d43..c1e3042e 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -CONNECTIONS=bolt://neo4j:testtest@neo4j,http://neo4j:testtest@neo4j,bolt://neo4j:testtest@core1 +PHP_VERSION=8.1 diff --git a/.github/workflows/integration-test-cluster-neo4j-4.yml b/.github/workflows/integration-test-cluster-neo4j-4.yml index e1782258..e2a164f8 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -14,9 +14,6 @@ jobs: strategy: matrix: php: ['8.1.31', '8.3.17'] - env: - PHP_VERSION: ${{ matrix.php }} - CONNECTION: neo4j://neo4j:testtest@localhost:7688 name: "Running on PHP ${{ matrix.php }} in a Neo4j 4.4 cluster" steps: @@ -24,7 +21,7 @@ jobs: - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env - echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env + echo "CONNECTION=neo4j://neo4j:testtest@core1" >> .env - uses: hoverkraft-tech/compose-action@v2.0.2 name: Start services with: diff --git a/.github/workflows/integration-test-cluster-neo4j-5.yml b/.github/workflows/integration-test-cluster-neo4j-5.yml index 0fd98a25..8fe9402c 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -14,9 +14,6 @@ jobs: strategy: matrix: php: ['8.1.31', '8.3.17'] - env: - PHP_VERSION: ${{ matrix.php }} - CONNECTION: neo4j://neo4j:testtest@localhost:7687 name: "Running on PHP ${{ matrix.php }} with a Neo4j 5.20-enterprise cluster" steps: @@ -24,7 +21,7 @@ jobs: - name: Populate .env run: | echo "PHP_VERSION=${{ matrix.php }}" > .env - echo "CONNECTION=neo4j://neo4j:testtest@neo4j" >> .env + echo "CONNECTION=neo4j://neo4j:testtest@server1" >> .env - uses: hoverkraft-tech/compose-action@v2.0.2 name: Start services with: diff --git a/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php index 2b711146..7e5cd742 100644 --- a/src/Authentication/Authenticate.php +++ b/src/Authentication/Authenticate.php @@ -35,13 +35,13 @@ final class Authenticate */ public static function basic(string $username, string $password, ?Neo4jLogger $logger = null): BasicAuth { + /** @psalm-suppress ImpureMethodCall Uri is a pure object */ + return new BasicAuth($username, $password, $logger); } /** * Authenticate using a kerberos token. - * - * @pure */ public static function kerberos(string $token, ?Neo4jLogger $logger = null): KerberosAuth { @@ -50,8 +50,6 @@ public static function kerberos(string $token, ?Neo4jLogger $logger = null): Ker /** * Authenticate using a OpenID Connect token. - * - * @pure */ public static function oidc(string $token, ?Neo4jLogger $logger = null): OpenIDConnectAuth { @@ -60,8 +58,6 @@ public static function oidc(string $token, ?Neo4jLogger $logger = null): OpenIDC /** * Don't authenticate at all. - * - * @pure */ public static function disabled(?Neo4jLogger $logger = null): NoAuth { @@ -70,8 +66,6 @@ public static function disabled(?Neo4jLogger $logger = null): NoAuth /** * Authenticate from information found in the url. - * - * @pure */ public static function fromUrl(UriInterface $uri, ?Neo4jLogger $logger = null): AuthenticateInterface { diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 82ef2d65..a1d7bdcc 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -20,20 +20,17 @@ use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\UriInterface; -use Psr\Log\LogLevel; /** * Authenticates connections using a basic username and password. */ final class BasicAuth implements AuthenticateInterface { - /** - * @psalm-external-mutation-free - */ public function __construct( private readonly string $username, private readonly string $password, @@ -48,36 +45,60 @@ public function __construct( */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { + $factory = $this->createMessageFactory($protocol); + if (method_exists($protocol, 'logon')) { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $protocol->hello(['user_agent' => $userAgent]); + $helloMetadata = ['user_agent' => $userAgent]; + + $factory->createHelloMessage($helloMetadata)->send(); $response = ResponseHelper::getResponse($protocol); - $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'basic', 'principal' => $this->username]); - $protocol->logon([ + + $credentials = [ 'scheme' => 'basic', 'principal' => $this->username, 'credentials' => $this->password, - ]); + ]; + + $factory->createLogonMessage($credentials)->send(); ResponseHelper::getResponse($protocol); /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; - } else { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'basic', 'principal' => $this->username]); - $protocol->hello([ - 'user_agent' => $userAgent, - 'scheme' => 'basic', - 'principal' => $this->username, - 'credentials' => $this->password, - ]); - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; } + + $helloMetadata = [ + 'user_agent' => $userAgent, + 'scheme' => 'basic', + 'principal' => $this->username, + 'credentials' => $this->password, + ]; + + $factory->createHelloMessage($helloMetadata)->send(); + + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } + + /** + * @throws Exception + */ + public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void + { + $factory = $this->createMessageFactory($protocol); + $factory->createLogoffMessage()->send(); + ResponseHelper::getResponse($protocol); } public function toString(UriInterface $uri): string { return sprintf('Basic %s:%s@%s:%s', $this->username, '######', $uri->getHost(), $uri->getPort() ?? ''); } + + /** + * Helper to create message factory. + */ + private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + { + return new BoltMessageFactory($protocol, $this->logger); + } } diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 51656d65..e50a27dc 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -20,6 +20,7 @@ use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; @@ -34,9 +35,6 @@ */ final class KerberosAuth implements AuthenticateInterface { - /** - * @psalm-external-mutation-free - */ public function __construct( private readonly string $token, private readonly ?Neo4jLogger $logger, @@ -47,11 +45,6 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s { $this->logger?->log(LogLevel::DEBUG, 'Authenticating using KerberosAuth'); - /** - * @psalm-suppress ImpureMethodCall Request is a pure object: - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects - */ return $request->withHeader('Authorization', 'Kerberos '.$this->token) ->withHeader('User-Agent', $userAgent); } @@ -63,36 +56,40 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - if (method_exists($protocol, 'logon')) { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $protocol->hello(['user_agent' => $userAgent]); - $response = ResponseHelper::getResponse($protocol); - $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']); - $protocol->logon([ - 'scheme' => 'kerberos', - 'principal' => '', - 'credentials' => $this->token, - ]); - ResponseHelper::getResponse($protocol); - - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; - } else { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'kerberos', 'principal' => '']); - $protocol->hello([ - 'user_agent' => $userAgent, - 'scheme' => 'kerberos', - 'principal' => '', - 'credentials' => $this->token, - ]); - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; - } + $factory = $this->createMessageFactory($protocol); + + $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); + + $factory->createHelloMessage(['user_agent' => $userAgent])->send(); + + $response = ResponseHelper::getResponse($protocol); + + $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']); + + $factory->createLogonMessage([ + 'scheme' => 'kerberos', + 'principal' => '', + 'credentials' => $this->token, + ])->send(); + + ResponseHelper::getResponse($protocol); + + /** + * @var array{server: string, connection_id: string, hints: list} + */ + return $response->content; } public function toString(UriInterface $uri): string { return sprintf('Kerberos %s@%s:%s', $this->token, $uri->getHost(), $uri->getPort() ?? ''); } + + /** + * Helper to create the message factory. + */ + private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + { + return new BoltMessageFactory($protocol, $this->logger); + } } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 69d7ad07..80b1b1b9 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -20,6 +20,7 @@ use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; @@ -29,14 +30,8 @@ use function sprintf; -/** - * Doesn't authenticate connections. - */ final class NoAuth implements AuthenticateInterface { - /** - * @pure - */ public function __construct( private readonly ?Neo4jLogger $logger, ) { @@ -46,11 +41,6 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s { $this->logger?->log(LogLevel::DEBUG, 'Authentication disabled'); - /** - * @psalm-suppress ImpureMethodCall Request is a pure object: - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects - */ return $request->withHeader('User-Agent', $userAgent); } @@ -61,32 +51,46 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { + $factory = $this->createMessageFactory($protocol); + if (method_exists($protocol, 'logon')) { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $protocol->hello(['user_agent' => $userAgent]); + $helloMetadata = ['user_agent' => $userAgent]; + + $factory->createHelloMessage($helloMetadata)->send(); $response = ResponseHelper::getResponse($protocol); - $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'none']); - $protocol->logon([ - 'scheme' => 'none', - ]); + + $factory->createLogonMessage(['scheme' => 'none'])->send(); ResponseHelper::getResponse($protocol); /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; - } else { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'none']); - $protocol->hello([ - 'user_agent' => $userAgent, - 'scheme' => 'none', - ]); - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; } + + $helloMetadata = [ + 'user_agent' => $userAgent, + 'scheme' => 'none', + ]; + + $factory->createHelloMessage($helloMetadata)->send(); + + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } + + public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void + { + $factory = $this->createMessageFactory($protocol); + $factory->createLogoffMessage()->send(); + ResponseHelper::getResponse($protocol); } public function toString(UriInterface $uri): string { return sprintf('No Auth %s:%s', $uri->getHost(), $uri->getPort() ?? ''); } + + private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + { + return new BoltMessageFactory($protocol, $this->logger); + } } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index c7421cbf..bb7a134f 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -20,6 +20,7 @@ use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; @@ -29,11 +30,8 @@ use function sprintf; -final class OpenIDConnectAuth implements AuthenticateInterface +class OpenIDConnectAuth implements AuthenticateInterface { - /** - * @psalm-external-mutation-free - */ public function __construct( private readonly string $token, private readonly ?Neo4jLogger $logger, @@ -44,11 +42,6 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s { $this->logger?->log(LogLevel::DEBUG, 'Authenticating using OpenIDConnectAuth'); - /** - * @psalm-suppress ImpureMethodCall Request is a pure object: - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects - */ return $request->withHeader('Authorization', 'Bearer '.$this->token) ->withHeader('User-Agent', $userAgent); } @@ -60,34 +53,39 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - if (method_exists($protocol, 'logon')) { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $protocol->hello(['user_agent' => $userAgent]); - $response = ResponseHelper::getResponse($protocol); - $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']); - $protocol->logon([ - 'scheme' => 'bearer', - 'credentials' => $this->token, - ]); - ResponseHelper::getResponse($protocol); - - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; - } else { - $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'bearer']); - $protocol->hello([ - 'user_agent' => $userAgent, - 'scheme' => 'bearer', - 'credentials' => $this->token, - ]); - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; - } + $factory = $this->createMessageFactory($protocol); + + $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); + + $factory->createHelloMessage(['user_agent' => $userAgent])->send(); + + $response = ResponseHelper::getResponse($protocol); + + $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']); + + $factory->createLogonMessage([ + 'scheme' => 'bearer', + 'credentials' => $this->token, + ])->send(); + + ResponseHelper::getResponse($protocol); + + /** + * @var array{server: string, connection_id: string, hints: list} + */ + return $response->content; } public function toString(UriInterface $uri): string { return sprintf('OpenId %s@%s:%s', $this->token, $uri->getHost(), $uri->getPort() ?? ''); } + + /** + * Helper to create the message factory. + */ + public function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + { + return new BoltMessageFactory($protocol, $this->logger); + } } diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 07a41a48..d4bbd057 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -47,6 +47,8 @@ */ class BoltConnection implements ConnectionInterface { + private BoltMessageFactory $messageFactory; + /** * @note We are using references to "subscribed results" to maintain backwards compatibility and try and strike * a balance between performance and ease of use. @@ -82,6 +84,7 @@ public function __construct( private readonly ConnectionConfiguration $config, private readonly ?Neo4jLogger $logger, ) { + $this->messageFactory = new BoltMessageFactory($this->protocol(), $this->logger); } public function getEncryptionLevel(): string @@ -196,10 +199,8 @@ public function consumeResults(): void */ public function reset(): void { - $this->logger?->log(LogLevel::DEBUG, 'RESET'); - $response = $this->protocol() - ->reset() - ->getResponse(); + $message = $this->messageFactory->createResetMessage(); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); $this->subscribedResults = []; } @@ -214,10 +215,8 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder $this->consumeResults(); $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); - $this->logger?->log(LogLevel::DEBUG, 'BEGIN', $extra); - $response = $this->protocol() - ->begin($extra) - ->getResponse(); + $message = $this->messageFactory->createBeginMessage($extra); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); } @@ -229,10 +228,9 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder public function discard(?int $qid): void { $extra = $this->buildResultExtra(null, $qid); - $this->logger?->log(LogLevel::DEBUG, 'DISCARD', $extra); - $response = $this->protocol() - ->discard($extra) - ->getResponse(); + + $message = $this->messageFactory->createDiscardMessage($extra); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); } @@ -252,10 +250,8 @@ public function run( ?AccessMode $mode, ): array { $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); - $this->logger?->log(LogLevel::DEBUG, 'RUN', $extra); - $response = $this->protocol() - ->run($text, $parameters, $extra) - ->getResponse(); + $message = $this->messageFactory->createRunMessage($text, $parameters, $extra); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); /** @var BoltMeta */ @@ -269,12 +265,10 @@ public function run( */ public function commit(): void { - $this->logger?->log(LogLevel::DEBUG, 'COMMIT'); $this->consumeResults(); - $response = $this->protocol() - ->commit() - ->getResponse(); + $message = $this->messageFactory->createCommitMessage(); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); } @@ -285,12 +279,10 @@ public function commit(): void */ public function rollback(): void { - $this->logger?->log(LogLevel::DEBUG, 'ROLLBACK'); $this->consumeResults(); - $response = $this->protocol() - ->rollback() - ->getResponse(); + $message = $this->messageFactory->createRollbackMessage(); + $response = $message->send()->getResponse(); $this->assertNoFailure($response); } @@ -316,8 +308,9 @@ public function pull(?int $qid, ?int $fetchSize): array $this->logger?->log(LogLevel::DEBUG, 'PULL', $extra); $tbr = []; - /** @var Response $response */ - foreach ($this->protocol()->pull($extra)->getResponses() as $response) { + $message = $this->messageFactory->createPullMessage($extra); + + foreach ($message->send()->getResponses() as $response) { $this->assertNoFailure($response); $tbr[] = $response->content; } @@ -339,8 +332,8 @@ public function close(): void $this->consumeResults(); } - $this->logger?->log(LogLevel::DEBUG, 'GOODBYE'); - $this->protocol()->goodbye(); + $message = $this->messageFactory->createGoodbyeMessage(); + $message->send(); unset($this->boltProtocol); // has to be set to null as the sockets don't recover nicely contrary to what the underlying code might lead you to believe; } @@ -406,7 +399,8 @@ private function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { $this->logger?->log(LogLevel::ERROR, 'FAILURE'); - $resetResponse = $this->protocol()->reset()->getResponse(); + $message = $this->messageFactory->createResetMessage(); + $resetResponse = $message->send()->getResponse(); $this->subscribedResults = []; if ($resetResponse->signature === Signature::FAILURE) { throw new Neo4jException([Neo4jError::fromBoltResponse($resetResponse), Neo4jError::fromBoltResponse($response)]); diff --git a/src/Bolt/BoltMessageFactory.php b/src/Bolt/BoltMessageFactory.php new file mode 100644 index 00000000..589ed672 --- /dev/null +++ b/src/Bolt/BoltMessageFactory.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\Messages\BoltBeginMessage; +use Laudis\Neo4j\Bolt\Messages\BoltCommitMessage; +use Laudis\Neo4j\Bolt\Messages\BoltDiscardMessage; +use Laudis\Neo4j\Bolt\Messages\BoltGoodbyeMessage; +use Laudis\Neo4j\Bolt\Messages\BoltHelloMessage; +use Laudis\Neo4j\Bolt\Messages\BoltLogoffMessage; +use Laudis\Neo4j\Bolt\Messages\BoltLogonMessage; +use Laudis\Neo4j\Bolt\Messages\BoltPullMessage; +use Laudis\Neo4j\Bolt\Messages\BoltResetMessage; +use Laudis\Neo4j\Bolt\Messages\BoltRollbackMessage; +use Laudis\Neo4j\Bolt\Messages\BoltRunMessage; +use Laudis\Neo4j\Common\Neo4jLogger; + +/** + * Factory class for creating Bolt protocol messages. + */ +class BoltMessageFactory +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger = null, + ) { + } + + public function createResetMessage(): BoltResetMessage + { + return new BoltResetMessage($this->protocol, $this->logger); + } + + public function createBeginMessage(array $extra): BoltBeginMessage + { + return new BoltBeginMessage($this->protocol, $extra, $this->logger); + } + + public function createDiscardMessage(array $extra): BoltDiscardMessage + { + return new BoltDiscardMessage($this->protocol, $extra, $this->logger); + } + + public function createRunMessage(string $text, array $parameters, array $extra): BoltRunMessage + { + return new BoltRunMessage($this->protocol, $text, $parameters, $extra, $this->logger); + } + + public function createCommitMessage(): BoltCommitMessage + { + return new BoltCommitMessage($this->protocol, $this->logger); + } + + public function createRollbackMessage(): BoltRollbackMessage + { + return new BoltRollbackMessage($this->protocol, $this->logger); + } + + public function createPullMessage(array $extra): BoltPullMessage + { + return new BoltPullMessage($this->protocol, $extra, $this->logger); + } + + public function createHelloMessage(array $extra): BoltHelloMessage + { + /** @var array $extra */ + + return new BoltHelloMessage($this->protocol, $extra, $this->logger); + } + + public function createLogonMessage(array $credentials): BoltLogonMessage + { + /** @var array $credentials */ + + return new BoltLogonMessage($this->protocol, $credentials, $this->logger); + } + + public function createLogoffMessage(): BoltLogoffMessage + { + return new BoltLogoffMessage($this->protocol, $this->logger); + } + + public function createGoodbyeMessage(): BoltGoodbyeMessage + { + return new BoltGoodbyeMessage($this->protocol, $this->logger); + } +} diff --git a/src/Bolt/Messages/BoltBeginMessage.php b/src/Bolt/Messages/BoltBeginMessage.php new file mode 100644 index 00000000..339c2e92 --- /dev/null +++ b/src/Bolt/Messages/BoltBeginMessage.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltBeginMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly array $extra, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltBeginMessage + { + $this->logger?->log(LogLevel::DEBUG, 'BEGIN', $this->extra); + $this->protocol->begin($this->extra); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltCommitMessage.php b/src/Bolt/Messages/BoltCommitMessage.php new file mode 100644 index 00000000..becb54cf --- /dev/null +++ b/src/Bolt/Messages/BoltCommitMessage.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltCommitMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltCommitMessage + { + $this->logger?->log(LogLevel::DEBUG, 'COMMIT'); + $this->protocol->commit(); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltDiscardMessage.php b/src/Bolt/Messages/BoltDiscardMessage.php new file mode 100644 index 00000000..e35adfbc --- /dev/null +++ b/src/Bolt/Messages/BoltDiscardMessage.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltDiscardMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly array $extra, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltDiscardMessage + { + $this->logger?->log(LogLevel::DEBUG, 'DISCARD', $this->extra); + $this->protocol->discard($this->extra); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltGoodbyeMessage.php b/src/Bolt/Messages/BoltGoodbyeMessage.php new file mode 100644 index 00000000..a944cadb --- /dev/null +++ b/src/Bolt/Messages/BoltGoodbyeMessage.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltGoodbyeMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltGoodbyeMessage + { + $this->logger?->log(LogLevel::DEBUG, 'GOODBYE'); + $this->protocol->goodbye(); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltHelloMessage.php b/src/Bolt/Messages/BoltHelloMessage.php new file mode 100644 index 00000000..cbc3d4be --- /dev/null +++ b/src/Bolt/Messages/BoltHelloMessage.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\error\BoltException; +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltHelloMessage extends BoltMessage +{ + /** + * Constructor for the BoltHelloMessage. + * + * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The protocol connection + * @param array $metadata The metadata for the HELLO message (like user agent, supported versions) + * @param Neo4jLogger|null $logger Optional logger for debugging purposes + */ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly array $metadata, + private readonly ?Neo4jLogger $logger = null, + ) { + parent::__construct($protocol); + } + + /** + * Sends the HELLO message to the server. + * + * @throws BoltException + */ + public function send(): BoltHelloMessage + { + $this->logger?->log(LogLevel::DEBUG, 'HELLO', $this->metadata); + + $this->protocol->hello($this->metadata); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltLogoffMessage.php b/src/Bolt/Messages/BoltLogoffMessage.php new file mode 100644 index 00000000..2eda2cd6 --- /dev/null +++ b/src/Bolt/Messages/BoltLogoffMessage.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +/** + * A message that issues a LOGOFF request to the server to terminate the connection. + */ +class BoltLogoffMessage extends BoltMessage +{ + /** + * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version + * @param Neo4jLogger|null $logger Optional logger for logging purposes + */ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger = null, + ) { + parent::__construct($protocol); + } + + /** + * Sends the LOGOFF request to the server to disconnect. + * + * @return BoltLogoffMessage The current instance of the message + */ + public function send(): BoltLogoffMessage + { + $this->logger?->log(LogLevel::DEBUG, 'LOGOFF', []); + /** @psalm-suppress PossiblyUndefinedMethod */ + $this->protocol->logoff(); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltLogonMessage.php b/src/Bolt/Messages/BoltLogonMessage.php new file mode 100644 index 00000000..eeeec0b2 --- /dev/null +++ b/src/Bolt/Messages/BoltLogonMessage.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +/** + * A message that issues a LOGON request to the server for authentication. + */ +final class BoltLogonMessage extends BoltMessage +{ + /** + * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version + * @param array $credentials The credentials for the LOGON request (e.g., username and password) + * @param Neo4jLogger|null $logger Optional logger for logging purposes + */ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly array $credentials, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + /** + * Sends the LOGON request to the server with the provided credentials. + * + * @return BoltLogonMessage The current instance of the message + */ + public function send(): BoltLogonMessage + { + $toLog = $this->credentials; + unset($toLog['credentials']); + + $this->logger?->log(LogLevel::DEBUG, 'LOGON', $toLog); + /** @psalm-suppress PossiblyUndefinedMethod */ + $this->protocol->logon($this->credentials); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltPullMessage.php b/src/Bolt/Messages/BoltPullMessage.php new file mode 100644 index 00000000..ab141786 --- /dev/null +++ b/src/Bolt/Messages/BoltPullMessage.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltPullMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly array $extra, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltPullMessage + { + $this->logger?->log(LogLevel::DEBUG, 'PULL', $this->extra); + $this->protocol->pull($this->extra); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltResetMessage.php b/src/Bolt/Messages/BoltResetMessage.php new file mode 100644 index 00000000..223e3d01 --- /dev/null +++ b/src/Bolt/Messages/BoltResetMessage.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltResetMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltResetMessage + { + $this->logger?->log(LogLevel::DEBUG, 'RESET'); + $this->protocol->reset(); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltRollbackMessage.php b/src/Bolt/Messages/BoltRollbackMessage.php new file mode 100644 index 00000000..170d376c --- /dev/null +++ b/src/Bolt/Messages/BoltRollbackMessage.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltRollbackMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltRollbackMessage + { + $this->logger?->log(LogLevel::DEBUG, 'ROLLBACK'); + $this->protocol->rollback(); + + return $this; + } +} diff --git a/src/Bolt/Messages/BoltRunMessage.php b/src/Bolt/Messages/BoltRunMessage.php new file mode 100644 index 00000000..b07911fe --- /dev/null +++ b/src/Bolt/Messages/BoltRunMessage.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Contracts\BoltMessage; +use Psr\Log\LogLevel; + +final class BoltRunMessage extends BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly string $text, + private readonly array $parameters, + private readonly array $extra, + private readonly ?Neo4jLogger $logger, + ) { + parent::__construct($protocol); + } + + public function send(): BoltRunMessage + { + $this->logger?->log(LogLevel::DEBUG, 'RUN', [ + 'text' => $this->text, + 'parameters' => $this->parameters, + 'extra' => $this->extra, + ]); + $this->protocol->run($this->text, $this->parameters, $this->extra); + + return $this; + } +} diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index 42a79232..aed74592 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -66,9 +66,6 @@ public static function create(?string $logLevel = null, ?LoggerInterface $logger ); } - /** - * @psalm-mutation-free - */ public function withDriver(string $alias, string $url, ?AuthenticateInterface $authentication = null, ?int $priority = 0): self { $uri = Uri::create($url); diff --git a/src/Contracts/BoltMessage.php b/src/Contracts/BoltMessage.php new file mode 100644 index 00000000..02eeeac1 --- /dev/null +++ b/src/Contracts/BoltMessage.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Contracts; + +use Bolt\protocol\Response; +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Iterator; + +abstract class BoltMessage +{ + public function __construct( + private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + ) { + } + + /** + * Sends the Bolt message. + */ + abstract public function send(): BoltMessage; + + public function getResponse(): Response + { + return $this->protocol->getResponse(); + } + + /** + * @return Iterator + */ + public function getResponses(): Iterator + { + /** + * @var Iterator + */ + return $this->protocol->getResponses(); + } +} diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index 31987e9a..1912bb54 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -29,6 +29,10 @@ final class BoltDriverIntegrationTest extends EnvironmentAwareIntegrationTest */ public function testValidHostname(): void { + if (!str_contains($this->getUri()->getScheme(), 'bolt')) { + $this->markTestSkipped('This test only works with Bolt drivers.'); + } + $results = BoltDriver::create($this->getUri()) ->createSession() ->run('RETURN 1 AS x'); @@ -43,6 +47,10 @@ public function testValidHostname(): void */ public function testValidUrl(): void { + if (!str_contains($this->getUri()->getScheme(), 'bolt')) { + $this->markTestSkipped('This test only works with Bolt drivers.'); + } + $ip = gethostbyname($this->getUri()->getHost()); try { $results = BoltDriver::create($this->getUri()->withHost($ip)->__toString()) diff --git a/tests/Integration/Neo4jLoggerTest.php b/tests/Integration/Neo4jLoggerTest.php index acb618c4..aa8f75c5 100644 --- a/tests/Integration/Neo4jLoggerTest.php +++ b/tests/Integration/Neo4jLoggerTest.php @@ -24,10 +24,14 @@ class Neo4jLoggerTest extends EnvironmentAwareIntegrationTest { public function testLogger(): void { - if ($this->getUri()->getScheme() === 'http') { + if (str_contains($this->getUri()->getScheme(), 'http')) { self::markTestSkipped('This test is not applicable for the HTTP driver'); } + if (str_contains($this->getUri()->getScheme(), 'neo4j')) { + self::markTestSkipped('This test is not applicable clusters'); + } + // Close connections so that we can test the logger logging // during authentication while acquiring a new connection $this->driver->closeConnections(); @@ -83,7 +87,11 @@ static function (string $message, array $context) use (&$infoLogs) { [ 'RUN', [ - 'mode' => 'w', + 'text' => 'RETURN 1 as test', + 'parameters' => [], + 'extra' => [ + 'mode' => 'w', + ], ], ], [ diff --git a/tests/Unit/BasicAuthTest.php b/tests/Unit/BasicAuthTest.php new file mode 100644 index 00000000..626a93a0 --- /dev/null +++ b/tests/Unit/BasicAuthTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit; + +use Bolt\enum\Message; +use Bolt\enum\Signature; +use Bolt\protocol\Response; +use Bolt\protocol\V5; +use Laudis\Neo4j\Authentication\BasicAuth; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Exception\Neo4jException; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\UriInterface; + +class BasicAuthTest extends TestCase +{ + private BasicAuth $auth; + private string $username = 'neo4j'; + private string $password = 'test'; + + protected function setUp(): void + { + $logger = $this->createMock(Neo4jLogger::class); + $this->auth = new BasicAuth($this->username, $this->password, $logger); + } + + public function testToString(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->toString($uri); + + $this->assertSame('Basic neo4j:######@localhost:7687', $result); + } + + /** + * @throws Exception + * @throws \Exception + */ + public function testAuthenticateBoltSuccess(): void + { + $userAgent = 'neo4j-client/1.0'; + + $protocol = $this->createMock(V5::class); + + $response = new Response( + Message::HELLO, + Signature::SUCCESS, + ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] + ); + + $protocol->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $result = $this->auth->authenticateBolt($protocol, $userAgent); + $this->assertArrayHasKey('server', $result); + $this->assertSame('neo4j-server', $result['server']); + $this->assertSame('12345', $result['connection_id']); + } + + public function testAuthenticateBoltFailure(): void + { + $this->expectException(Neo4jException::class); + + $protocol = $this->createMock(V5::class); + $response = new Response( + Message::HELLO, + Signature::FAILURE, + ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] + ); + + $protocol->method('getResponse')->willReturn($response); + + $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); + } + + public function testEmptyCredentials(): void + { + $emptyAuth = new BasicAuth('', '', null); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $emptyAuth->toString($uri); + + $this->assertSame('Basic :######@localhost:7687', $result); + } +} diff --git a/tests/Unit/KerberosAuthTest.php b/tests/Unit/KerberosAuthTest.php new file mode 100644 index 00000000..d60a359e --- /dev/null +++ b/tests/Unit/KerberosAuthTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit; + +use Bolt\enum\Message; +use Bolt\enum\Signature; +use Bolt\protocol\Response; +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Laudis\Neo4j\Authentication\KerberosAuth; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Exception\Neo4jException; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; + +class KerberosAuthTest extends TestCase +{ + private KerberosAuth $auth; + + protected function setUp(): void + { + $logger = $this->createMock(Neo4jLogger::class); + $this->auth = new KerberosAuth('test-token', $logger); + } + + public function testAuthenticateHttpSuccess(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->exactly(2)) + ->method('withHeader') + ->willReturnSelf(); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $auth = new KerberosAuth('test-token', null); + $result = $auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); + + $this->assertSame($request, $result); + } + + public function testAuthenticateBoltFailureV5(): void + { + $this->expectException(Neo4jException::class); + + $protocol = $this->createMock(V5::class); + $response = new Response( + Message::HELLO, + Signature::FAILURE, + ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] + ); + + $protocol->method('getResponse')->willReturn($response); + + $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); + } + + public function testAuthenticateBoltFailureV4(): void + { + $this->expectException(Neo4jException::class); + + $protocol = $this->createMock(V4_4::class); + $response = new Response( + Message::HELLO, + Signature::FAILURE, + ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] + ); + + $protocol->method('getResponse')->willReturn($response); + + $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); + } + + public function testToString(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->toString($uri); + + $this->assertSame('Kerberos test-token@localhost:7687', $result); + } + + public function testEmptyCredentials(): void + { + $emptyAuth = new KerberosAuth('', null); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $emptyAuth->toString($uri); + + $this->assertSame('Kerberos @localhost:7687', $result); + } +} diff --git a/tests/Unit/NoAuthTest.php b/tests/Unit/NoAuthTest.php new file mode 100644 index 00000000..ddfc4274 --- /dev/null +++ b/tests/Unit/NoAuthTest.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit; + +use Bolt\enum\Message; +use Bolt\enum\Signature; +use Bolt\protocol\Response; +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Laudis\Neo4j\Authentication\NoAuth; +use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Exception\Neo4jException; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; + +class NoAuthTest extends TestCase +{ + private NoAuth $auth; + + protected function setUp(): void + { + $logger = $this->createMock(Neo4jLogger::class); + $this->auth = new NoAuth($logger); + } + + public function testAuthenticateHttpSuccess(): void + { + $request = $this->createMock(RequestInterface::class); + $request->expects($this->once()) + ->method('withHeader') + ->with('User-Agent', 'neo4j-client/1.0') + ->willReturnSelf(); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); + $this->assertSame($request, $result); + } + + public function testAuthenticateBoltSuccessV5(): void + { + $userAgent = 'neo4j-client/1.0'; + + $protocol = $this->createMock(V5::class); + + $response = new Response( + Message::HELLO, + Signature::SUCCESS, + ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] + ); + + $protocol->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $result = $this->auth->authenticateBolt($protocol, $userAgent); + $this->assertArrayHasKey('server', $result); + $this->assertSame('neo4j-server', $result['server']); + $this->assertSame('12345', $result['connection_id']); + } + + public function testAuthenticateBoltFailureV5(): void + { + $this->expectException(Neo4jException::class); + + $protocol = $this->createMock(V5::class); + $response = new Response( + Message::HELLO, + Signature::FAILURE, + ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] + ); + + $protocol->method('getResponse')->willReturn($response); + + $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); + } + + public function testAuthenticateBoltSuccessV4(): void + { + $userAgent = 'neo4j-client/1.0'; + + $protocol = $this->createMock(V4_4::class); + + $response = new Response( + Message::HELLO, + Signature::SUCCESS, + ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] + ); + + $protocol->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $result = $this->auth->authenticateBolt($protocol, $userAgent); + $this->assertArrayHasKey('server', $result); + $this->assertSame('neo4j-server', $result['server']); + $this->assertSame('12345', $result['connection_id']); + } + + public function testToString(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->toString($uri); + + $this->assertSame('No Auth localhost:7687', $result); + } + + public function testEmptyCredentials(): void + { + $emptyAuth = new NoAuth(null); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $emptyAuth->toString($uri); + + $this->assertSame('No Auth localhost:7687', $result); + } +} diff --git a/tests/Unit/OpenIDConnectionAuthTest.php b/tests/Unit/OpenIDConnectionAuthTest.php new file mode 100644 index 00000000..75f6b2da --- /dev/null +++ b/tests/Unit/OpenIDConnectionAuthTest.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit; + +use Laudis\Neo4j\Authentication\OpenIDConnectAuth; +use Laudis\Neo4j\Common\Neo4jLogger; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; + +class OpenIDConnectionAuthTest extends TestCase +{ + private OpenIDConnectAuth $auth; + + protected function setUp(): void + { + $this->auth = new OpenIDConnectAuth('test-token', $this->createMock(Neo4jLogger::class)); + } + + public function testAuthenticateHttpSuccess(): void + { + $request = $this->createMock(RequestInterface::class); + $uri = $this->createMock(UriInterface::class); + + $request->expects($this->exactly(2)) + ->method('withHeader') + ->willReturnSelf(); + + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); + + $this->assertSame($request, $result); + } + + public function testAuthenticateHttpAddsAuthorizationHeader(): void + { + $request = $this->createMock(RequestInterface::class); + $uri = $this->createMock(UriInterface::class); + + $sequence = 0; + + $request->expects($this->exactly(2)) + ->method('withHeader') + ->willReturnCallback(function (string $header, string $value) use (&$sequence, $request) { + if ($sequence === 0) { + TestCase::assertSame('Authorization', $header); + TestCase::assertSame('Bearer test-token', $value); + } elseif ($sequence === 1) { + TestCase::assertSame('User-Agent', $header); + TestCase::assertSame('neo4j-client/1.0', $value); + } else { + TestCase::fail('Unexpected header call'); + } + + ++$sequence; + + return $request; + }); + + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); + + $this->assertSame($request, $result); + } + + public function testAuthenticateHttpWithDifferentUri(): void + { + $request = $this->createMock(RequestInterface::class); + $uri = $this->createMock(UriInterface::class); + + $request->expects($this->exactly(2)) + ->method('withHeader') + ->willReturnSelf(); + + $uri->method('getHost')->willReturn('my-neo4j-host'); + $uri->method('getPort')->willReturn(7474); + + $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/2.0'); + + $this->assertSame($request, $result); + } + + public function testAuthenticateHttpWithHeaderReturnsNewInstance(): void + { + $initialRequest = $this->createMock(RequestInterface::class); + $modifiedRequest = $this->createMock(RequestInterface::class); + $finalRequest = $this->createMock(RequestInterface::class); + $uri = $this->createMock(UriInterface::class); + + $initialRequest->expects($this->once()) + ->method('withHeader') + ->with('Authorization', 'Bearer test-token') + ->willReturn($modifiedRequest); + + $modifiedRequest->expects($this->once()) + ->method('withHeader') + ->with('User-Agent', 'neo4j-client/1.0') + ->willReturn($finalRequest); + + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(7687); + + $result = $this->auth->authenticateHttp($initialRequest, $uri, 'neo4j-client/1.0'); + + $this->assertSame($finalRequest, $result); + } +}