capturedRequest = null; // Create mock HttpClient that captures request details $this->httpClient = new class($this) implements HttpClient { private $testContext; public function __construct($testContext) { $this->testContext = $testContext; } public function send(ClientRequest $request): ClientResponse { // Capture request for assertions $this->testContext->capturedRequest = $request; // Mock successful response return new ClientResponse( status: Status::OK, headers: new Headers(['Content-Type' => 'application/json']), body: '{"success": true}' ); } }; // Create mock dependencies for ApiGateway // Create mock dependencies for CircuitBreakerManager $mockCache = new class implements \App\Framework\Cache\Cache { public function get(\App\Framework\Cache\CacheIdentifier ...$identifiers): \App\Framework\Cache\CacheResult { return \App\Framework\Cache\CacheResult::empty(); } public function set(\App\Framework\Cache\CacheItem ...$items): bool { return true; } public function has(\App\Framework\Cache\CacheIdentifier ...$identifiers): array { return []; } public function delete(\App\Framework\Cache\CacheIdentifier ...$identifiers): bool { return true; } public function forget(\App\Framework\Cache\CacheIdentifier ...$identifiers): bool { return true; } public function clear(): bool { return true; } public function flush(): bool { return true; } public function remember(\App\Framework\Cache\CacheKey $key, callable $callback, ?\App\Framework\Core\ValueObjects\Duration $ttl = null): \App\Framework\Cache\CacheItem { $value = $callback(); return new \App\Framework\Cache\CacheItem($key, $value, $ttl); } }; $mockClock = new class implements \App\Framework\DateTime\Clock { public function now(): \DateTimeImmutable { return new \DateTimeImmutable(); } public function fromTimestamp(\App\Framework\Core\ValueObjects\Timestamp $timestamp): \DateTimeImmutable { return new \DateTimeImmutable('@' . $timestamp->toUnixTimestamp()); } public function fromString(string $dateTime, ?string $format = null): \DateTimeImmutable { return new \DateTimeImmutable($dateTime); } public function today(): \DateTimeImmutable { return new \DateTimeImmutable('today'); } public function yesterday(): \DateTimeImmutable { return new \DateTimeImmutable('yesterday'); } public function tomorrow(): \DateTimeImmutable { return new \DateTimeImmutable('tomorrow'); } public function time(): \App\Framework\Core\ValueObjects\Timestamp { return \App\Framework\Core\ValueObjects\Timestamp::now(); } }; $this->circuitBreakerManager = new \App\Framework\CircuitBreaker\CircuitBreakerManager( cache: $mockCache, clock: $mockClock ); $this->metrics = new \App\Framework\ApiGateway\Metrics\ApiMetrics(); $memoryMonitor = new \App\Framework\Performance\MemoryMonitor(); $this->operationTracker = new \App\Framework\Performance\OperationTracker( clock: $mockClock, memoryMonitor: $memoryMonitor, logger: null, eventDispatcher: null ); $this->apiGateway = new ApiGateway( $this->httpClient, $this->circuitBreakerManager, $this->metrics, $this->operationTracker ); }); describe('HasAuth Interface Integration', function () { it('applies Basic authentication when ApiRequest implements HasAuth', function () { $request = new class implements ApiRequest, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::basic('testuser', 'testpass'); } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.basic_auth'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->options->auth)->not->toBeNull(); expect($this->capturedRequest->options->auth->type->value)->toBe('basic'); }); it('applies custom header authentication when ApiRequest uses AuthConfig::custom()', function () { $request = new class implements ApiRequest, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::custom([ 'X-API-Key' => 'test-api-key-123', ]); } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.custom_auth'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->options->auth)->not->toBeNull(); expect($this->capturedRequest->options->auth->type->value)->toBe('custom'); }); it('does not apply authentication when ApiRequest does not implement HasAuth', function () { $request = new class implements ApiRequest { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.no_auth'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); // No auth should be applied expect($this->capturedRequest->options->auth ?? null)->toBeNull(); }); }); describe('HasPayload Interface Integration', function () { it('includes payload when ApiRequest implements HasPayload', function () { $request = new class implements ApiRequest, HasPayload, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::POST; } public function getPayload(): array { return [ 'name' => 'Test User', 'email' => 'test@example.com', ]; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::basic('testuser', 'testpass'); } public function getHeaders(): Headers { return new Headers([ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ]); } public function getRequestName(): string { return 'test.with_payload'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); $bodyData = json_decode($this->capturedRequest->body, true); expect($bodyData)->toBe([ 'name' => 'Test User', 'email' => 'test@example.com', ]); }); it('does not include payload when ApiRequest does not implement HasPayload', function () { $request = new class implements ApiRequest, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::basic('testuser', 'testpass'); } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.no_payload'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); // No payload should be sent for GET request expect($this->capturedRequest->body)->toBeEmpty(); }); }); describe('Request Name Tracking', function () { it('uses request name from ApiRequest', function () { $request = new class implements ApiRequest { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'custom.request.name'; } }; expect($request->getRequestName())->toBe('custom.request.name'); }); }); describe('HTTP Method Support', function () { it('supports GET requests', function () { $request = new class implements ApiRequest { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.get'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->method->value)->toBe('GET'); }); it('supports POST requests with payload', function () { $request = new class implements ApiRequest, HasPayload { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::POST; } public function getPayload(): array { return ['data' => 'test']; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getHeaders(): Headers { return new Headers(['Content-Type' => 'application/json']); } public function getRequestName(): string { return 'test.post'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->method->value)->toBe('POST'); $bodyData = json_decode($this->capturedRequest->body, true); expect($bodyData)->toBe(['data' => 'test']); }); it('supports DELETE requests', function () { $request = new class implements ApiRequest, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test/123')); } public function getMethod(): HttpMethod { return HttpMethod::DELETE; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::basic('user', 'pass'); } public function getHeaders(): Headers { return new Headers(['Accept' => 'application/json']); } public function getRequestName(): string { return 'test.delete'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->method->value)->toBe('DELETE'); }); it('supports PATCH requests with payload', function () { $request = new class implements ApiRequest, HasPayload, HasAuth { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test/123')); } public function getMethod(): HttpMethod { return HttpMethod::PATCH; } public function getPayload(): array { return ['name' => 'Updated']; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getAuth(): AuthConfig { return AuthConfig::basic('user', 'pass'); } public function getHeaders(): Headers { return new Headers(['Content-Type' => 'application/json']); } public function getRequestName(): string { return 'test.patch'; } }; $response = $this->apiGateway->send($request); expect($response->status)->toBe(Status::OK); expect($this->capturedRequest->method->value)->toBe('PATCH'); $bodyData = json_decode($this->capturedRequest->body, true); expect($bodyData)->toBe(['name' => 'Updated']); }); }); describe('Headers Configuration', function () { it('includes custom headers from ApiRequest', function () { $request = new class implements ApiRequest { public function getEndpoint(): ApiEndpoint { return ApiEndpoint::fromUrl(WhatwgUrl::parse('https://api.example.com/test')); } public function getMethod(): HttpMethod { return HttpMethod::GET; } public function getTimeout(): Duration { return Duration::fromSeconds(10); } public function getRetryStrategy(): ?RetryStrategy { return null; } public function getHeaders(): Headers { return new Headers([ 'Accept' => 'application/json', 'X-Custom-Header' => 'custom-value', 'X-API-Version' => '2.0', ]); } public function getRequestName(): string { return 'test.custom_headers'; } }; $headers = $request->getHeaders(); expect($headers->get('Accept'))->toBe(['application/json']); expect($headers->get('X-Custom-Header'))->toBe(['custom-value']); expect($headers->get('X-API-Version'))->toBe(['2.0']); }); }); });