Skip to content

Commit 7226193

Browse files
committed
Exception testing code
1 parent 97b5c6c commit 7226193

File tree

6 files changed

+220
-64
lines changed

6 files changed

+220
-64
lines changed

src/Exception/Neo4jException.php

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,63 @@
66

77
class Neo4jException extends Exception
88
{
9-
private string $errorCode;
9+
private readonly string $errorCode;
10+
private readonly ?string $errorType;
11+
private readonly ?string $errorSubType;
12+
private readonly ?string $errorName;
1013

11-
public function __construct(string $errorCode, string $message, int $code = 0, Exception $previous = null)
14+
public function __construct(
15+
array $errorDetails = [],
16+
int $statusCode = 0,
17+
?\Throwable $previous = null
18+
)
1219
{
13-
$this->errorCode = $errorCode;
14-
parent::__construct($message, $code, $previous);
20+
$this->errorCode = $errorDetails['code'] ?? 'Neo.UnknownError';
21+
$errorParts = explode('.', $this->errorCode);
22+
$this->errorType = $errorParts[1] ?? null;
23+
$this->errorSubType = $errorParts[2] ?? null;
24+
$this->errorName = $errorParts[3] ?? null;
25+
26+
27+
$message = $errorDetails['message'] ?? 'An unknown error occurred.';
28+
parent::__construct($message, $statusCode, $previous);
1529
}
1630

31+
/**
32+
* Get the Neo4j error code associated with this exception.
33+
*/
1734
public function getErrorCode(): string
1835
{
1936
return $this->errorCode;
2037
}
2138

22-
public static function fromNeo4jResponse(array $response): self
39+
public function getType(): ?string
40+
{
41+
return $this->errorType;
42+
}
43+
44+
public function getSubType(): ?string
45+
{
46+
return $this->errorSubType;
47+
}
48+
49+
public function getName(): ?string
50+
{
51+
return $this->errorName;
52+
}
53+
54+
/**
55+
* Create a Neo4jException instance from a Neo4j error response array.
56+
*
57+
* @param array $response The error response from Neo4j.
58+
* @param \Throwable|null $exception Optional previous exception for chaining.
59+
* @return self
60+
*/
61+
public static function fromNeo4jResponse(array $response, ?\Throwable $exception = null): self
2362
{
24-
$errorCode = $response['code'] ?? 'Neo.UnknownError';
25-
$message = $response['message'] ?? 'An unknown error occurred.';
26-
return new self($errorCode, $message);
63+
$errorDetails = $response['errors'][0] ?? [];
64+
$statusCode = $errorDetails['statusCode'] ?? 0;
65+
66+
return new self($errorDetails, (int)$statusCode, $exception);
2767
}
2868
}

src/Neo4jQueryAPI.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GuzzleHttp\Exception\GuzzleException;
88
use GuzzleHttp\Exception\RequestException;
99
use Neo4j\QueryAPI\Exception\Neo4jException;
10+
use Psr\Http\Client\RequestExceptionInterface;
1011
use RuntimeException;
1112
use stdClass;
1213

@@ -36,7 +37,8 @@ public static function login(string $address, string $username, string $password
3637
}
3738

3839
/**
39-
* @throws GuzzleException
40+
* @throws Neo4jException
41+
* @throws RequestExceptionInterface
4042
*/
4143
public function run(string $cypher, array $parameters, string $database = 'neo4j'): array
4244
{
@@ -54,20 +56,16 @@ public function run(string $cypher, array $parameters, string $database = 'neo4j
5456

5557
// Decode the response body
5658
return json_decode($response->getBody()->getContents(), true);
57-
} catch (RequestException $e) {
58-
// Catch any HTTP request errors
59-
$errorResponse = [
60-
'code' => 'Neo.HttpRequestError',
61-
'message' => 'HTTP request failed: ' . $e->getMessage(),
62-
];
63-
throw Neo4jException::fromNeo4jResponse($errorResponse);
64-
} catch (Exception $e) {
65-
// Catch any other unexpected errors
66-
$errorResponse = [
67-
'code' => 'Neo.UnknownError',
68-
'message' => 'An unknown error occurred: ' . $e->getMessage(),
69-
];
70-
throw Neo4jException::fromNeo4jResponse($errorResponse);
59+
} catch (RequestExceptionInterface $e) {
60+
$response = $e->getResponse();
61+
if ($response !== null) {
62+
$contents = $response->getBody()->getContents();
63+
$errorResponse = json_decode($contents, true);
64+
65+
throw Neo4jException::fromNeo4jResponse($errorResponse, $e);
66+
}
67+
68+
throw $e;
7169
}
7270
}
7371

src/Service/Neo4jClient.php

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/run_query.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
);
1616

1717
// Define a Cypher query
18-
$query = "MATCH (n:Person {DateTime:'2024-12-11T11:00:00Z'}) RETURN n LIMIT 10";
18+
$query = "MATCH (n:Person) RETURN n ";
1919

2020
// Fetch results in plain JSON format
2121
$plainResults = $api->run($query, [], 'neo4j', false);

tests/Integration/Neo4jQueryAPIIntegrationTest.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public function testRunSuccessWithParameters(
7373
string $query,
7474
array $parameters,
7575
array $expectedResults
76-
): void {
76+
): void
77+
{
7778
$results = $this->executeQuery($query, $parameters);
7879
$subsetResults = $this->createSubset($expectedResults, $results);
7980

@@ -83,20 +84,44 @@ public function testRunSuccessWithParameters(
8384

8485
public function testInvalidQueryException(): void
8586
{
86-
$this->expectException(Neo4jException::class);
87-
8887
try {
89-
$client = new Neo4jClient();
90-
$client->executeQuery('CREATE (:Person {createdAt: $date})', [
88+
$this->api->run('CREATE (:Person {createdAt: $invalidParam})', [
9189
'date' => new \DateTime('2000-01-01 00:00:00')
9290
]);
91+
} catch (\Throwable $e) {
92+
$this->assertInstanceOf(Neo4jException::class, $e);
93+
$this->assertEquals('Neo.ClientError.Statement.ParameterMissing', $e->getErrorCode());
94+
$this->assertEquals('Expected parameter(s): invalidParam', $e->getMessage());
95+
}
96+
}
97+
98+
public function testInvalidInputException(): void
99+
{
100+
try {
101+
$this->api->run('match (n:Person) return', []);
102+
} catch (\Throwable $e) {
103+
$this->assertInstanceOf(Neo4jException::class, $e);
104+
$this->assertEquals('Neo.ClientError.Statement.SyntaxError', $e->getErrorCode());
105+
$this->assertEquals('Invalid input \'\': expected an expression, \'*\' or \'DISTINCT\' (line 1, column 24 (offset: 23))
106+
"match (n:Person) return"
107+
^', $e->getMessage());
108+
}
109+
}
110+
111+
public function testCreateDuplicateConstraintException(): void
112+
{
113+
try {
114+
$this->api->run('CREATE CONSTRAINT person_name FOR (n:Person1) REQUIRE n.name IS UNIQUE', []);
115+
$this->fail('Expected a Neo4jException to be thrown.');
93116
} catch (Neo4jException $e) {
94-
$this->assertEquals('Neo.DatabaseError.Database.UnableToStartDatabase', $e->getErrorCode());
95-
$this->assertEquals('Unable to start database.', $e->getMessage());
96-
throw $e;
117+
// $errorMessages = $e->getErrorType() . $e->errorSubType() . $e->errorName();
118+
$this->assertInstanceOf(Neo4jException::class, $e);
119+
$this->assertEquals('Neo.ClientError.Schema.ConstraintWithNameAlreadyExists', $e->getErrorCode());
120+
$this->assertNotEmpty($e->getMessage());
97121
}
98122
}
99123

124+
100125
private function createSubset(array $expected, array $actual): array
101126
{
102127
$subset = [];
@@ -115,8 +140,6 @@ private function createSubset(array $expected, array $actual): array
115140
}
116141

117142

118-
119-
120143
public static function queryProvider(): array
121144
{
122145

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace Neo4j\QueryAPI\Tests\Unit;
4+
5+
use Exception;
6+
use PHPUnit\Framework\TestCase;
7+
use Neo4j\QueryAPI\Exception\Neo4jException;
8+
9+
class Neo4jExceptionUnitTest extends TestCase
10+
{
11+
/**
12+
* Test the constructor and property initialization.
13+
*/
14+
public function testConstructor(): void
15+
{
16+
$errorDetails = [
17+
'code' => 'Neo.ClientError.Statement.SyntaxError',
18+
'message' => 'Invalid syntax near ...',
19+
'statusCode' => 400
20+
];
21+
22+
$exception = new Neo4jException($errorDetails);
23+
24+
$this->assertSame('Neo.ClientError.Statement.SyntaxError', $exception->getErrorCode());
25+
$this->assertSame('ClientError', $exception->getType());
26+
$this->assertSame('Statement', $exception->getSubType());
27+
$this->assertSame('SyntaxError', $exception->getName());
28+
$this->assertSame('Invalid syntax near ...', $exception->getMessage());
29+
$this->assertSame(0, $exception->getCode()); // Default statusCode to 0
30+
}
31+
32+
/**
33+
* Test the handling of missing error details.
34+
*/
35+
public function testConstructorWithMissingErrorDetails(): void
36+
{
37+
$exception = new Neo4jException([]);
38+
39+
$this->assertSame('Neo.UnknownError', $exception->getErrorCode());
40+
$this->assertSame('UnknownError', $exception->getType());
41+
$this->assertNull($exception->getSubType());
42+
$this->assertNull($exception->getName());
43+
$this->assertSame('An unknown error occurred.', $exception->getMessage());
44+
$this->assertSame(0, $exception->getCode());
45+
}
46+
47+
/**
48+
* Test the `fromNeo4jResponse` static method with valid input.
49+
*/
50+
public function testFromNeo4jResponse(): void
51+
{
52+
$response = [
53+
'errors' => [
54+
[
55+
'code' => 'Neo.ClientError.Transaction.InvalidRequest',
56+
'message' => 'Transaction error occurred.',
57+
'statusCode' => 500
58+
]
59+
]
60+
];
61+
62+
$exception = Neo4jException::fromNeo4jResponse($response);
63+
64+
$this->assertSame('Neo.ClientError.Transaction.InvalidRequest', $exception->getErrorCode());
65+
$this->assertSame('ClientError', $exception->getType());
66+
$this->assertSame('Transaction', $exception->getSubType());
67+
$this->assertSame('InvalidRequest', $exception->getName());
68+
$this->assertSame('Transaction error occurred.', $exception->getMessage());
69+
$this->assertSame(500, $exception->getCode());
70+
}
71+
72+
/**
73+
* Test the `fromNeo4jResponse` static method with missing error details.
74+
*/
75+
public function testFromNeo4jResponseWithMissingDetails(): void
76+
{
77+
$response = ['errors' => []];
78+
79+
$exception = Neo4jException::fromNeo4jResponse($response);
80+
81+
$this->assertSame('Neo.UnknownError', $exception->getErrorCode());
82+
$this->assertSame('UnknownError', $exception->getType());
83+
$this->assertNull($exception->getSubType());
84+
$this->assertNull($exception->getName());
85+
$this->assertSame('An unknown error occurred.', $exception->getMessage());
86+
$this->assertSame(0, $exception->getCode());
87+
}
88+
89+
/**
90+
* Test the `fromNeo4jResponse` static method with null response.
91+
*/
92+
public function testFromNeo4jResponseWithNullResponse(): void
93+
{
94+
$response = ['errors' => null];
95+
96+
$exception = Neo4jException::fromNeo4jResponse($response);
97+
98+
$this->assertSame('Neo.UnknownError', $exception->getErrorCode());
99+
$this->assertSame('UnknownError', $exception->getType());
100+
$this->assertNull($exception->getSubType(), "Expected 'getSubType()' to return null for null response");
101+
$this->assertNull($exception->getName(), "Expected 'getName()' to return null for null response");
102+
$this->assertSame('An unknown error occurred.', $exception->getMessage());
103+
$this->assertSame(0, $exception->getCode());
104+
}
105+
106+
/**
107+
* Test exception chaining.
108+
*/
109+
public function testExceptionChaining(): void
110+
{
111+
$previousException = new Exception('Previous exception');
112+
113+
$errorDetails = [
114+
'code' => 'Neo.ClientError.Security.Unauthorized',
115+
'message' => 'Authentication failed.',
116+
'statusCode' => 401
117+
];
118+
119+
$exception = new Neo4jException($errorDetails, $errorDetails['statusCode'], $previousException);
120+
121+
$this->assertSame($previousException, $exception->getPrevious());
122+
$this->assertSame('Unauthorized', $exception->getName());
123+
}
124+
}
125+

0 commit comments

Comments
 (0)