<?php // RouterTest.php // Created: 2022-01-20 // Updated: 2025-03-23 declare(strict_types=1); use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\CoversClass; use Index\Http\{HttpHeaders,HttpResponseBuilder,HttpRequest,HttpUri}; use Index\Http\Content\{FormContent,MultipartFormContent,UrlEncodedFormContent}; use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon,Router}; use Index\Http\Routing\AccessControl\{AccessControl}; 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}; use Index\Http\Streams\Stream; /** * This test isn't super representative of the current functionality * it mostly just does the same tests that were done against the previous implementation */ #[CoversClass(Router::class)] #[CoversClass(RouteInfo::class)] #[CoversClass(PrefixFilter::class)] #[CoversClass(ExactRoute::class)] #[CoversClass(RouteHandler::class)] #[CoversClass(RouteHandlerCommon::class)] #[CoversClass(PatternRoute::class)] #[CoversClass(FilterInfo::class)] #[CoversClass(AccessControl::class)] final class RouterTest extends TestCase { public function testRouter(): void { $router1 = new Router; $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')); $this->assertSame('get', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody()); $this->assertSame('wacky', (string)$router1->handle(HttpRequest::createRequestWithoutBody('CUSTOM', '/'))->getBody()); $router1->filter(FilterInfo::prefix('/', function() { /* this one intentionally does nothing */ })); // registration order should matter $router1->filter(FilterInfo::prefix('/deep', fn() => 'deep')); $router1->filter(FilterInfo::pattern('#^/user/([A-Za-z0-9]+)/below#u', fn(string $user) => 'warioware below ' . $user)); $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)); $this->assertSame('warioware below static', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/user/static/below'))->getBody()); $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->assertSame('meow', (string)$router2->handle(HttpRequest::createRequestWithoutBody('GET', '/rules'))->getBody()); } public function testAttribute(): void { $router = new Router; $handler = new class implements RouteHandler { use RouteHandlerCommon; #[ExactRoute('GET', '/')] public function getIndex(): string { return 'index'; } #[ExactRoute('POST', '/avatar')] public function postAvatar(): string { return 'avatar'; } #[ExactRoute('PUT', '/static')] public static function putStatic(): string { return 'static'; } #[ExactRoute('GET', '/meow')] #[ExactRoute('POST', '/meow')] public function multiple(): string { return 'meow'; } #[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); } #[ExactRoute('GET', '/announce')] #[ExactRoute('GET', '/announce.php')] #[PatternRoute('GET', '/announce/([A-Za-z0-9]+)')] #[PatternRoute('GET', '/announce.php/([A-Za-z0-9]+)')] public function getMultipleTestWithDefault(string $key = 'empty'): string { return $key; } #[PatternRoute('GET', '/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')] public function getEepromEdgeCase( string $uploadId, string $variant = '', string $extension = '' ): string { return sprintf('%s//%s//%s', $uploadId, $variant, $extension); } #[ExactRoute('GET', '/messages/stats')] public function getStats(): string { return 'hit exact'; } #[PatternRoute('GET', '/messages/([A-Za-z0-9]+)')] public function getView(): string { return 'hit pattern'; } #[ExactRoute('GET', '/assets/avatar')] #[PatternRoute('GET', '/assets/avatar/([0-9]+)(?:\.[a-z]+)?')] public function getTrailingSlash(): string { return 'hit'; } public function hasNoAttr(): string { return 'not a route'; } }; $router->register($handler); $this->assertSame('index', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody()); $this->assertSame('avatar', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/avatar'))->getBody()); $this->assertSame('static', (string)$router->handle(HttpRequest::createRequestWithoutBody('PUT', '/static'))->getBody()); $this->assertSame('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/meow'))->getBody()); $this->assertSame('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody()); $this->assertSame('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody()); $this->assertSame('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody()); $this->assertSame('empty', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce'))->getBody()); $this->assertSame('empty', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce.php'))->getBody()); $this->assertSame('Z643QANLgGNkF4D4h4qvFpyeXjx4TcDE', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce/Z643QANLgGNkF4D4h4qvFpyeXjx4TcDE'))->getBody()); $this->assertSame('1aKq8VaGyHohNUUR7RzU1W57Z3hQ6m0YMazAkr2IoiSPsvQJ6QoQutywwiOBlNka', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/announce.php/1aKq8VaGyHohNUUR7RzU1W57Z3hQ6m0YMazAkr2IoiSPsvQJ6QoQutywwiOBlNka'))->getBody()); $this->assertSame('1RJNSRYmxrvXUr////', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr'))->getBody()); $this->assertSame('1RJNSRYmxrvXUr//thumb//', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr-thumb'))->getBody()); $this->assertSame('1RJNSRYmxrvXUr//thumb//jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr-thumb.jpg'))->getBody()); $this->assertSame('1RJNSRYmxrvXUr////jpg', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/1RJNSRYmxrvXUr.jpg'))->getBody()); $this->assertSame('hit exact', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/stats'))->getBody()); $this->assertSame('hit pattern', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/messages/soaps'))->getBody()); $this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/'))->getBody()); $this->assertSame('hit', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/assets/avatar/123/'))->getBody()); } 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->assertSame('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))->getBody()); $this->assertSame('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(HttpRequest::createRequestWithoutBody('DELETE', '/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'))->getBody()); } 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')); $this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody()); $this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/test'))->getBody()); $this->assertSame('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/error'))->getBody()); } 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')); $response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/test')); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('OK', $response->getReasonPhrase()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow')); } public function testDefaultProcessorsNoRegister(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage(sprintf('preprocessor "%s" was not found', 'input:urlencoded:required')); $router = new Router(registerDefaultProcessors: false); $router->route(RouteInfo::exact( 'GET', '/soap', #[Before('input:urlencoded:required')] function() { return 'test'; }, )); $router->handle(HttpRequest::createRequestWithoutBody('GET', '/soap')); } public function testDefaultProcessors(): void { $router = new Router; $router->register(new class implements RouteHandler { use RouteHandlerCommon; #[Before('input:urlencoded', required: false)] #[Before('input:multipart', required: false)] #[ExactRoute('POST', '/optional-form')] public function formOptional(?FormContent $content = null): string { if($content instanceof MultipartFormContent) return 'multipart:' . (string)$content->getParam('test'); if($content instanceof UrlEncodedFormContent) return 'urlencoded:' . (string)$content->getParam('test'); return 'none'; } #[Before('input:multipart')] #[ExactRoute('POST', '/required-multipart')] public function multipartRequired(MultipartFormContent $content): string { return (string)$content->getParam('test'); } #[Before('input:urlencoded')] #[ExactRoute('POST', '/required-urlencoded')] public function urlencodedRequired(UrlEncodedFormContent $content): string { return (string)$content->getParam('test'); } #[After('output:stream')] #[ExactRoute('GET', '/output/stream')] public function testOutputStream(): string { return '<!doctype html><h1>ensuring autodetect gets skipped</h1>'; } #[After('output:plain', charset: 'utf-8')] #[ExactRoute('GET', '/output/plain')] public function testOutputPlain(): string { return '<!doctype html><h1>ensuring autodetect gets skipped</h1>'; } #[After('output:html', charset: 'us-ascii')] #[ExactRoute('GET', '/output/html')] public function testOutputHtml(): string { return '<?xml idk how xml opens the prefix is enough><beans></beans>'; } #[After('output:xml')] #[ExactRoute('GET', '/output/xml')] public function testOutputXml(): string { return 'soup'; } #[After('output:css')] #[ExactRoute('GET', '/output/css')] public function testOutputCss(): string { return '<!doctype html><h1>ensuring autodetect gets skipped</h1>'; } #[After('output:js')] #[ExactRoute('GET', '/output/js')] public function testOutputJs(): string { return '<!doctype html><h1>ensuring autodetect gets skipped</h1>'; } /** @return array{wow: string} */ #[After('output:json', flags: 0)] #[ExactRoute('GET', '/output/json')] public function testOutputJson(): array { return ['wow' => 'objects?? / epic!!']; } /** @return array{benben: int} */ #[After('output:bencode')] #[ExactRoute('GET', '/output/bencode')] public function testOutputBencode(): array { return ['benben' => 12345]; } #[After('output:bencode')] #[ExactRoute('GET', '/output/bencode/object')] public function testOutputBencodeObject(): object { return new class implements \Index\Bencode\BencodeSerializable { use \Index\Bencode\BencodeSerializableCommon; public function __construct( #[\Index\Bencode\BencodeProperty('failure reason')] public private(set) string $reason = 'Invalid info hash.' ) {} }; } }); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/stream')); $this->assertSame('application/octet-stream', $response->getHeaderLine('Content-Type')); $this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/plain')); $this->assertSame('text/plain;charset=utf-8', $response->getHeaderLine('Content-Type')); $this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/html')); $this->assertSame('text/html;charset=us-ascii', $response->getHeaderLine('Content-Type')); $this->assertSame('<?xml idk how xml opens the prefix is enough><beans></beans>', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/xml')); $this->assertSame('application/xml', $response->getHeaderLine('Content-Type')); $this->assertSame('soup', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/css')); $this->assertSame('text/css', $response->getHeaderLine('Content-Type')); $this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/js')); $this->assertSame('application/javascript', $response->getHeaderLine('Content-Type')); $this->assertSame('<!doctype html><h1>ensuring autodetect gets skipped</h1>', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/json')); $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); $this->assertSame('{"wow":"objects?? \/ epic!!"}', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/bencode')); $this->assertSame('application/x-bittorrent', $response->getHeaderLine('Content-Type')); $this->assertSame('d6:benbeni12345ee', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/output/bencode/object')); $this->assertSame('application/x-bittorrent', $response->getHeaderLine('Content-Type')); $this->assertSame('d14:failure reason18:Invalid info hash.e', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/optional-form')); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('none', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', [], Stream::createStream('test=mewow'))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('none', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=mewow'))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('urlencoded:mewow', (string)$response->getBody()); // an empty string is valid too $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream(''))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('urlencoded:', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [ '----soap12345', 'Content-Disposition: form-data; name="test"', '', 'wowof', '----soap12345--', '', ])))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('multipart:wowof', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [ '----soap12345--', '', ])))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('multipart:', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-urlencoded')); $this->assertSame(400, $response->getStatusCode()); $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-multipart')); $this->assertSame(400, $response->getStatusCode()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=meow'))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('meow', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream(implode("\r\n", [ '----soap56789', 'Content-Disposition: form-data; name="test"', '', 'woof', '----soap56789--', '', ])))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('woof', (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream('test=meow'))); $this->assertSame(400, $response->getStatusCode()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream(implode("\r\n", [ '----soap56789', 'Content-Disposition: form-data; name="test"', '', 'woof', '----soap56789--', '', ])))); $this->assertSame(400, $response->getStatusCode()); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data']], Stream::createStream(implode("\r\n", [ '----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'Content-Disposition: form-data; name="test"', '', 'the', '----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--', '', ])))); $this->assertSame(400, $response->getStatusCode()); } 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(); $context->response->body = Stream::createStream( 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'; } )); $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/test', [], Stream::createStream(base64_encode('mewow')))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame("{\n \"data\": \"mewow\"\n}", (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithBody('PUT', '/alternate', [], Stream::createStream(base64_encode('soap')))); $this->assertSame(200, $response->getStatusCode()); $this->assertSame("{\n \"path\": \"/alternate\",\n \"data\": \"soap\"\n}", (string)$response->getBody()); $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/filtered')); $this->assertSame(403, $response->getStatusCode()); $this->assertSame('it can work like a filter too', (string)$response->getBody()); } public function testAccessControl(): void { $router = new Router; $router->register(new class implements RouteHandler { use RouteHandlerCommon; #[ExactRoute('GET', '/nothing')] public function nothing(): void {} #[AccessControl] #[ExactRoute('GET', '/acld')] public function getAccessControlDifferent(): void {} #[ExactRoute('POST', '/acld')] public function postAccessControlDifferent(): void {} #[AccessControl] #[ExactRoute('GET', '/copdodnop')] public function getCredentialsOnPostDenyOnDeleteNothingOnPut(): void {} #[AccessControl(credentials: true)] #[ExactRoute('POST', '/copdodnop')] public function postCredentialsOnPostDenyOnDeleteNothingOnPut(): void {} #[AccessControl(allow: false)] #[ExactRoute('DELETE', '/copdodnop')] public function deleteCredentialsOnPostDenyOnDeleteNothingOnPut(): void {} #[ExactRoute('PUT', '/copdodnop')] public function putCredentialsOnPostDenyOnDeleteNothingOnPut(): void {} #[AccessControl(allowMethods: ['HEAD'])] #[ExactRoute('GET', '/methods/extra')] public function getMethodsExtra(): void {} #[AccessControl(allowHeaders: true)] #[ExactRoute('GET', '/headers/allow-all')] public function getHeadersAllowAll(): void {} #[AccessControl(allowHeaders: ['Authorization', 'X-Content-Index', 'Content-Type'])] #[ExactRoute('GET', '/headers/allow-specific')] public function getHeadersAllowSpecific(): void {} #[AccessControl] #[ExactRoute('GET', '/headers/expose-safe')] public function getHeadersExposeSafe(): void {} #[AccessControl(exposeHeaders: true)] #[ExactRoute('GET', '/headers/expose-all')] public function getHeadersExposeAll(): void {} #[AccessControl(exposeHeaders: ['X-EEPROM-Max-Size', 'Vary'])] #[ExactRoute('GET', '/headers/expose-specific')] public function getHeadersExposeSpecific(): void {} // including a little curveball! i'm sure this won't lead to confusion #[AccessControl(allow: true, credentials: true, allowMethods: ['POST', 'HEAD'], allowHeaders: ['Content-Length', 'X-Content-Index'], exposeHeaders: ['X-Satori-Time', 'X-Satori-Hash'])] #[ExactRoute('GET', '/all-together-now')] public function getAllTogetherNow(): void {} #[ExactRoute('POST', '/all-together-now')] public function postAllTogetherNow(): void {} }); $routeWithNo = RouteInfo::exact( 'PUT', '/no-override', #[AccessControl(allow: false)] function(): void {} ); $routeWithNo->accessControl = new AccessControl; // allow: true $router->route($routeWithNo); $response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/nothing')); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('GET, HEAD, OPTIONS', $response->getHeaderLine('Allow')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/nothing', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/nothing', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/no-override', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['PUT'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'PUT', '/no-override', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/acld', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/acld', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'POST', '/acld', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['POST'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('https://railgun.sh', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'POST', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('https://railgun.sh', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['DELETE'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'DELETE', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['PUT'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'PUT', '/copdodnop', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/methods/extra', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['HEAD'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, HEAD', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/methods/extra', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'HEAD', '/methods/extra', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-all', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-all', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => ['X-Soap'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/allow-all', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-all', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => [''], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/allow-all', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-specific', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-specific', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => ['X-Content-Index', 'Authorization'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertSame('Authorization, X-Content-Index', $response->getHeaderLine('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/allow-specific', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => ['X-Beans'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/allow-specific', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/expose-safe', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/expose-safe', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/expose-all', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/expose-all', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertTrue($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/headers/expose-specific', [ 'Origin' => ['https://railgun.sh'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/headers/expose-specific', [ 'Origin' => ['https://railgun.sh'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertTrue($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('Vary, X-EEPROM-Max-Size', $response->getHeaderLine('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], 'Access-Control-Request-Method' => ['GET'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => [''], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertTrue($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('', $response->getHeaderLine('Access-Control-Allow-Headers')); $this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], 'Access-Control-Request-Method' => ['GET'], 'Access-Control-Request-Headers' => ['x-content-index'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertSame('x-content-index', $response->getHeaderLine('Access-Control-Allow-Headers')); $this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'GET', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertSame('true', $response->getHeaderLine('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertSame('https://flash.moe:8443', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertSame('X-Satori-Hash, X-Satori-Time', $response->getHeaderLine('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); // although routes can declare other methods than their own as CORS supported, // allow credentials and headers ONLY apply to their own method $response = $router->handle(HttpRequest::createRequestWithoutBody( 'OPTIONS', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], 'Access-Control-Request-Method' => ['POST'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('0', $response->getHeaderLine('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertSame('GET, HEAD, POST', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('300', $response->getHeaderLine('Access-Control-Max-Age')); $response = $router->handle(HttpRequest::createRequestWithoutBody( 'POST', '/all-together-now', [ 'Origin' => ['https://flash.moe:8443'], ], )); $this->assertSame(200, $response->getStatusCode()); $this->assertFalse($response->hasHeader('Content-Length')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Credentials')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Methods')); $this->assertFalse($response->hasHeader('Access-Control-Allow-Origin')); $this->assertFalse($response->hasHeader('Access-Control-Expose-Headers')); $this->assertFalse($response->hasHeader('Access-Control-Max-Age')); } }