refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure: - Remove docker-compose-direct-access.yml (VPN-bound services) - Remove VPN-only middlewares from Grafana, Prometheus, Portainer - Remove WireGuard middleware definitions from Traefik - Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers All monitoring services now publicly accessible via subdomains: - grafana.michaelschiemer.de (with Grafana native auth) - prometheus.michaelschiemer.de (with Basic Auth) - portainer.michaelschiemer.de (with Portainer native auth) All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
634
tests/Framework/ApiGateway/ApiGatewayTest.php
Normal file
634
tests/Framework/ApiGateway/ApiGatewayTest.php
Normal file
@@ -0,0 +1,634 @@
|
||||
<?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\Url;
|
||||
use App\Framework\HttpClient\AuthConfig;
|
||||
use App\Framework\HttpClient\ClientRequest;
|
||||
use App\Framework\HttpClient\ClientResponse;
|
||||
use App\Framework\HttpClient\HttpClient;
|
||||
use App\Framework\HttpClient\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 new \App\Framework\Cache\CacheResult(hits: [], misses: $identifiers);
|
||||
}
|
||||
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();
|
||||
|
||||
$this->operationTracker = new class implements \App\Framework\Performance\OperationTracker {
|
||||
public function startOperation(
|
||||
string $operationId,
|
||||
\App\Framework\Performance\PerformanceCategory $category,
|
||||
array $contextData = []
|
||||
): \App\Framework\Performance\PerformanceSnapshot {
|
||||
return new \App\Framework\Performance\PerformanceSnapshot(
|
||||
operationId: $operationId,
|
||||
category: $category,
|
||||
startTime: microtime(true),
|
||||
duration: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(10),
|
||||
memoryUsed: 1024,
|
||||
peakMemory: 2048,
|
||||
contextData: $contextData
|
||||
);
|
||||
}
|
||||
|
||||
public function completeOperation(string $operationId): ?\App\Framework\Performance\PerformanceSnapshot {
|
||||
return new \App\Framework\Performance\PerformanceSnapshot(
|
||||
operationId: $operationId,
|
||||
category: \App\Framework\Performance\PerformanceCategory::HTTP,
|
||||
startTime: microtime(true) - 0.01,
|
||||
duration: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(10),
|
||||
memoryUsed: 1024,
|
||||
peakMemory: 2048,
|
||||
contextData: []
|
||||
);
|
||||
}
|
||||
|
||||
public function failOperation(string $operationId, \Throwable $exception): ?\App\Framework\Performance\PerformanceSnapshot {
|
||||
return 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(Url::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)->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(Url::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)->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(Url::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(Url::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(Url::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(Url::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(Url::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)->toBe('GET');
|
||||
});
|
||||
|
||||
it('supports POST requests with payload', function () {
|
||||
$request = new class implements ApiRequest, HasPayload {
|
||||
public function getEndpoint(): ApiEndpoint
|
||||
{
|
||||
return ApiEndpoint::fromUrl(Url::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)->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(Url::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)->toBe('DELETE');
|
||||
});
|
||||
|
||||
it('supports PATCH requests with payload', function () {
|
||||
$request = new class implements ApiRequest, HasPayload, HasAuth {
|
||||
public function getEndpoint(): ApiEndpoint
|
||||
{
|
||||
return ApiEndpoint::fromUrl(Url::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)->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(Url::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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user