index/tests/RouterTest.php

232 lines
12 KiB
PHP
Raw Normal View History

2022-09-13 15:13:11 +02:00
<?php
// RouterTest.php
// Created: 2022-01-20
2025-03-12 20:56:48 +00:00
// Updated: 2025-03-12
2022-09-13 15:13:11 +02:00
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
2025-03-12 20:56:48 +00:00
use Index\Http\{HttpHeaders,HttpResponseBuilder,HttpRequest,HttpUri};
2025-03-07 23:39:15 +00:00
use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon,Router};
use Index\Http\Routing\Filters\{FilterInfo,PrefixFilter};
use Index\Http\Routing\Processors\{After,Before,Postprocessor,Preprocessor,ProcessorInfo};
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute,RouteInfo};
2025-03-12 20:56:48 +00:00
use Index\Http\Streams\{NullStream,Stream};
2022-09-13 15:13:11 +02:00
/**
2024-03-30 16:24:34 +00:00
* This test isn't super representative of the current functionality
* it mostly just does the same tests that were done against the previous implementation
2022-09-13 15:13:11 +02:00
*/
#[CoversClass(Router::class)]
#[CoversClass(RouteInfo::class)]
#[CoversClass(PrefixFilter::class)]
#[CoversClass(ExactRoute::class)]
#[CoversClass(RouteHandler::class)]
#[CoversClass(RouteHandlerCommon::class)]
#[CoversClass(PatternRoute::class)]
#[CoversClass(FilterInfo::class)]
2022-09-13 15:13:11 +02:00
final class RouterTest extends TestCase {
public function testRouter(): void {
$router1 = new Router;
2024-03-30 16:24:34 +00:00
$router1->route(RouteInfo::exact('GET', '/', fn() => 'get'));
$router1->route(RouteInfo::exact('POST', '/', fn() => 'post'));
$router1->route(RouteInfo::exact('DELETE', '/', fn() => 'delete'));
$router1->route(RouteInfo::exact('PATCH', '/', fn() => 'patch'));
$router1->route(RouteInfo::exact('PUT', '/', fn() => 'put'));
$router1->route(RouteInfo::exact('CUSTOM', '/', fn() => 'wacky'));
2024-03-30 16:24:34 +00:00
$this->assertEquals('get', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
$this->assertEquals('wacky', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'CUSTOM', HttpUri::createUri('/'), [], []))->getBody());
2024-03-30 16:24:34 +00:00
$router1->filter(FilterInfo::prefix('/', function() { /* this one intentionally does nothing */ }));
2024-03-30 16:24:34 +00:00
// registration order should matter
$router1->filter(FilterInfo::prefix('/deep', fn() => 'deep'));
2024-03-30 16:24:34 +00:00
$router1->filter(FilterInfo::pattern('#^/user/([A-Za-z0-9]+)/below#u', fn(string $user) => 'warioware below ' . $user));
2024-03-30 16:24:34 +00:00
$router1->route(RouteInfo::exact('GET', '/user/static', fn() => 'the static one'));
$router1->route(RouteInfo::exact('GET', '/user/static/below', fn() => 'below the static one'));
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)$#uD', fn(string $user) => $user));
$router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)/below$#uD', fn(string $user) => 'below ' . $user));
2024-03-30 16:24:34 +00:00
$this->assertEquals('warioware below static', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/user/static/below'), [], []))->getBody());
2024-03-30 16:24:34 +00:00
$router2 = new Router;
$router2->filter(FilterInfo::prefix('/', fn() => 'meow'));
$router2->route(RouteInfo::exact('GET', '/rules', fn() => 'rules page'));
$router2->route(RouteInfo::exact('GET', '/contact', fn() => 'contact page'));
$router2->route(RouteInfo::exact('GET', '/25252', fn() => 'numeric test'));
$this->assertEquals('meow', (string)$router2->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/rules'), [], []))->getBody());
2022-09-13 15:13:11 +02:00
}
2023-09-07 22:08:31 +00:00
public function testAttribute(): void {
$router = new Router;
2024-10-02 02:09:21 +00:00
$handler = new class implements RouteHandler {
use RouteHandlerCommon;
2024-10-02 02:09:21 +00:00
#[ExactRoute('GET', '/')]
public function getIndex(): string {
2023-09-07 22:08:31 +00:00
return 'index';
}
#[ExactRoute('POST', '/avatar')]
public function postAvatar(): string {
2023-09-07 22:08:31 +00:00
return 'avatar';
}
#[ExactRoute('PUT', '/static')]
public static function putStatic(): string {
2023-09-07 22:08:31 +00:00
return 'static';
}
#[ExactRoute('GET', '/meow')]
#[ExactRoute('POST', '/meow')]
public function multiple(): string {
2023-09-07 22:08:31 +00:00
return 'meow';
}
2023-09-07 22:37:04 +00:00
#[PrefixFilter('/filter')]
public function useFilter(): string {
return 'this intercepts';
}
#[ExactRoute('GET', '/filter')]
public function getFilter(): string {
return 'this is intercepted';
}
#[PatternRoute('GET', '/profile/([A-Za-z0-9]+)')]
public function getPattern(string $beans): string {
return sprintf('profile of %s', $beans);
}
#[PatternRoute('GET', '#^/profile-but-raw/([A-Za-z0-9]+)$#uD', raw: true)]
public function getPatternRaw(string $beans): string {
return sprintf('still the profile of %s', $beans);
}
public function hasNoAttr(): string {
2023-09-07 22:37:04 +00:00
return 'not a route';
}
2023-09-07 22:08:31 +00:00
};
$router->register($handler);
$this->assertEquals('index', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
$this->assertEquals('avatar', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/avatar'), [], []))->getBody());
$this->assertEquals('static', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'PUT', HttpUri::createUri('/static'), [], []))->getBody());
$this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/meow'), [], []))->getBody());
$this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/meow'), [], []))->getBody());
$this->assertEquals('profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile/Cool134'), [], []))->getBody());
$this->assertEquals('still the profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile-but-raw/Cool134'), [], []))->getBody());
2023-09-07 22:08:31 +00:00
}
public function testEEPROMSituation(): void {
$router = new Router;
$router->route(RouteInfo::pattern('GET', '#^/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?$#uD', fn(string $id) => "Get {$id}"));
$router->route(RouteInfo::pattern('DELETE', '#^/uploads/([A-Za-z0-9\-_]+)$#uD', fn(string $id) => "Delete {$id}"));
// make sure both GET and DELETE are able to execute with a different pattern
$this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), [], []))->getBody());
$this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'DELETE', HttpUri::createUri('/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'), [], []))->getBody());
}
2024-08-18 16:41:02 +00:00
public function testFilterInterceptionOnRoot(): void {
$router = new Router;
$router->filter(FilterInfo::prefix('/', fn() => 'expected'));
$router->route(RouteInfo::exact('GET', '/', fn() => 'unexpected'));
$router->route(RouteInfo::exact('GET', '/test', fn() => 'also unexpected'));
2024-08-18 16:41:02 +00:00
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/test'), [], []))->getBody());
$this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/error'), [], []))->getBody());
}
2024-08-18 16:41:02 +00:00
public function testDefaultOptionsImplementation(): void {
$router = new Router;
$router->route(RouteInfo::exact('GET', '/test', fn() => 'get'));
$router->route(RouteInfo::exact('POST', '/test', fn() => 'post'));
$router->route(RouteInfo::exact('PATCH', '/test', fn() => 'patch'));
2024-08-18 16:41:02 +00:00
$response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'OPTIONS', HttpUri::createUri('/test'), [], []));
$this->assertEquals(204, $response->getStatusCode());
$this->assertEquals('No Content', $response->getReasonPhrase());
$this->assertEquals('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow'));
2024-08-18 16:41:02 +00:00
}
2025-03-07 23:39:15 +00:00
public function testProcessors(): void {
$router = new Router;
$router->register(new class implements RouteHandler {
use RouteHandlerCommon;
#[Preprocessor('base64-decode-body')]
public function decodePre(HandlerContext $context, HttpRequest $request): void {
$context->addArgument(base64_decode((string)$request->getBody()));
}
#[Postprocessor('crash')]
public function crashPost(): void {
throw new RuntimeException('this shouldnt run!');
}
#[Postprocessor('json-encode')]
public function encodePost(HandlerContext $context, int $flags): void {
$context->halt();
$context->response->setTypeJson();
2025-03-12 20:56:48 +00:00
$context->response->body = Stream::createStream(
2025-03-07 23:39:15 +00:00
json_encode($context->result, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR | $flags)
);
}
/** @return array{data: string} */
#[ExactRoute('POST', '/test')]
#[Before('base64-decode-body')]
#[After('json-encode', flags: JSON_PRETTY_PRINT)]
#[After('crash')]
public function route(string $decoded): array {
return ['data' => $decoded];
}
});
$router->preprocessor(ProcessorInfo::pre('intercept', function(HttpResponseBuilder $response) {
$response->statusCode = 403;
return 'it can work like a filter too';
}));
$router->route(RouteInfo::exact(
'PUT', '/alternate',
#[Before('base64-decode-body')]
#[After('json-encode', flags: JSON_PRETTY_PRINT)]
function(HttpRequest $request, string $decoded): array {
return ['path' => $request->uri->path, 'data' => $decoded];
}
));
$router->route(RouteInfo::exact(
'GET', '/filtered',
#[Before('intercept')]
#[Before('base64-decode-body')]
function(): string {
return 'should not get here';
}
));
2025-03-12 20:56:48 +00:00
$response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('mewow')), [], 'POST', HttpUri::createUri('/test'), [], []));
2025-03-07 23:39:15 +00:00
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("{\n \"data\": \"mewow\"\n}", (string)$response->getBody());
2025-03-12 20:56:48 +00:00
$response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('soap')), [], 'PUT', HttpUri::createUri('/alternate'), [], []));
2025-03-07 23:39:15 +00:00
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals("{\n \"path\": \"/alternate\",\n \"data\": \"soap\"\n}", (string)$response->getBody());
$response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/filtered'), [], []));
$this->assertEquals(403, $response->getStatusCode());
$this->assertEquals('it can work like a filter too', (string)$response->getBody());
}
2022-09-13 15:13:11 +02:00
}