refactor(kernel): unify HTTP and CLI application lifecycle
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<?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\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))->handle(Request::create('/'));
|
||||
|
||||
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))->handle(Request::create('/'));
|
||||
} 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))->handle(Request::create('/'));
|
||||
|
||||
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 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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user