diff options
Diffstat (limited to 'tests/Integration/RealisticRouteTableTest.php')
| -rw-r--r-- | tests/Integration/RealisticRouteTableTest.php | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/tests/Integration/RealisticRouteTableTest.php b/tests/Integration/RealisticRouteTableTest.php new file mode 100644 index 0000000..f93896c --- /dev/null +++ b/tests/Integration/RealisticRouteTableTest.php @@ -0,0 +1,153 @@ +<?php + +declare(strict_types=1); + +namespace BrettParson\MiniRoute\Tests\Integration; + +use ArrayObject; +use BrettParson\MiniRoute\Http\RequestInterface; +use BrettParson\MiniRoute\Http\ResponseInterface; +use BrettParson\MiniRoute\Middleware\MiddlewareInterface; +use BrettParson\MiniRoute\Middleware\MiddlewarePipeline; +use BrettParson\MiniRoute\Routing\RouteLoader; +use BrettParson\MiniRoute\Routing\RouteNotFoundException; +use BrettParson\MiniRoute\Routing\Router; +use BrettParson\MiniRoute\Tests\Fixtures\AdminLoggingMiddleware; +use BrettParson\MiniRoute\Tests\Fixtures\CsrfLoggingMiddleware; +use BrettParson\MiniRoute\Tests\Fixtures\FakeResolver; +use BrettParson\MiniRoute\Tests\Fixtures\RealisticAdminController; +use BrettParson\MiniRoute\Tests\Fixtures\RealisticPublicController; +use BrettParson\MiniRoute\Tests\Fixtures\TestRequest; +use BrettParson\MiniRoute\Tests\Fixtures\TestResponse; +use PHPUnit\Framework\TestCase; + +/** + * Compatibility suite: the realistic brett-parson.com route table. + * + * Ported from the tmp-miniroute stress test so the design requirements it + * proved (22-route table, /admin group + POST-only CSRF group, literal + * preview beating {id}, param injection) stay pinned as PHPUnit coverage. + * Bead: miniroute-4dh.1. + */ +final class RealisticRouteTableTest extends TestCase +{ + private FakeResolver $resolver; + + private Router $router; + + private ArrayObject $log; + + protected function setUp(): void + { + $this->log = new ArrayObject(); + $this->resolver = new FakeResolver(); + $this->resolver->bind(RealisticPublicController::class, new RealisticPublicController()); + $this->resolver->bind(RealisticAdminController::class, new RealisticAdminController()); + $this->resolver->bind(AdminLoggingMiddleware::class, new AdminLoggingMiddleware($this->log)); + $this->resolver->bind(CsrfLoggingMiddleware::class, new CsrfLoggingMiddleware($this->log)); + + $this->router = new Router($this->resolver); + $this->router->group('/admin', [AdminLoggingMiddleware::class]); + $this->router->group('/admin', [CsrfLoggingMiddleware::class], ['POST']); + $this->router->registerController(RealisticPublicController::class); + $this->router->registerController(RealisticAdminController::class); + } + + public function testRouteTableCompilesToTwentyTwoRoutes(): void + { + $loaded = count((new RouteLoader())->load(RealisticPublicController::class)) + + count((new RouteLoader())->load(RealisticAdminController::class)); + + $this->assertSame(22, $loaded); + } + + public function testLiteralPreviewBeatsParameterizedId(): void + { + $this->assertSame( + 'entries-preview-literal', + $this->dispatch('POST', '/admin/entries/preview')->body(), + ); + } + + public function testInjectsSlugAndIdParameters(): void + { + $this->assertSame('entry-show:hello-world', $this->dispatch('GET', '/entries/hello-world')->body()); + $this->assertSame('note-show:42', $this->dispatch('GET', '/notes/42')->body()); + $this->assertSame('entries-edit:7', $this->dispatch('GET', '/admin/entries/7/edit')->body()); + $this->assertSame('notes-edit:9', $this->dispatch('GET', '/admin/notes/9/edit')->body()); + } + + public function testLiteralSegmentsWithDotsMatch(): void + { + $this->assertSame('feed', $this->dispatch('GET', '/feed.xml')->body()); + $this->assertSame('robots', $this->dispatch('GET', '/robots.txt')->body()); + $this->assertSame('sitemap', $this->dispatch('GET', '/sitemap.xml')->body()); + } + + public function testAdminGroupThenCsrfGroupOrderOnPost(): void + { + $this->dispatch('POST', '/admin/entries/preview'); + + $this->assertSame( + ['admin:before', 'csrf:before', 'csrf:after', 'admin:after'], + $this->log->getArrayCopy(), + ); + } + + public function testCsrfGroupSkippedForGet(): void + { + $this->dispatch('GET', '/admin/notes'); + + $this->assertSame(['admin:before', 'admin:after'], $this->log->getArrayCopy()); + } + + public function testNotFoundStillThrows(): void + { + $this->expectException(RouteNotFoundException::class); + + $this->dispatch('GET', '/missing'); + } + + public function testFiveLayerOnionOrder(): void + { + $order = new ArrayObject(); + $mk = static function (string $name) use ($order): MiddlewareInterface { + return new class ($name, $order) implements MiddlewareInterface { + /** @param ArrayObject<int, string> $order */ + public function __construct( + private readonly string $name, + private readonly ArrayObject $order, + ) { + } + + public function handle(RequestInterface $request, callable $next): ResponseInterface + { + $this->order->append($this->name . ':before'); + $response = $next($request); + $this->order->append($this->name . ':after'); + + return $response; + } + }; + }; + + $core = static function (RequestInterface $request) use ($order): ResponseInterface { + $order->append('core'); + + return new TestResponse(200, 'ok'); + }; + + $pipeline = MiddlewarePipeline::compose([$mk('A'), $mk('B'), $mk('C'), $mk('D'), $mk('E')], $core); + $pipeline(new TestRequest('GET', '/')); + + $this->assertSame( + ['A:before', 'B:before', 'C:before', 'D:before', 'E:before', 'core', 'E:after', 'D:after', 'C:after', 'B:after', 'A:after'], + $order->getArrayCopy(), + ); + } + + private function dispatch(string $method, string $path): ResponseInterface + { + return $this->router->dispatch(new TestRequest($method, $path)); + } +} |
