Files
michaelschiemer/tests/Framework/ApiGateway/ApiGatewayTest.php
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

610 lines
21 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\ApiGateway\ApiGateway;
use App\Framework\ApiGateway\ApiRequest;
use App\Framework\ApiGateway\HasAuth;
use App\Framework\ApiGateway\HasPayload;
use App\Framework\ApiGateway\ValueObjects\ApiEndpoint;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Headers;
use App\Framework\Http\Method as HttpMethod;
use App\Framework\Http\Url\WhatwgUrl;
use App\Framework\HttpClient\AuthConfig;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\HttpClient;
use App\Framework\Http\Status;
use App\Framework\Retry\RetryStrategy;
describe('ApiGateway', function () {
beforeEach(function () {
// Track requests for assertion
$this->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']);
});
});
});