'application/json', 'Accept' => 'application/json', 'x-openstatus-key' => 'secret', ], ); $request = new CheckRequest(['service' => 'frontend']); $result = $transport->call( 'openstatus.health.v1.HealthService', 'Check', $request, CheckResponse::class, ['x-trace-id' => 'trace-123'], ); self::assertInstanceOf(CheckResponse::class, $result); self::assertSame(ServingStatus::SERVING_STATUS_SERVING, $result->getStatus()); $sent = $http->lastRequest; self::assertNotNull($sent); self::assertSame('POST', $sent->getMethod()); self::assertSame( 'https://api.openstatus.dev/rpc/openstatus.health.v1.HealthService/Check', (string) $sent->getUri(), ); self::assertSame('application/json', $sent->getHeaderLine('Content-Type')); self::assertSame('application/json', $sent->getHeaderLine('Accept')); self::assertSame('secret', $sent->getHeaderLine('x-openstatus-key')); self::assertSame('trace-123', $sent->getHeaderLine('x-trace-id')); self::assertSame('{"service":"frontend"}', (string) $sent->getBody()); } public function testCallDeserializesResponseBody(): void { $http = new MockHttpClient(new Response(200, [], '{"status":2}')); $factory = new HttpFactory(); $transport = $this->makeTransport($http, $factory); $result = $transport->call( 'openstatus.health.v1.HealthService', 'Check', new CheckRequest(), CheckResponse::class, ); self::assertSame(ServingStatus::SERVING_STATUS_NOT_SERVING, $result->getStatus()); } public function testCallMapsAuthenticationError(): void { $body = json_encode(['code' => 'unauthenticated', 'message' => 'bad key']); self::assertIsString($body); $http = new MockHttpClient(new Response(401, [], $body)); $transport = $this->makeTransport($http, new HttpFactory()); $this->expectException(AuthenticationException::class); $this->expectExceptionMessage('bad key'); $transport->call( 'openstatus.health.v1.HealthService', 'Check', new CheckRequest(), CheckResponse::class, ); } public function testCallMapsNotFoundError(): void { $body = json_encode(['code' => 'not_found', 'message' => 'monitor 1 not found']); self::assertIsString($body); $http = new MockHttpClient(new Response(404, [], $body)); $transport = $this->makeTransport($http, new HttpFactory()); try { $transport->call( 'openstatus.monitor.v1.MonitorService', 'GetMonitor', new CheckRequest(), CheckResponse::class, ); } catch (NotFoundException $e) { self::assertSame('monitor 1 not found', $e->getMessage()); self::assertSame('not_found', $e->connectCode()); self::assertSame(404, $e->httpStatus()); self::assertSame($body, $e->rawBody()); self::assertSame([], $e->details()); } } public function testCallWrapsTransportExceptionAsServiceUnavailable(): void { $clientException = new class ('connection refused') extends \RuntimeException implements ClientExceptionInterface { }; $http = new MockHttpClient($clientException); $transport = $this->makeTransport($http, new HttpFactory()); try { $transport->call( 'openstatus.health.v1.HealthService', 'Check', new CheckRequest(), CheckResponse::class, ); } catch (ServiceUnavailableException $e) { self::assertStringContainsString('connection refused', $e->getMessage()); self::assertSame(0, $e->httpStatus()); self::assertSame($clientException, $e->getPrevious()); } } public function testCallTrimsTrailingSlashOnBaseUrl(): void { $http = new MockHttpClient(new Response(200, [], '{}')); $factory = new HttpFactory(); $transport = new JsonTransport( http: $http, requestFactory: $factory, streamFactory: $factory, baseUrl: 'https://api.openstatus.dev///', defaultHeaders: [], ); $transport->call( 'openstatus.health.v1.HealthService', 'Check', new CheckRequest(), CheckResponse::class, ); $sent = $http->lastRequest; self::assertNotNull($sent); self::assertSame( 'https://api.openstatus.dev/rpc/openstatus.health.v1.HealthService/Check', (string) $sent->getUri(), ); } private function makeTransport(MockHttpClient $http, HttpFactory $factory): JsonTransport { return new JsonTransport( http: $http, requestFactory: $factory, streamFactory: $factory, baseUrl: 'https://api.openstatus.dev', defaultHeaders: [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], ); } }