eb99eb5a2e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
260 lines
7.6 KiB
PHP
260 lines
7.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Unit\Runtime;
|
|
|
|
use KTXC\Application\Execution\ExecutionContext;
|
|
use KTXC\Application\Execution\ExecutionDescriptor;
|
|
use KTXC\Application\Execution\ExecutionOutcome;
|
|
use KTXC\Application\Execution\ExecutionRunner;
|
|
use KTXC\Application\Execution\ExecutionRunnerInterface;
|
|
use KTXC\Application\Execution\ExecutionScope;
|
|
use KTXC\Application\Execution\TerminationReport;
|
|
use KTXC\Context\IdentityContext;
|
|
use KTXC\Context\TenantContext;
|
|
use KTXC\Http\Middleware\AuthenticationMiddleware;
|
|
use KTXC\Http\Middleware\FirewallMiddleware;
|
|
use KTXC\Http\Middleware\MiddlewareInterface;
|
|
use KTXC\Http\Middleware\RequestHandlerInterface;
|
|
use KTXC\Http\Middleware\RouterMiddleware;
|
|
use KTXC\Http\Middleware\TenantMiddleware;
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Http\Response\Response;
|
|
use KTXC\Http\Response\StreamedResponse;
|
|
use KTXC\KernelInterface;
|
|
use KTXC\Runtime\Http\HttpRuntime;
|
|
use KTXC\Service\TenantService;
|
|
use PHPUnit\Framework\TestCase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\Attributes\TestDox;
|
|
use Psr\Container\ContainerInterface;
|
|
|
|
final class HttpRuntimeTest extends TestCase
|
|
{
|
|
#[Test]
|
|
#[TestDox('Stream callbacks run before execution termination')]
|
|
public function streams(): void
|
|
{
|
|
$kernel = null;
|
|
$kernel = new RuntimeKernel(
|
|
new RuntimeContainer([
|
|
TenantMiddleware::class => new PassMiddleware(),
|
|
FirewallMiddleware::class => new PassMiddleware(),
|
|
AuthenticationMiddleware::class => new PassMiddleware(),
|
|
RouterMiddleware::class => new StreamMiddleware(
|
|
static function () use (&$kernel): bool {
|
|
return $kernel->active;
|
|
},
|
|
),
|
|
]),
|
|
new TenantContext($this->createStub(TenantService::class)),
|
|
new IdentityContext(),
|
|
);
|
|
|
|
ob_start();
|
|
try {
|
|
(new HttpRuntime($kernel, false))->run(Request::create('/stream'));
|
|
$content = ob_get_contents();
|
|
} finally {
|
|
ob_end_clean();
|
|
}
|
|
|
|
self::assertSame('active', $content);
|
|
self::assertFalse($kernel->active);
|
|
self::assertSame(1, $kernel->terminations);
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Successful HTTP handling terminates exactly once')]
|
|
public function succeeds(): void
|
|
{
|
|
$kernel = $this->kernel(new ResponseMiddleware(new Response('ok', 200)));
|
|
$response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false);
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('ok', $response->getContent());
|
|
self::assertSame(1, $kernel->terminations);
|
|
self::assertTrue($kernel->outcome?->successful);
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('HTTP exceptions render a response and still terminate')]
|
|
public function fails(): void
|
|
{
|
|
$kernel = $this->kernel(new ThrowingMiddleware());
|
|
$previousLog = ini_get('error_log');
|
|
ini_set('error_log', '/dev/null');
|
|
try {
|
|
$response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false);
|
|
} finally {
|
|
ini_set('error_log', (string) $previousLog);
|
|
}
|
|
|
|
self::assertSame(500, $response->getStatusCode());
|
|
self::assertSame(1, $kernel->terminations);
|
|
self::assertFalse($kernel->outcome?->successful);
|
|
self::assertInstanceOf(\RuntimeException::class, $kernel->outcome?->error);
|
|
}
|
|
|
|
#[Test]
|
|
#[TestDox('Deferred termination failures do not replace the HTTP response')]
|
|
public function preservesResponses(): void
|
|
{
|
|
$kernel = $this->kernel(new ResponseMiddleware(new Response('accepted', 202)));
|
|
$kernel->terminationReport = new TerminationReport(
|
|
failures: [new \RuntimeException('Deferred listener failed.')],
|
|
);
|
|
|
|
$response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false);
|
|
|
|
self::assertSame(202, $response->getStatusCode());
|
|
self::assertSame('accepted', $response->getContent());
|
|
}
|
|
|
|
private function kernel(MiddlewareInterface $terminal): RuntimeKernel
|
|
{
|
|
return new RuntimeKernel(
|
|
new RuntimeContainer([
|
|
TenantMiddleware::class => new PassMiddleware(),
|
|
FirewallMiddleware::class => new PassMiddleware(),
|
|
AuthenticationMiddleware::class => new PassMiddleware(),
|
|
RouterMiddleware::class => $terminal,
|
|
]),
|
|
new TenantContext($this->createStub(TenantService::class)),
|
|
new IdentityContext(),
|
|
);
|
|
}
|
|
}
|
|
|
|
final class RuntimeKernel implements KernelInterface
|
|
{
|
|
public bool $active = false;
|
|
public int $terminations = 0;
|
|
public ?ExecutionOutcome $outcome = null;
|
|
public TerminationReport $terminationReport;
|
|
|
|
public function __construct(
|
|
private readonly ContainerInterface $services,
|
|
private readonly TenantContext $tenantContext,
|
|
private readonly IdentityContext $identityContext,
|
|
) {
|
|
$this->terminationReport = new TerminationReport();
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
}
|
|
|
|
public function executionRunner(): ExecutionRunnerInterface
|
|
{
|
|
return new ExecutionRunner($this);
|
|
}
|
|
|
|
public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope
|
|
{
|
|
$this->active = true;
|
|
|
|
return new ExecutionScope(
|
|
$descriptor,
|
|
ExecutionContext::fromDescriptor($descriptor),
|
|
$this->tenantContext,
|
|
$this->identityContext,
|
|
);
|
|
}
|
|
|
|
public function terminateExecution(
|
|
ExecutionScope $scope,
|
|
ExecutionOutcome $outcome,
|
|
): TerminationReport {
|
|
$this->active = false;
|
|
$this->terminations++;
|
|
$this->outcome = $outcome;
|
|
$scope->dispose();
|
|
$scope->markTerminated();
|
|
|
|
return $this->terminationReport;
|
|
}
|
|
|
|
public function shutdown(): void
|
|
{
|
|
}
|
|
|
|
public function container(): ContainerInterface
|
|
{
|
|
return $this->services;
|
|
}
|
|
|
|
public function environment(): string
|
|
{
|
|
return 'test';
|
|
}
|
|
|
|
public function debug(): bool
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
final class RuntimeContainer implements ContainerInterface
|
|
{
|
|
public function __construct(
|
|
private readonly array $services,
|
|
) {
|
|
}
|
|
|
|
public function get(string $id): mixed
|
|
{
|
|
return $this->services[$id];
|
|
}
|
|
|
|
public function has(string $id): bool
|
|
{
|
|
return isset($this->services[$id]);
|
|
}
|
|
}
|
|
|
|
class PassMiddleware implements MiddlewareInterface
|
|
{
|
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
|
{
|
|
return $handler->handle($request);
|
|
}
|
|
}
|
|
|
|
final class StreamMiddleware extends PassMiddleware
|
|
{
|
|
public function __construct(
|
|
private readonly \Closure $active,
|
|
) {
|
|
}
|
|
|
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
|
{
|
|
return new StreamedResponse(function (): void {
|
|
echo ($this->active)() ? 'active' : 'terminated';
|
|
});
|
|
}
|
|
}
|
|
|
|
final class ResponseMiddleware extends PassMiddleware
|
|
{
|
|
public function __construct(
|
|
private readonly Response $response,
|
|
) {
|
|
}
|
|
|
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
|
{
|
|
return $this->response;
|
|
}
|
|
}
|
|
|
|
final class ThrowingMiddleware extends PassMiddleware
|
|
{
|
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
|
{
|
|
throw new \RuntimeException('HTTP failed.');
|
|
}
|
|
}
|