diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index 378cd40f..20f10997 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"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 +{"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\/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\/Objects\/ProfiledQueryPlan.php":"d9ba608f3177426ea34d73276d75f20b","src\/Objects\/php your-script.php":"63c3a9abbf4774d1da8c5f3c9f8f455e","src\/AuthenticateInterface.php":"36290631a54b09926af0d78af8fc7282","src\/Objects\/ResultSet.php":"f126eac07a2190797052d123971933be","tests\/resources\/expected\/complex-query-profile.php":"10f481c27e83c1478b5c0e3ad509ab26","src\/Transaction.php":"3e57e12e463749f8e3aabece091c91fc","tests\/Unit\/Neo4jQueryAPIUnitTest.php":"c6159b4657299288776b7db0b942c059","tests\/Unit\/AuthenticationTest.php":"e89756798eaaca89c043d3243f35466d","tests\/Integration\/Neo4jQueryAPIIntegrationTest.php":"12f666622b78259b5a08529e6d21db33","src\/Neo4jQueryAPI.php":"c145914ee1458602691bab00d1822e77","src\/BearerAuthentication.php":"860e05908155ceec82344df65166cd2e","src\/NoAuth.php":"b41cb12e4a51542c297e4387747b77e9","src\/BasicAuthentication.php":"1e5ad5b44a10ee11dcd0198859ae9e79","src\/Objects\/Authentication.php":"af4e140f7199e4c43d0257944419e540"}} \ 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/composer.json b/composer.json index 1a89e801..8b92246d 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ }, "require-dev": { "phpunit/phpunit": "^11.0", - "friendsofphp/php-cs-fixer": "^3.68" + "friendsofphp/php-cs-fixer": "^3.68", + "ext-http": "*" }, "autoload": { "psr-4": { 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 . ':' . $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..83dd2de1 --- /dev/null +++ b/src/BearerAuthentication.php @@ -0,0 +1,28 @@ +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..ca55b5cc 100644 --- a/src/Neo4jQueryAPI.php +++ b/src/Neo4jQueryAPI.php @@ -2,122 +2,170 @@ namespace Neo4j\QueryAPI; -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 { - $response = $this->client->post("/db/neo4j/query/v2/tx"); + $response = $this->client->sendRequest(new Request('POST', '/db/neo4j/query/v2/tx')); $clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity'); $responseData = json_decode($response->getBody(), true); @@ -130,16 +178,12 @@ 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 +205,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 +219,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..6cfc5b0d --- /dev/null +++ b/src/NoAuth.php @@ -0,0 +1,23 @@ +api = $this->initializeApi(); - // Clear database and populate test data $this->clearDatabase(); $this->populateTestData(); @@ -39,11 +37,11 @@ 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"})'); @@ -51,6 +49,10 @@ public function testCounters(): void $this->assertEquals(1, $result->getQueryCounters()->getNodesCreated()); } + /** + * @throws Neo4jException + * @throws RequestExceptionInterface + */ public function testCreateBookmarks(): void { $result = $this->api->run(cypher: 'CREATE (x:Node {hello: "world"})'); @@ -170,7 +172,7 @@ public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void WHERE a.id < b.id AND rand() < 0.1 CREATE (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(a); "; - + $auth = Authentication::basic(); $body = file_get_contents(__DIR__ . '/../resources/responses/complex-query-profile.json'); $mockSack = new MockHandler([ new Response(200, [], $body), @@ -178,7 +180,7 @@ public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void $handler = HandlerStack::create($mockSack); $client = new Client(['handler' => $handler]); - $api = new Neo4jQueryAPI($client); + $api = new Neo4jQueryAPI($client,$auth); $result = $api->run($query); @@ -221,7 +223,7 @@ public function testChildQueryPlanExistence(): void - public function testTransactionCommit(): void +/* public function testTransactionCommit(): void { // Begin a new transaction $tsx = $this->api->beginTransaction(); @@ -246,7 +248,7 @@ public function testTransactionCommit(): void // 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 - } + }*/ /** @@ -270,17 +272,21 @@ private function populateTestData(): void public function testInvalidQueryException(): void { - try { - $this->api->run('CREATE (:Person {createdAt: $invalidParam})', [ - 'date' => new \DateTime('2000-01-01 00:00:00') - ]); - } catch (\Throwable $e) { - $this->assertInstanceOf(Neo4jException::class, $e); - $this->assertEquals('Neo.ClientError.Statement.ParameterMissing', $e->getErrorCode()); - $this->assertEquals('Expected parameter(s): invalidParam', $e->getMessage()); - } + $this->expectException(Neo4jException::class); + $this->expectExceptionMessage('Expected parameter(s): invalidParam'); + + // Log the query and parameters to ensure they are correct + $query = 'CREATE (:Person {createdAt: $invalidParam})'; + $params = [ + 'date' => new \DateTime('2000-01-01 00:00:00') + ]; + + + // Execute query + $this->api->run($query, $params); } + public function testCreateDuplicateConstraintException(): void { try { diff --git a/tests/Unit/AuthenticationTest.php b/tests/Unit/AuthenticationTest.php new file mode 100644 index 00000000..ab64eeab --- /dev/null +++ b/tests/Unit/AuthenticationTest.php @@ -0,0 +1,53 @@ +assertEquals("Bearer $mockToken", $auth->getHeader()); + $this->assertEquals('Bearer', $auth->getType()); + } + + public function testBasicAuthentication(): void + { + $mockUsername = 'mockUser'; + $mockPassword = 'mockPass'; + putenv('NEO4J_USERNAME=' . $mockUsername); + putenv('NEO4J_PASSWORD=' . $mockPassword); + $auth = Authentication::basic(); + $expectedHeader = 'Basic ' . base64_encode("$mockUsername:$mockPassword"); + $this->assertEquals($expectedHeader, $auth->getHeader()); + $this->assertEquals('Basic', $auth->getType()); + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } + + public function testFallbackToEnvironmentVariables(): void + { + putenv('NEO4J_USERNAME=mockEnvUser'); + putenv('NEO4J_PASSWORD=mockEnvPass'); + $auth = Authentication::basic(); + $expectedHeader = 'Basic ' . base64_encode("mockEnvUser:mockEnvPass"); + $this->assertEquals($expectedHeader, $auth->getHeader()); + $this->assertEquals('Basic', $auth->getType()); + putenv('NEO4J_USERNAME'); + putenv('NEO4J_PASSWORD'); + } + + public function testNoAuth(): void + { + $auth = Authentication::noAuth(); + + $this->assertNull($auth->getHeader()); + + $this->assertEquals('NoAuth', $auth->getType()); + } + +} diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php index f03c16fb..6fa1aa43 100644 --- a/tests/Unit/Neo4jQueryAPIUnitTest.php +++ b/tests/Unit/Neo4jQueryAPIUnitTest.php @@ -3,63 +3,74 @@ namespace Neo4j\QueryAPI\Tests\Unit; use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Handler\MockHandler; 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'; 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: [],