Keep configured auth transports when faking an actor

withActor() replaced the whole TransportRegistry with a fresh one holding a
single transport named "testing". AuthTransportMiddleware, which is how
HttpAuthBootloader wires auth into an application, asks the registry for a
transport by name, so a request through it died with

    Spiral\Auth\Exception\TransportException: Undefined auth transport header

AuthMiddleware kept working because it iterates over every transport, which is
why the failure looked like it only happened with a custom transport.

The fake registry is now built from the names the application has configured,
each mapped to the same fake HeaderTransport that reads X-Test-Token, so
whichever name the middleware asks for it gets a transport that finds the test
token. "testing" stays and becomes the default. An application with no registry
bound behaves as before.

The test app had no withActor() coverage at all, so the three tests come with
two middleware stubs: one that runs AuthTransportMiddleware('header') resolved
from the http scope, one that reports the actor into the response body.

For spiral/framework#1231
This commit is contained in:
dualfroz committed 2026-09-13 15:23:26 +02:00
1 parent 20591647eb
commit 86d47b17ae
4 files changed
+144 -3

No files matched your search

+26 -3
View File
@@ -421,9 +421,7 @@ class FakeHttp
$bindings[ActorProviderInterface::class] = new FakeActorProvider($this->actor);
$bindings[TokenStorageInterface::class] = new FakeTokenStorage();
$transport = new TransportRegistry();
$transport->setTransport('testing', new HeaderTransport(static::AUTH_TOKEN_HEADER_KEY));
$bindings[TransportRegistry::class] = $transport;
$bindings[TransportRegistry::class] = $this->createFakeTransportRegistry();
}
if ($this->session) {
@@ -461,4 +459,29 @@ class FakeHttp
throw new \InvalidArgumentException('$data should be an array or an object.');
}
}
/**
* The fake token travels in the {@see self::AUTH_TOKEN_HEADER_KEY} header, so every transport the
* application has configured is replaced by one that reads it. The configured names are kept because
* {@see \Spiral\Auth\Middleware\AuthTransportMiddleware} resolves a transport by name.
*/
private function createFakeTransportRegistry(): TransportRegistry
{
$registry = new TransportRegistry();
$transport = new HeaderTransport(static::AUTH_TOKEN_HEADER_KEY);
try {
$configured = $this->container->get(TransportRegistry::class);
foreach (\array_keys($configured->getTransports()) as $name) {
$registry->setTransport((string) $name, $transport);
}
} catch (\Throwable) {
// Nothing to copy the names from; the fake transport is all there is.
}
$registry->setTransport('testing', $transport);
$registry->setDefaultTransport('testing');
return $registry;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Spiral\Testing\Tests\Http;
use Spiral\Auth\Middleware\AuthMiddleware;
use Spiral\Bootloader\Auth\HttpAuthBootloader;
use Spiral\Testing\Tests\Http\Stub\ActorReportingMiddleware;
use Spiral\Testing\Tests\Http\Stub\HeaderAuthMiddleware;
use Spiral\Testing\Tests\TestCase;
final class FakeHttpAuthTransportTest extends TestCase
{
public function defineBootloaders(): array
{
return [...parent::defineBootloaders(), HttpAuthBootloader::class];
}
public function testActorIsAuthenticatedThroughNamedTransport(): void
{
$actor = new \stdClass();
$response = $this->fakeHttp()
->withActor($actor)
->withMiddleware(ActorReportingMiddleware::class, HeaderAuthMiddleware::class)
->get('/get/query-params');
$response->assertOk();
$response->assertBodySame($actor::class);
}
public function testActorIsAuthenticatedThroughEveryTransport(): void
{
$actor = new \stdClass();
$response = $this->fakeHttp()
->withActor($actor)
->withMiddleware(ActorReportingMiddleware::class, AuthMiddleware::class)
->get('/get/query-params');
$response->assertOk();
$response->assertBodySame($actor::class);
}
public function testRequestWithoutActorStaysUnauthenticated(): void
{
$response = $this->fakeHttp()
->withMiddleware(ActorReportingMiddleware::class, HeaderAuthMiddleware::class)
->get('/get/query-params');
$response->assertOk();
$response->assertBodySame(ActorReportingMiddleware::NO_ACTOR);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Spiral\Testing\Tests\Http\Stub;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Auth\AuthContextInterface;
use Spiral\Auth\Middleware\AuthMiddleware;
final class ActorReportingMiddleware implements MiddlewareInterface
{
public const NO_ACTOR = 'guest';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$context = $request->getAttribute(AuthMiddleware::ATTRIBUTE);
$actor = $context instanceof AuthContextInterface ? $context->getActor() : null;
return new Response(200, body: $actor === null ? self::NO_ACTOR : $actor::class);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Spiral\Testing\Tests\Http\Stub;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Auth\ActorProviderInterface;
use Spiral\Auth\Middleware\AuthTransportMiddleware;
use Spiral\Auth\TokenStorageInterface;
use Spiral\Auth\TransportRegistry;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\ScopeInterface;
final class HeaderAuthMiddleware implements MiddlewareInterface
{
public function __construct(
#[Proxy] private readonly ContainerInterface $container,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$middleware = new AuthTransportMiddleware(
'header',
$this->container->get(ScopeInterface::class),
$this->container->get(ActorProviderInterface::class),
$this->container->get(TokenStorageInterface::class),
$this->container->get(TransportRegistry::class),
);
return $middleware->process($request, $handler);
}
}