From 0e2b7d5377fd10aab3b2c3ea68b9a2f38a725285 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Thu, 5 Jun 2025 16:19:51 +0700 Subject: [PATCH 1/6] uppercase encrypted --- src/Libraries/Security.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Security.php b/src/Libraries/Security.php index 7fdc5df..cd3d798 100644 --- a/src/Libraries/Security.php +++ b/src/Libraries/Security.php @@ -52,7 +52,8 @@ public static function encrypt(string $plaintext): string if (!$ecrypted) { throw new \Exception("failed to encrypt string"); } - return bin2hex($iv . $ecrypted); + + return strtoupper(bin2hex($iv . $ecrypted)); } /** From 139a5f536dcdd2a75c7d4c9419102fa6cdc82b39 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Thu, 5 Jun 2025 16:21:53 +0700 Subject: [PATCH 2/6] uppercase encrypted --- src/Libraries/Security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Security.php b/src/Libraries/Security.php index cd3d798..c5feba5 100644 --- a/src/Libraries/Security.php +++ b/src/Libraries/Security.php @@ -52,7 +52,7 @@ public static function encrypt(string $plaintext): string if (!$ecrypted) { throw new \Exception("failed to encrypt string"); } - + return strtoupper(bin2hex($iv . $ecrypted)); } From 1031e2c511c18b05be04c6bebd7f2438ebeed4fa Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Fri, 6 Jun 2025 17:36:03 +0700 Subject: [PATCH 3/6] disable middleware activity for ping path --- src/Libraries/Security.php | 1 - src/Middlewares/ActivityMonitor.php | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Security.php b/src/Libraries/Security.php index c5feba5..313bb05 100644 --- a/src/Libraries/Security.php +++ b/src/Libraries/Security.php @@ -66,7 +66,6 @@ public static function encrypt(string $plaintext): string */ public static function decrypt(string $encrypted): string { - $ivHex = substr($encrypted, 0, 32); $iv = hex2bin($ivHex); $encrypted = substr($encrypted, 32); diff --git a/src/Middlewares/ActivityMonitor.php b/src/Middlewares/ActivityMonitor.php index 067ac1b..d710221 100644 --- a/src/Middlewares/ActivityMonitor.php +++ b/src/Middlewares/ActivityMonitor.php @@ -58,6 +58,9 @@ public function __construct(Context $contextService) */ public function handle($request, Closure $next) { + if ($request->path() == "ping") { + return $next($request); + } $meta = new Metadata(); $meta->api_key = $request->header('X-Api-Key'); $meta->app = $request->header('X-App'); @@ -107,6 +110,9 @@ public function handle($request, Closure $next) */ public function terminate($request, $response) { + if ($request->path() == "ping") { + return; + } $fileData = (array) $this->contextService->get('fileData'); $log = new StdClass(); $log->app_name = getenv('APP_NAME'); From 0142c4088abc8b8304fa59ee2dc353e98ebeb6c8 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 24 Jun 2025 10:18:22 +0700 Subject: [PATCH 4/6] feat : add graphql client --- .gitignore | 3 +- src/Dtos/GraphQLQueryBuilder.php | 74 +++++++++++ src/Libraries/GraphQLClient.php | 208 +++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/Dtos/GraphQLQueryBuilder.php create mode 100644 src/Libraries/GraphQLClient.php diff --git a/.gitignore b/.gitignore index 81cea5e..64f647e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .env /.vscode report_coverage.xml -/storage \ No newline at end of file +/storage +.idea \ No newline at end of file diff --git a/src/Dtos/GraphQLQueryBuilder.php b/src/Dtos/GraphQLQueryBuilder.php new file mode 100644 index 0000000..2e767c2 --- /dev/null +++ b/src/Dtos/GraphQLQueryBuilder.php @@ -0,0 +1,74 @@ + + * @license https://mit-license.org/ MIT License + * @version GIT: 0.0.4 + * @link https://github.com/spotlibs + */ + +declare(strict_types=1); + +namespace Spotlibs\PhpLib\Dtos; + +use Spotlibs\PhpLib\Exceptions\ParameterException; + +/** + * GraphQLQueryBuilder + * + * @category Library + * @package Dtos + * @author Mufthi Ryanda + * @license https://mit-license.org/ MIT License + * @link https://github.com/spotlibs + */ + +trait GraphQLQueryBuilder +{ + private array $selectedFields = []; + abstract protected function getQueryFields(): array; + abstract protected function getQueryName(): string; + abstract protected function getOperationName(): string; + + public function select(string ...$fields): self + { + $queryFields = $this->getQueryFields(); + $invalidFields = array_diff($fields, $queryFields); + + if (!empty($invalidFields)) { + throw new ParameterException( + 'Invalid field(s): ' . implode(', ', $invalidFields) . + '. Available fields: ' . implode(', ', $queryFields) + ); + } + + $this->selectedFields = array_merge($this->selectedFields, $fields); + return $this; + } + + public function toGraphQLQueryString(): string + { + $fields = empty($this->selectedFields) + ? $this->getSelectedPropertiesAsFields() + : $this->selectedFields; + + $indentedFields = array_map(fn($field) => " {$field}", $fields); + $fieldsList = implode("\n", $indentedFields); + + return sprintf( + "query %s {\n %s {\n%s\n }\n}", + $this->getQueryName(), + $this->getOperationName(), + $fieldsList + ); + } + + private function getSelectedPropertiesAsFields(): array + { + return $this->getQueryFields(); + } +} \ No newline at end of file diff --git a/src/Libraries/GraphQLClient.php b/src/Libraries/GraphQLClient.php new file mode 100644 index 0000000..ce158c4 --- /dev/null +++ b/src/Libraries/GraphQLClient.php @@ -0,0 +1,208 @@ + + * @license https://mit-license.org/ MIT License + * @version GIT: 0.3.7 + * @link https://github.com/spotlibs + */ + +declare(strict_types=1); + +namespace Spotlibs\PhpLib\Libraries; + +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Psr7\Request; +use Psr\Http\Message\ResponseInterface; +use Spotlibs\PhpLib\Logs\Log; + +/** + * GraphQLClient + * + * Name for GraphQLClient + * + * @category HttpClient + * @package Client + * @author Mufthi Ryanda + * @license https://mit-license.org/ MIT License + * @link https://github.com/spotlibs + */ + +class GraphQLClient +{ + /** + * HTTP client for making requests + * + * @var GuzzleClient $httpClient + */ + private GuzzleClient $httpClient; + + /** + * GraphQL endpoint URL + * + * @var string $endpoint + */ + private string $endpoint; + + /** + * Request headers + * + * @var array $headers + */ + private array $headers = []; + + /** + * Basic auth credentials + * + * @var array $basicAuth + */ + private array $basicAuth = []; + + /** + * Create a new GraphQLClient instance. + * + * @param string $endpoint GraphQL endpoint URL + * @param array $config Additional GuzzleHttp client configuration + * + * @return void + */ + public function __construct(string $endpoint, array $config = []) + { + $this->endpoint = $endpoint; + + $defaultConfig = [ + 'timeout' => 10, + 'verify' => false, + ]; + + $this->httpClient = new GuzzleClient(array_merge($defaultConfig, $config)); + + // Set default headers for GraphQL + $this->headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + } + + /** + * Set basic authentication credentials + * + * @param string $username Username for basic auth + * @param string $password Password for basic auth + * + * @return self + */ + public function setBasicAuth(string $username, string $password): self + { + $this->basicAuth = [$username, $password]; + return $this; + } + + /** + * Set JWT bearer token authentication + * + * @param string $token JWT token + * + * @return self + */ + public function setBearerToken(string $token): self + { + $this->headers['Authorization'] = 'Bearer ' . $token; + return $this; + } + + /** + * Set additional request headers + * + * @param array $headers Associative array of headers + * + * @return self + */ + public function setHeaders(array $headers): self + { + $this->headers = array_merge($this->headers, $headers); + return $this; + } + + /** + * Execute GraphQL query + * + * @param string $query GraphQL query string + * @param array|null $variables Optional variables for the query + * @param string|null $operationName Optional operation name + * + * @return ResponseInterface GraphQL response + */ + public function query(string $query, ?array $variables = null, ?string $operationName = null): ResponseInterface + { + $startTime = microtime(true); + + $body = ['query' => $query]; + + if ($variables !== null) { + $body['variables'] = $variables; + } + + if ($operationName !== null) { + $body['operationName'] = $operationName; + } + + $jsonBody = json_encode($body, JSON_THROW_ON_ERROR); + + $request = new Request('POST', $this->endpoint, $this->headers, $jsonBody); + + $options = []; + + // Add basic auth if set + if (!empty($this->basicAuth)) { + $options['auth'] = $this->basicAuth; + } + + $response = $this->httpClient->send($request, $options); + + $elapsed = microtime(true) - $startTime; + + if (env('APP_DEBUG', false)) { + $request->getBody()->rewind(); + $reqBody = $request->getBody()->getContents(); + $respBody = $response->getBody()->getContents(); + + if (strlen($reqBody) > 5000) { + $reqBody = "more than 5000 characters"; + } + if (strlen($respBody) > 5000) { + $respBody = "more than 5000 characters"; + } + + $logData = [ + 'host' => $request->getUri()->getHost(), + 'url' => $request->getUri()->getPath(), + 'graphql' => [ + 'query' => $query, + 'variables' => $variables, + 'operationName' => $operationName, + ], + 'request' => [ + 'method' => $request->getMethod(), + 'headers' => $request->getHeaders(), + 'body' => json_decode($reqBody, true), + ], + 'response' => [ + 'headers' => $response->getHeaders(), + 'body' => json_decode($respBody, true), + ], + 'responseTime' => round($elapsed * 1000), + 'memoryUsage' => memory_get_usage() + ]; + + $response->getBody()->rewind(); + Log::activity()->info($logData); + } + + return $response; + } +} From f432344591ffa53760c953a12c180a7c233a2642 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 24 Jun 2025 13:18:38 +0700 Subject: [PATCH 5/6] chore : adjust linter --- src/Dtos/GraphQLQueryBuilder.php | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Dtos/GraphQLQueryBuilder.php b/src/Dtos/GraphQLQueryBuilder.php index 2e767c2..15a37cc 100644 --- a/src/Dtos/GraphQLQueryBuilder.php +++ b/src/Dtos/GraphQLQueryBuilder.php @@ -30,10 +30,37 @@ trait GraphQLQueryBuilder { private array $selectedFields = []; + + /** + * Get available query fields + * + * @return array Array of available field names + */ abstract protected function getQueryFields(): array; + + /** + * Get the GraphQL query name + * + * @return string The query name + */ abstract protected function getQueryName(): string; + + /** + * Get the GraphQL operation name + * + * @return string The operation name + */ abstract protected function getOperationName(): string; + /** + * Select specific fields for the GraphQL query + * + * @param string ...$fields The field names to select + * + * @return self Returns the current instance for method chaining + * + * @throws ParameterException When invalid fields are provided + */ public function select(string ...$fields): self { $queryFields = $this->getQueryFields(); @@ -50,6 +77,11 @@ public function select(string ...$fields): self return $this; } + /** + * Convert the query to a GraphQL query string + * + * @return string The formatted GraphQL query string + */ public function toGraphQLQueryString(): string { $fields = empty($this->selectedFields) @@ -67,8 +99,13 @@ public function toGraphQLQueryString(): string ); } + /** + * Get selected properties as GraphQL fields + * + * @return array Array of field names + */ private function getSelectedPropertiesAsFields(): array { return $this->getQueryFields(); } -} \ No newline at end of file +} From 6c5cb2d33615882d33bcf7384e7f277040e5fac2 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Wed, 25 Jun 2025 09:05:30 +0700 Subject: [PATCH 6/6] feat : add unit test --- tests/Dtos/GraphQLQueryBuilderTest.php | 96 ++++++++ tests/Dtos/MockGraphQLQuery.php | 32 +++ tests/Libraries/GraphQLClientTest.php | 294 +++++++++++++++++++++++++ 3 files changed, 422 insertions(+) create mode 100644 tests/Dtos/GraphQLQueryBuilderTest.php create mode 100644 tests/Dtos/MockGraphQLQuery.php create mode 100644 tests/Libraries/GraphQLClientTest.php diff --git a/tests/Dtos/GraphQLQueryBuilderTest.php b/tests/Dtos/GraphQLQueryBuilderTest.php new file mode 100644 index 0000000..6ea0f8d --- /dev/null +++ b/tests/Dtos/GraphQLQueryBuilderTest.php @@ -0,0 +1,96 @@ +select('name', 'email'); + + $this->assertEquals($query, $result); + $this->assertEquals(['name', 'email'], $query->getSelectedFields()); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testSelectMultipleCallsAppendFields() + { + $query = new MockGraphQLQuery(); + $query->select('name') + ->select('email', 'age'); + + $this->assertEquals(['name', 'email', 'age'], $query->getSelectedFields()); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testSelectInvalidFieldsThrowsException() + { + $this->expectException(ParameterException::class); + $this->expectExceptionMessage('Invalid field(s): invalid, unknown. Available fields: name, email, age, active'); + + $query = new MockGraphQLQuery(); + $query->select('name', 'invalid', 'unknown'); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testToGraphQLQueryStringWithSelectedFields() + { + $query = new MockGraphQLQuery(); + $query->select('name', 'email'); + $result = $query->toGraphQLQueryString(); + + $expected = "query TestQuery {\n testOperation {\n name\n email\n }\n}"; + $this->assertEquals($expected, $result); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testToGraphQLQueryStringWithoutSelectedFields() + { + $query = new MockGraphQLQuery(); + $result = $query->toGraphQLQueryString(); + + $expected = "query TestQuery {\n testOperation {\n name\n email\n age\n active\n }\n}"; + $this->assertEquals($expected, $result); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testToGraphQLQueryStringFormat() + { + $query = new MockGraphQLQuery(); + $query->select('name'); + $result = $query->toGraphQLQueryString(); + + $this->assertStringContainsString('query TestQuery', $result); + $this->assertStringContainsString('testOperation', $result); + $this->assertStringContainsString(' name', $result); + } + + /** @test */ + /** @runInSeparateProcess */ + public function testSelectEmptyFieldsArray() + { + $query = new MockGraphQLQuery(); + $result = $query->select(); + + $this->assertEquals($query, $result); + $this->assertEquals([], $query->getSelectedFields()); + } +} \ No newline at end of file diff --git a/tests/Dtos/MockGraphQLQuery.php b/tests/Dtos/MockGraphQLQuery.php new file mode 100644 index 0000000..22c51a4 --- /dev/null +++ b/tests/Dtos/MockGraphQLQuery.php @@ -0,0 +1,32 @@ +selectedFields; + } +} \ No newline at end of file diff --git a/tests/Libraries/GraphQLClientTest.php b/tests/Libraries/GraphQLClientTest.php new file mode 100644 index 0000000..e627e2d --- /dev/null +++ b/tests/Libraries/GraphQLClientTest.php @@ -0,0 +1,294 @@ + 'application/json'], json_encode([ + 'data' => ['user' => ['id' => '1', 'name' => 'John Doe']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query('{ user(id: "1") { id name } }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('John Doe', $contents['data']['user']['name']); + } + + public function testQueryWithVariables(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['user' => ['id' => '123', 'email' => 'test@example.com']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query( + 'query GetUser($id: ID!) { user(id: $id) { id email } }', + ['id' => '123'] + ); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('test@example.com', $contents['data']['user']['email']); + } + + public function testQueryWithOperationName(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['posts' => [['id' => '1', 'title' => 'Test Post']]] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query( + 'query GetPosts { posts { id title } } query GetUsers { users { id name } }', + null, + 'GetPosts' + ); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('Test Post', $contents['data']['posts'][0]['title']); + } + + public function testQueryWithVariablesAndOperationName(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['createUser' => ['id' => '456', 'name' => 'Jane Doe']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query( + 'mutation CreateUser($input: UserInput!) { createUser(input: $input) { id name } }', + ['input' => ['name' => 'Jane Doe', 'email' => 'jane@example.com']], + 'CreateUser' + ); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('Jane Doe', $contents['data']['createUser']['name']); + } + + public function testBasicAuth(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['authenticated' => true] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $client->setBasicAuth('username', 'password'); + $response = $client->query('{ authenticated }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertTrue($contents['data']['authenticated']); + } + + public function testBearerToken(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['user' => ['role' => 'admin']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $client->setBearerToken('jwt-token-here'); + $response = $client->query('{ user { role } }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('admin', $contents['data']['user']['role']); + } + + public function testCustomHeaders(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['success' => true] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $client->setHeaders(['X-Custom-Header' => 'custom-value', 'X-API-Version' => '2.0']); + $response = $client->query('{ success }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertTrue($contents['data']['success']); + } + + public function testConnectionError(): void + { + $this->expectException(GuzzleException::class); + + $request = new Request('POST', 'https://api.example.com/graphql'); + $mock = new MockHandler([ + new ConnectException('Connection failed', $request) + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $client->query('{ user { id } }'); + } + + public function testHttpError(): void + { + $mock = new MockHandler([ + new Response(500, ['Content-Type' => 'application/json'], json_encode([ + 'errors' => [['message' => 'Internal server error']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query('{ user { id } }'); + + $this->assertEquals(500, $response->getStatusCode()); + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('Internal server error', $contents['errors'][0]['message']); + } + + public function testGraphQLErrors(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => null, + 'errors' => [['message' => 'Field "nonexistent" not found']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query('{ nonexistent }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertNull($contents['data']); + $this->assertEquals('Field "nonexistent" not found', $contents['errors'][0]['message']); + } + + public function testDebugLogging(): void + { + putenv('APP_DEBUG=true'); + + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['user' => ['id' => '1', 'name' => 'Debug User']] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query('{ user(id: "1") { id name } }', ['debug' => true]); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('Debug User', $contents['data']['user']['name']); + + putenv('APP_DEBUG=false'); + } + + public function testLargeBodyTruncation(): void + { + putenv('APP_DEBUG=true'); + + // Create large response body (over 5000 characters) + $largeData = []; + for ($i = 0; $i < 20; $i++) { + $largeData["message$i"] = str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ', 10); + } + + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => $largeData + ])), + ]); + $handlerStack = new HandlerStack($mock); + + // Create large query variables + $largeVariables = []; + for ($i = 0; $i < 20; $i++) { + $largeVariables["input$i"] = str_repeat('This is a very long input string that will make the request body exceed 5000 characters. ', 20); + } + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client->query( + 'mutation CreateLargeData($input: LargeInput!) { createData(input: $input) { success } }', + $largeVariables + ); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertArrayHasKey('data', $contents); + + putenv('APP_DEBUG=false'); + } + + public function testCustomConfig(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['config' => 'custom'] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', [ + 'handler' => $handlerStack, + 'timeout' => 30, + 'verify' => true, + 'connect_timeout' => 5 + ]); + + $response = $client->query('{ config }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('custom', $contents['data']['config']); + } + + public function testChainedAuthentication(): void + { + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode([ + 'data' => ['authenticated' => true, 'role' => 'admin'] + ])), + ]); + $handlerStack = new HandlerStack($mock); + + $client = new GraphQLClient('https://api.example.com/graphql', ['handler' => $handlerStack]); + $response = $client + ->setBearerToken('jwt-token') + ->setHeaders(['X-API-Key' => 'api-key-123']) + ->query('{ authenticated role }'); + + $contents = json_decode($response->getBody()->getContents(), true); + $this->assertTrue($contents['data']['authenticated']); + $this->assertEquals('admin', $contents['data']['role']); + } +} \ No newline at end of file