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 ;
2024-07-31 18:12:46 +00:00
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
*/
2025-02-28 22:44:56 +00:00
#[CoversClass(Router::class)]
2025-03-07 00:25:00 +00:00
#[CoversClass(RouteInfo::class)]
#[CoversClass(PrefixFilter::class)]
#[CoversClass(ExactRoute::class)]
2024-07-31 18:12:46 +00:00
#[CoversClass(RouteHandler::class)]
2025-01-18 21:57:27 +00:00
#[CoversClass(RouteHandlerCommon::class)]
2025-03-07 00:25:00 +00:00
#[CoversClass(PatternRoute::class)]
#[CoversClass(FilterInfo::class)]
2022-09-13 15:13:11 +02:00
final class RouterTest extends TestCase {
public function testRouter () : void {
2025-02-28 22:44:56 +00:00
$router1 = new Router ;
2024-03-30 16:24:34 +00:00
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +00:00
$router1 -> filter ( FilterInfo :: prefix ( '/' , function () { /* this one intentionally does nothing */ }));
2024-03-30 16:24:34 +00:00
// registration order should matter
2025-03-07 00:25:00 +00:00
$router1 -> filter ( FilterInfo :: prefix ( '/deep' , fn () => 'deep' ));
2024-03-30 16:24:34 +00:00
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +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
2025-02-28 22:44:56 +00:00
$router2 = new Router ;
2025-03-07 00:25:00 +00:00
$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 {
2025-02-28 22:44:56 +00:00
$router = new Router ;
2024-10-02 02:09:21 +00:00
$handler = new class implements RouteHandler {
2025-01-18 21:57:27 +00:00
use RouteHandlerCommon ;
2024-10-02 02:09:21 +00:00
2025-03-07 00:25:00 +00:00
#[ExactRoute('GET', '/')]
2024-12-02 01:30:36 +00:00
public function getIndex () : string {
2023-09-07 22:08:31 +00:00
return 'index' ;
}
2025-03-07 00:25:00 +00:00
#[ExactRoute('POST', '/avatar')]
2024-12-02 01:30:36 +00:00
public function postAvatar () : string {
2023-09-07 22:08:31 +00:00
return 'avatar' ;
}
2025-03-07 00:25:00 +00:00
#[ExactRoute('PUT', '/static')]
2024-12-02 01:30:36 +00:00
public static function putStatic () : string {
2023-09-07 22:08:31 +00:00
return 'static' ;
}
2025-03-07 00:25:00 +00:00
#[ExactRoute('GET', '/meow')]
#[ExactRoute('POST', '/meow')]
2024-12-02 01:30:36 +00:00
public function multiple () : string {
2023-09-07 22:08:31 +00:00
return 'meow' ;
}
2023-09-07 22:37:04 +00:00
2025-03-07 00:25:00 +00:00
#[PrefixFilter('/filter')]
2025-03-02 02:08:45 +00:00
public function useFilter () : string {
2023-09-08 00:09:23 +00:00
return 'this intercepts' ;
}
2025-03-07 00:25:00 +00:00
#[ExactRoute('GET', '/filter')]
2025-03-02 02:08:45 +00:00
public function getFilter () : string {
2023-09-08 00:09:23 +00:00
return 'this is intercepted' ;
}
2025-03-07 00:25:00 +00:00
#[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 );
}
2024-12-02 01:30:36 +00:00
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 );
2025-03-07 00:25:00 +00:00
$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
}
2024-04-02 17:27:06 +00:00
public function testEEPROMSituation () : void {
2025-02-28 22:44:56 +00:00
$router = new Router ;
2025-03-07 00:25:00 +00:00
$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 } " ));
2024-04-02 17:27:06 +00:00
2025-03-07 00:25:00 +00:00
// 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-04-02 17:27:06 +00:00
}
2024-08-18 16:41:02 +00:00
2025-03-02 02:08:45 +00:00
public function testFilterInterceptionOnRoot () : void {
2025-02-28 22:44:56 +00:00
$router = new Router ;
2025-03-07 00:25:00 +00:00
$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
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +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
2025-03-07 00:25:00 +00:00
$response = $router -> handle ( new HttpRequest ( '1.1' , [], NullStream :: instance (), [], 'OPTIONS' , HttpUri :: createUri ( '/test' ), [], []));
$this -> assertEquals ( 204 , $response -> getStatusCode ());
2025-03-07 00:32:06 +00:00
$this -> assertEquals ( 'No Content' , $response -> getReasonPhrase ());
2025-03-07 00:25:00 +00:00
$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
}