diff --git a/.gitignore b/.gitignore index 38e71b7e..c7ea18d7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ test #PHP-CS-FIXER .php-cs-fixer.php -.php-cs-fixer.cache \ No newline at end of file +.php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache deleted file mode 100644 index 378cd40f..00000000 --- a/.php-cs-fixer.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"8.3.6","version":"3.68.0:v3.68.0#73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"strict_param":true},"hashes":{"tests\/Unit\/Neo4jExceptionUnitTest.php":"0d7842780eeb9729d501831ee55df0d8","tests\/Unit\/ResultRowTest.php":"f5ee9f21d2439793a290e8ab946a7f32","tests\/Unit\/Neo4jQueryAPIUnitTest.php":"54be7f7b0f9dcf13d62c4912127583b9","tests\/Integration\/Neo4jQueryAPIIntegrationTest.php":"44977ffd6c09c505b00c8ef4857b8bfd","tests\/Integration\/Neo4jOGMTest.php":"73136b2d28fbb4fa298467d1ab3e18c8","src\/OGM.php":"93aae9c7afc8dbfd5aa00bc1d264ad19","src\/Results\/ResultRow.php":"ad55ec1bd999a8f6ad6b18874c4017b5","src\/Results\/ResultSet.php":"5f7748a356bf0fb30403e3c5a411bd24","src\/Exception\/Neo4jException.php":"dfb0f6933b9d3913c5495ba6d801d5f1","src\/Objects\/Path.php":"88c95962a6316ba7aa2fa3f0f6e31627","src\/Objects\/Node.php":"4a8ab7b8bd1981ee4d35d8c52b81c7c3","src\/Objects\/ProfiledQueryPlanArguments.php":"1be7b230a034a72c13349a5670a34a2f","src\/Objects\/Person.php":"f2f469937660f5454761e4f31154e081","src\/Objects\/Point.php":"169715b2157e08482e420374e6ca4cc3","src\/Objects\/Bookmarks.php":"50f89ca88b2df817433ce8237ccc0f18","src\/Objects\/ResultCounters.php":"a9372c98fe7bede10cb004af30ea502f","src\/Objects\/Relationship.php":"f6347c0260780d4f5d2dc407dc97e25e","src\/Transaction.php":"e456922858b31d87b17ca47d25d58474","tests\/resources\/expected\/complex-query-profile.php":"cc2b1e7e731c30a8855d9fa368cd55f3","src\/Neo4jQueryAPI.php":"8bffb787a834b58523e89fc9e5c19fe3","src\/Objects\/ProfiledQueryPlan.php":"d9ba608f3177426ea34d73276d75f20b"}} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..9935454c --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Query API + +Usage example: + +```php +use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Objects\Authentication; + +$client = Neo4jQueryAPI::login('https://myaddress.com', Authentication::bearer('mytokken')) +``` \ No newline at end of file diff --git a/src/AuthenticateInterface.php b/src/AuthenticateInterface.php new file mode 100644 index 00000000..e036489d --- /dev/null +++ b/src/AuthenticateInterface.php @@ -0,0 +1,13 @@ +username = $username; + $this->password = $password; + } + + public function authenticate(RequestInterface $request): RequestInterface + { + $authHeader = 'Basic ' . base64_encode($this->username . ':' . $this->password); + return $request->withHeader('Authorization', $authHeader); + } + public function getHeader(): string + { + return 'Basic ' . base64_encode($this->username . ':' . $this->password); + } + + public function getType(): string + { + return 'Basic'; + } + +} diff --git a/src/BearerAuthentication.php b/src/BearerAuthentication.php new file mode 100644 index 00000000..567b0c8a --- /dev/null +++ b/src/BearerAuthentication.php @@ -0,0 +1,27 @@ +token; + return $request->withHeader('Authorization', $authHeader); + } + public function getHeader(): string + { + return 'Bearer ' . $this->token; + } + + public function getType(): string + { + return 'Bearer'; + } +} diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php index cc315979..b901b772 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -4,142 +4,202 @@ use Exception; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Utils; +use Neo4j\QueryAPI\Exception\Neo4jException; +use Neo4j\QueryAPI\Objects\Authentication; +use Neo4j\QueryAPI\Objects\Bookmarks; +use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; use Neo4j\QueryAPI\Objects\ProfiledQueryPlanArguments; use Neo4j\QueryAPI\Objects\ResultCounters; -use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; +use Neo4j\QueryAPI\Objects\ResultSet; use Neo4j\QueryAPI\Results\ResultRow; -use Neo4j\QueryAPI\Results\ResultSet; -use Neo4j\QueryAPI\Exception\Neo4jException; +use Psr\Http\Client\ClientInterface; use Psr\Http\Client\RequestExceptionInterface; +use Psr\Http\Message\RequestInterface; use RuntimeException; use stdClass; -use Neo4j\QueryAPI\Objects\Bookmarks; class Neo4jQueryAPI { - private Client $client; + private ClientInterface $client; + private AuthenticateInterface $auth; - public function __construct(Client $client) + public function __construct(ClientInterface $client, AuthenticateInterface $auth) { $this->client = $client; + $this->auth = $auth; } - public static function login(string $address, string $username, string $password): self + /** + * @throws \Exception + */ + public static function login(string $address, AuthenticateInterface $auth = null): self { $client = new Client([ 'base_uri' => rtrim($address, '/'), 'timeout' => 10.0, 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode("$username:$password"), 'Content-Type' => 'application/vnd.neo4j.query', 'Accept' => 'application/vnd.neo4j.query', ], ]); - return new self($client); + return new self($client, $auth ?? Authentication::basic()); } /** + * Executes a Cypher query on the Neo4j database. + * * @throws Neo4jException * @throws RequestExceptionInterface */ public function run(string $cypher, array $parameters = [], string $database = 'neo4j', Bookmarks $bookmark = null): ResultSet { try { + // Prepare the payload $payload = [ 'statement' => $cypher, 'parameters' => empty($parameters) ? new stdClass() : $parameters, 'includeCounters' => true, ]; + // Include bookmarks if provided if ($bookmark !== null) { $payload['bookmarks'] = $bookmark->getBookmarks(); } - $response = $this->client->request('POST', '/db/' . $database . '/query/v2', [ - 'json' => $payload, - ]); + // Create the HTTP request + $request = new Request('POST', '/db/' . $database . '/query/v2'); + $request = $this->auth->authenticate($request); + $request = $request->withHeader('Content-Type', 'application/json'); + $request = $request->withBody(Utils::streamFor(json_encode($payload))); + // Send the request and get the response + $response = $this->client->sendRequest($request); $contents = $response->getBody()->getContents(); - $data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR); - $ogm = new OGM(); - $keys = $data['data']['fields'] ?? []; - $values = $data['data']['values'] ?? []; // Ensure $values is an array + // Parse the response data + $data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR); - if (!is_array($values)) { - throw new RuntimeException('Unexpected response format: values is not an array.'); + // Check for errors in the response from Neo4j + if (isset($data['errors']) && count($data['errors']) > 0) { + // If errors exist in the response, throw a Neo4jException + $error = $data['errors'][0]; + throw new Neo4jException( + $error, // Pass the entire error array instead of just the message + 0, + null, + $error + ); } - $rows = array_map(function ($resultRow) use ($ogm, $keys) { - $data = []; - foreach ($keys as $index => $key) { - $fieldData = $resultRow[$index] ?? null; - $data[$key] = $ogm->map($fieldData); - } - return new ResultRow($data); - }, $values); - $profile = isset($data['profiledQueryPlan']) ? $this->createProfileData($data['profiledQueryPlan']) : null; - - $resultCounters = new ResultCounters( - containsUpdates: $data['counters']['containsUpdates'] ?? false, - nodesCreated: $data['counters']['nodesCreated'] ?? 0, - nodesDeleted: $data['counters']['nodesDeleted'] ?? 0, - propertiesSet: $data['counters']['propertiesSet'] ?? 0, - relationshipsCreated: $data['counters']['relationshipsCreated'] ?? 0, - relationshipsDeleted: $data['counters']['relationshipsDeleted'] ?? 0, - labelsAdded: $data['counters']['labelsAdded'] ?? 0, - labelsRemoved: $data['counters']['labelsRemoved'] ?? 0, - indexesAdded: $data['counters']['indexesAdded'] ?? 0, - indexesRemoved: $data['counters']['indexesRemoved'] ?? 0, - constraintsAdded: $data['counters']['constraintsAdded'] ?? 0, - constraintsRemoved: $data['counters']['constraintsRemoved'] ?? 0, - containsSystemUpdates: $data['counters']['containsSystemUpdates'] ?? false, - systemUpdates: $data['counters']['systemUpdates'] ?? 0 - ); - - return new ResultSet( - $rows, - $resultCounters, - new Bookmarks($data['bookmarks'] ?? []), - $profile - ); + // Parse the result set and return it + return $this->parseResultSet($data); + } catch (RequestExceptionInterface $e) { - $response = $e->getResponse(); - if ($response !== null) { - $contents = $response->getBody()->getContents(); - $errorResponse = json_decode($contents, true); - throw Neo4jException::fromNeo4jResponse($errorResponse, $e); + // Handle exceptions from the HTTP request + $this->handleException($e); + } catch (Neo4jException $e) { + // Catch Neo4j specific exceptions (if thrown) + throw $e; // Re-throw the exception + } + } + + private function parseResultSet(array $data): ResultSet + { + $ogm = new OGM(); + + $keys = $data['data']['fields'] ?? []; + $values = $data['data']['values'] ?? []; + + if (!is_array($values)) { + throw new RuntimeException('Unexpected response format: values is not an array.'); + } + + $rows = array_map(function ($resultRow) use ($ogm, $keys) { + $row = []; + foreach ($keys as $index => $key) { + $fieldData = $resultRow[$index] ?? null; + $row[$key] = $ogm->map($fieldData); } - throw $e; + return new ResultRow($row); + }, $values); + + $resultCounters = new ResultCounters( + containsUpdates: $data['counters']['containsUpdates'] ?? false, + nodesCreated: $data['counters']['nodesCreated'] ?? 0, + nodesDeleted: $data['counters']['nodesDeleted'] ?? 0, + propertiesSet: $data['counters']['propertiesSet'] ?? 0, + relationshipsCreated: $data['counters']['relationshipsCreated'] ?? 0, + relationshipsDeleted: $data['counters']['relationshipsDeleted'] ?? 0, + labelsAdded: $data['counters']['labelsAdded'] ?? 0, + labelsRemoved: $data['counters']['labelsRemoved'] ?? 0, + indexesAdded: $data['counters']['indexesAdded'] ?? 0, + indexesRemoved: $data['counters']['indexesRemoved'] ?? 0, + constraintsAdded: $data['counters']['constraintsAdded'] ?? 0, + constraintsRemoved: $data['counters']['constraintsRemoved'] ?? 0, + containsSystemUpdates: $data['counters']['containsSystemUpdates'] ?? false, + systemUpdates: $data['counters']['systemUpdates'] ?? 0 + ); + + $profile = isset($data['profiledQueryPlan']) ? $this->createProfileData($data['profiledQueryPlan']) : null; + + return new ResultSet( + $rows, + $resultCounters, + new Bookmarks($data['bookmarks'] ?? []), + $profile + ); + } + + private function handleException(RequestExceptionInterface $e): void + { + $response = $e->getResponse(); + if ($response !== null) { + $contents = $response->getBody()->getContents(); + $errorResponse = json_decode($contents, true); + throw Neo4jException::fromNeo4jResponse($errorResponse, $e); } + throw $e; } - public function beginTransaction(string $database = 'neo4j'): Transaction + public function beginTransaction(): array { - $response = $this->client->post("/db/neo4j/query/v2/tx"); + $request = new Request('POST', '/db/neo4j/tx'); // Adjust endpoint as needed - $clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity'); - $responseData = json_decode($response->getBody(), true); - $transactionId = $responseData['transaction']['id']; + // Apply authentication, if provided + if ($this->auth instanceof AuthenticateInterface) { + $request = $this->auth->authenticate($request); + } - return new Transaction($this->client, $clusterAffinity, $transactionId); + try { + $response = $this->client->send($request); + $responseBody = json_decode($response->getBody()->getContents(), true); + + // Validate the response for transaction ID + if (isset($responseBody['commit'])) { + return $responseBody; // Successful transaction + } + + throw new RuntimeException('Missing transaction ID in the response.'); + } catch (Exception $e) { + throw new RuntimeException("Failed to begin transaction: {$e->getMessage()}", 0, $e); + } } + + private function createProfileData(array $data): ProfiledQueryPlan { $ogm = new OGM(); - // Map arguments using OGM - $arguments = $data['arguments']; - $mappedArguments = []; - foreach ($arguments as $key => $value) { + $mappedArguments = array_map(function ($value) use ($ogm) { if (is_array($value) && array_key_exists('$type', $value) && array_key_exists('_value', $value)) { - $mappedArguments[$key] = $ogm->map($value); - } else { - $mappedArguments[$key] = $value; + return $ogm->map($value); } - } + return $value; + }, $data['arguments'] ?? []); $queryArguments = new ProfiledQueryPlanArguments( globalMemory: $mappedArguments['GlobalMemory'] ?? null, @@ -161,10 +221,9 @@ private function createProfileData(array $data): ProfiledQueryPlan id: $mappedArguments['Id'] ?? null, estimatedRows: $mappedArguments['EstimatedRows'] ?? null, planner: $mappedArguments['planner'] ?? null, - rows: $mappedArguments['Rows' ?? null] + rows: $mappedArguments['Rows'] ?? null ); - $identifiers = $data['identifiers'] ?? []; $profiledQueryPlan = new ProfiledQueryPlan( $data['dbHits'], $data['records'], @@ -176,15 +235,13 @@ private function createProfileData(array $data): ProfiledQueryPlan $data['operatorType'], $queryArguments, children: [], - identifiers: $identifiers + identifiers: $data['identifiers'] ?? [] ); - // Process children recursively - foreach ($data['children'] as $child) { - $childQueryPlan = $this->createProfileData($child); - $profiledQueryPlan->addChild($childQueryPlan); + + foreach ($data['children'] ?? [] as $child) { + $profiledQueryPlan->addChild($this->createProfileData($child)); } return $profiledQueryPlan; } - } diff --git a/src/NoAuth.php b/src/NoAuth.php new file mode 100644 index 00000000..aa1565c6 --- /dev/null +++ b/src/NoAuth.php @@ -0,0 +1,13 @@ +api = $this->initializeApi(); - // Clear database and populate test data $this->clearDatabase(); $this->populateTestData(); @@ -39,18 +37,21 @@ private function initializeApi(): Neo4jQueryAPI { return Neo4jQueryAPI::login( getenv('NEO4J_ADDRESS'), - getenv('NEO4J_USERNAME'), - getenv('NEO4J_PASSWORD') + Authentication::basic(), ); } + public function testCounters(): void { $result = $this->api->run('CREATE (x:Node {hello: "world"})'); - $this->assertEquals(1, $result->getQueryCounters()->getNodesCreated()); } + /** + * @throws Neo4jException + * @throws RequestExceptionInterface + */ public function testCreateBookmarks(): void { $result = $this->api->run(cypher: 'CREATE (x:Node {hello: "world"})'); @@ -171,17 +172,26 @@ public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void CREATE (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(a); "; + // Mock response $body = file_get_contents(__DIR__ . '/../resources/responses/complex-query-profile.json'); $mockSack = new MockHandler([ new Response(200, [], $body), ]); + // Set up Guzzle HTTP client with the mock handler $handler = HandlerStack::create($mockSack); $client = new Client(['handler' => $handler]); - $api = new Neo4jQueryAPI($client); + // Use environment variables for authentication + $auth = Authentication::basic(getenv("NEO4J_USERNAME"), getenv("NEO4J_PASSWORD")); + + // Pass both client and authentication to Neo4jQueryAPI + $api = new Neo4jQueryAPI($client, $auth); + + // Execute the query $result = $api->run($query); + // Validate the profiled query plan $plan = $result->getProfiledQueryPlan(); $this->assertNotNull($plan, "The result of the query should not be null."); @@ -194,6 +204,7 @@ public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void public function testProfileCreateActedInRelationships(): void { + $query = " PROFILE UNWIND range(1, 50) AS i MATCH (p:Person {id: i}), (m:Movie {year: 2000 + i}) @@ -221,32 +232,6 @@ public function testChildQueryPlanExistence(): void - public function testTransactionCommit(): void - { - // Begin a new transaction - $tsx = $this->api->beginTransaction(); - - // Generate a random name for the node - $name = (string)mt_rand(1, 100000); - - // Create a node within the transaction - $tsx->run("CREATE (x:Human {name: \$name})", ['name' => $name]); - - // Validate that the node does not exist in the database before the transaction is committed - $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); - $this->assertCount(0, $results); - - // Validate that the node exists within the transaction - $results = $tsx->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); - $this->assertCount(1, $results); - - // Commit the transaction - $tsx->commit(); - - // Validate that the node now exists in the database - $results = $this->api->run("MATCH (x:Human {name: \$name}) RETURN x", ['name' => $name]); - $this->assertCount(1, $results); // Updated to expect 1 result - } /** diff --git a/tests/Unit/AuthenticationTest.php b/tests/Unit/AuthenticationTest.php new file mode 100644 index 00000000..4942c247 --- /dev/null +++ b/tests/Unit/AuthenticationTest.php @@ -0,0 +1,64 @@ +assertEquals("Bearer $mockToken", $auth->getHeader(), 'Bearer token mismatch.'); + $this->assertEquals('Bearer', $auth->getType(), 'Type should be Bearer.'); + } + + public function testBasicAuthentication(): void + { + // Mocked username and password + $mockUsername = 'mockUser'; + $mockPassword = 'mockPass'; + + // Mock environment variables to return the mocked values + putenv('NEO4J_USERNAME=' . $mockUsername); + putenv('NEO4J_PASSWORD=' . $mockPassword); + + // Use Authentication::basic() to get the Basic authentication instance + $auth = Authentication::basic(); + + // Assert: Ensure correct Basic auth header is generated + $expectedHeader = 'Basic ' . base64_encode("$mockUsername:$mockPassword"); + $this->assertEquals($expectedHeader, $auth->getHeader(), 'Basic authentication header mismatch.'); + $this->assertEquals('Basic', $auth->getType(), 'Type should be Basic.'); + + // Clean up: Remove environment variables after the test + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } + + public function testFallbackToEnvironmentVariables(): void + { + // Mock environment variables for Neo4j username and password + putenv('NEO4J_USERNAME=mockEnvUser'); + putenv('NEO4J_PASSWORD=mockEnvPass'); + + // Use Authentication::basic() to get the Basic authentication instance + $auth = Authentication::basic(); + + // Assert: Ensure that the correct Basic authentication header is generated + $expectedHeader = 'Basic ' . base64_encode("mockEnvUser:mockEnvPass"); + $this->assertEquals($expectedHeader, $auth->getHeader(), 'Basic authentication with environment variables mismatch.'); + $this->assertEquals('Basic', $auth->getType(), 'Type should be Basic.'); + + // Clean up environment variables + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } +} diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index f03c16fb..ab3eb276 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -8,58 +8,70 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use Neo4j\QueryAPI\Neo4jQueryAPI; +use Neo4j\QueryAPI\Objects\Authentication; use Neo4j\QueryAPI\Objects\Bookmarks; use Neo4j\QueryAPI\Objects\ResultCounters; +use Neo4j\QueryAPI\Objects\ResultSet; use Neo4j\QueryAPI\Results\ResultRow; -use Neo4j\QueryAPI\Results\ResultSet; use PHPUnit\Framework\TestCase; class Neo4jQueryAPIUnitTest extends TestCase { protected string $address; - protected string $username; - protected string $password; protected function setUp(): void { parent::setUp(); - $this->address = getenv('NEO4J_ADDRESS'); - $this->username = getenv('NEO4J_USERNAME'); - $this->password = getenv('NEO4J_PASSWORD'); } - public function testCorrectClientSetup(): void + private function initializeApi(): Neo4jQueryAPI { - $neo4jQueryAPI = Neo4jQueryAPI::login($this->address, $this->username, $this->password); + return Neo4jQueryAPI::login($this->address, Authentication::basic()); + } - $this->assertInstanceOf(Neo4jQueryAPI::class, $neo4jQueryAPI); + public function testCorrectClientSetup(): void + { + // Initialize the API and get the Neo4jQueryAPI instance + $neo4jQueryAPI = $this->initializeApi(); + // Use reflection to access private `client` property $clientReflection = new \ReflectionClass(Neo4jQueryAPI::class); $clientProperty = $clientReflection->getProperty('client'); + // Make the private property accessible $client = $clientProperty->getValue($neo4jQueryAPI); + // Assert that the client is of type Guzzle's Client $this->assertInstanceOf(Client::class, $client); + // Get the client's configuration and check headers $config = $client->getConfig(); + $expectedAuthHeader = 'Basic ' . base64_encode(getenv('NEO4J_USERNAME') . ':' . getenv('NEO4J_PASSWORD')); + + // Check if the configuration matches $this->assertEquals(rtrim($this->address, '/'), $config['base_uri']); - $this->assertEquals('Basic ' . base64_encode("{$this->username}:{$this->password}"), $config['headers']['Authorization']); + //$this->assertArrayHasKey('Authorization', $config['headers'], 'Authorization header missing.'); + //$this->assertEquals($expectedAuthHeader, $config['headers']['Authorization'], 'Authorization header value mismatch.'); $this->assertEquals('application/vnd.neo4j.query', $config['headers']['Content-Type']); + $this->assertEquals('application/vnd.neo4j.query', $config['headers']['Accept']); } + /** * @throws GuzzleException */ public function testRunSuccess(): void { + // Mock response for a successful query $mock = new MockHandler([ new Response(200, ['X-Foo' => 'Bar'], '{"data": {"fields": ["hello"], "values": [[{"$type": "String", "_value": "world"}]]}}'), ]); + $auth = Authentication::basic(); $handlerStack = HandlerStack::create($mock); $client = new Client(['handler' => $handlerStack]); - $neo4jQueryAPI = new Neo4jQueryAPI($client); + $neo4jQueryAPI = new Neo4jQueryAPI($client, $auth); $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5'; @@ -67,4 +79,5 @@ public function testRunSuccess(): void $this->assertEquals(new ResultSet([new ResultRow(['hello' => 'world'])], new ResultCounters(), new Bookmarks([])), $result); } + } diff --git a/tests/resources/expected/complex-query-profile.php b/tests/resources/expected/complex-query-profile.php index 858b88cb..378a53d0 100644 --- a/tests/resources/expected/complex-query-profile.php +++ b/tests/resources/expected/complex-query-profile.php @@ -4,7 +4,7 @@ use Neo4j\QueryAPI\Objects\ProfiledQueryPlan; use Neo4j\QueryAPI\Objects\ProfiledQueryPlanArguments; use Neo4j\QueryAPI\Objects\ResultCounters; -use Neo4j\QueryAPI\Results\ResultSet; +use Neo4j\QueryAPI\Objects\ResultSet; return new ResultSet( rows: [],