initialized; } }; $this->assertTrue($engine->getInitializedVar()); // we need to setup a dummy route $engine->route('/someRoute', function () { }); $engine->request()->url = '/someRoute'; $engine->start(); $this->assertFalse($engine->router()->caseSensitive); $this->assertTrue($engine->response()->content_length); } public function testInitBeforeStartV2OutputBuffering() { $engine = new class extends Engine { public function getInitializedVar() { return $this->initialized; } }; $engine->set('flight.v2.output_buffering', true); $this->assertTrue($engine->getInitializedVar()); $engine->start(); // This is a necessary evil because of how the v2 output buffer works. ob_end_clean(); $this->assertFalse($engine->router()->caseSensitive); $this->assertTrue($engine->response()->content_length); } public function testHandleErrorNoErrorNumber() { $engine = new Engine(); $result = $engine->handleError(0, '', '', 0); $this->assertFalse($result); } public function testHandleErrorWithException() { $engine = new Engine(); $this->expectException(Exception::class); $this->expectExceptionCode(5); $this->expectExceptionMessage('thrown error message'); $engine->handleError(5, 'thrown error message', '', 0); } public function testHandleException() { $engine = new Engine(); $this->expectOutputRegex('~\500 Internal Server Error\[\s\S]*\thrown exception message \(20\)\~'); $engine->handleException(new Exception('thrown exception message', 20)); } public function testMapExistingMethod() { $engine = new Engine(); $this->expectException(Exception::class); $this->expectExceptionMessage('Cannot override an existing framework method.'); $engine->map('_start', function () { }); } public function testRegisterExistingMethod() { $engine = new Engine(); $this->expectException(Exception::class); $this->expectExceptionMessage('Cannot override an existing framework method.'); $engine->register('_error', 'stdClass'); } public function testSetArrayOfValues() { $engine = new Engine(); $engine->set([ 'key1' => 'value1', 'key2' => 'value2']); $this->assertEquals('value1', $engine->get('key1')); $this->assertEquals('value2', $engine->get('key2')); } public function testStartWithRoute() { $_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_URI'] = '/someRoute'; $engine = new class extends Engine { public function getInitializedVar() { return $this->initialized; } }; $engine->route('/someRoute', function () { echo 'i ran'; }, true); $this->expectOutputString('i ran'); $engine->start(); } // n0nag0n - I don't know why this does what it does, but it's existing framework functionality 1/1/24 public function testStartWithRouteButReturnedValueThrows404() { $_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_URI'] = '/someRoute'; $engine = new class extends Engine { public function getInitializedVar() { return $this->initialized; } }; $engine->route('/someRoute', function () { echo 'i ran'; return true; }, true); $this->expectOutputString('

404 Not Found

The page you have requested could not be found.

'); $engine->start(); } public function testStartWithRouteButReturnedValueThrows404V2OutputBuffering() { $_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['REQUEST_URI'] = '/someRoute'; $engine = new class extends Engine { public function getInitializedVar() { return $this->initialized; } }; $engine->set('flight.v2.output_buffering', true); $engine->route('/someRoute', function () { echo 'i ran'; return true; }, true); $this->expectOutputString('

404 Not Found

The page you have requested could not be found.

'); $engine->start(); } public function testStopWithCode() { $engine = new class extends Engine { public function getLoader() { return $this->loader; } }; // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self { return $this; } }; }); $engine->response()->write('I am a teapot'); $this->expectOutputString('I am a teapot'); $engine->stop(500); $this->assertEquals(500, $engine->response()->status()); } public function testStopWithCodeV2OutputBuffering() { $engine = new class extends Engine { public function getLoader() { return $this->loader; } }; // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self { return $this; } }; }); $engine->set('flight.v2.output_buffering', true); $engine->route('/testRoute', function () use ($engine) { echo 'I am a teapot'; $engine->stop(500); }); $engine->request()->url = '/testRoute'; $engine->start(); $this->expectOutputString('I am a teapot'); $this->assertEquals(500, $engine->response()->status()); } public function testPostRoute() { $engine = new Engine(); $engine->post('/someRoute', function () { echo 'i ran'; }, true); $routes = $engine->router()->getRoutes(); $this->assertEquals('POST', $routes[0]->methods[0]); $this->assertEquals('/someRoute', $routes[0]->pattern); } public function testPutRoute() { $engine = new Engine(); $engine->put('/someRoute', function () { echo 'i ran'; }, true); $routes = $engine->router()->getRoutes(); $this->assertEquals('PUT', $routes[0]->methods[0]); $this->assertEquals('/someRoute', $routes[0]->pattern); } public function testPatchRoute() { $engine = new Engine(); $engine->patch('/someRoute', function () { echo 'i ran'; }, true); $routes = $engine->router()->getRoutes(); $this->assertEquals('PATCH', $routes[0]->methods[0]); $this->assertEquals('/someRoute', $routes[0]->pattern); } public function testDeleteRoute() { $engine = new Engine(); $engine->delete('/someRoute', function () { echo 'i ran'; }, true); $routes = $engine->router()->getRoutes(); $this->assertEquals('DELETE', $routes[0]->methods[0]); $this->assertEquals('/someRoute', $routes[0]->pattern); } public function testHeadRoute() { $engine = new Engine(); $engine->route('GET /someRoute', function () { echo 'i ran'; }, true); $engine->request()->method = 'HEAD'; $engine->request()->url = '/someRoute'; $engine->start(); // No body should be sent $this->expectOutputString(''); } public function testHalt() { $engine = new class extends Engine { public function getLoader() { return $this->loader; } }; // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { public function setRealHeader( string $header_string, bool $replace = true, int $response_code = 0 ): self { return $this; } }; }); $engine->halt(500, '', false); $this->assertEquals(500, $engine->response()->status()); } public function testRedirect() { $engine = new Engine(); $engine->redirect('https://github.com', 302); $this->assertEquals('https://github.com', $engine->response()->headers()['Location']); $this->assertEquals(302, $engine->response()->status()); } public function testRedirectWithBaseUrl() { $engine = new Engine(); $engine->set('flight.base_url', '/subdirectory'); $engine->redirect('/someRoute', 301); $this->assertEquals('/subdirectory/someRoute', $engine->response()->headers()['Location']); $this->assertEquals(301, $engine->response()->status()); } public function testJsonRequestBody() { $engine = new Engine(); $tmpfile = tmpfile(); $stream_path = stream_get_meta_data($tmpfile)['uri']; file_put_contents($stream_path, '{"key1":"value1","key2":"value2"}'); $engine->register('request', Request::class, [ [ 'method' => 'POST', 'url' => '/something/fancy', 'base' => '/vagrant/public', 'type' => 'application/json', 'length' => 13, 'data' => new Collection(), 'query' => new Collection(), 'stream_path' => $stream_path ] ]); $engine->post('/something/fancy', function () use ($engine) { echo $engine->request()->data->key1; echo $engine->request()->data->key2; }); $engine->start(); $this->expectOutputString('value1value2'); } public function testJson() { $engine = new Engine(); $engine->json(['key1' => 'value1', 'key2' => 'value2']); $this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); $this->assertEquals('{"key1":"value1","key2":"value2"}', $engine->response()->getBody()); } public function testJsonV2OutputBuffering() { $engine = new Engine(); $engine->response()->v2_output_buffering = true; $engine->json(['key1' => 'value1', 'key2' => 'value2']); $this->expectOutputString('{"key1":"value1","key2":"value2"}'); $this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); } public function testJsonHalt() { $engine = new Engine(); $this->expectOutputString('{"key1":"value1","key2":"value2"}'); $engine->jsonHalt(['key1' => 'value1', 'key2' => 'value2']); $this->assertEquals('application/json; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); $this->assertEquals('{"key1":"value1","key2":"value2"}', $engine->response()->getBody()); } public function testJsonP() { $engine = new Engine(); $engine->request()->query['jsonp'] = 'whatever'; $engine->jsonp(['key1' => 'value1', 'key2' => 'value2']); $this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); $this->assertEquals('whatever({"key1":"value1","key2":"value2"});', $engine->response()->getBody()); } public function testJsonPV2OutputBuffering() { $engine = new Engine(); $engine->response()->v2_output_buffering = true; $engine->request()->query['jsonp'] = 'whatever'; $engine->jsonp(['key1' => 'value1', 'key2' => 'value2']); $this->expectOutputString('whatever({"key1":"value1","key2":"value2"});'); $this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); } public function testJsonpBadParam() { $engine = new Engine(); $engine->jsonp(['key1' => 'value1', 'key2' => 'value2']); $this->assertEquals('({"key1":"value1","key2":"value2"});', $engine->response()->getBody()); $this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); } public function testJsonpBadParamV2OutputBuffering() { $engine = new Engine(); $engine->response()->v2_output_buffering = true; $engine->jsonp(['key1' => 'value1', 'key2' => 'value2']); $this->expectOutputString('({"key1":"value1","key2":"value2"});'); $this->assertEquals('application/javascript; charset=utf-8', $engine->response()->headers()['Content-Type']); $this->assertEquals(200, $engine->response()->status()); } public function testEtagSimple() { $engine = new Engine(); $engine->etag('etag'); $this->assertEquals('"etag"', $engine->response()->headers()['ETag']); } public function testEtagWithHttpIfNoneMatch() { $engine = new Engine; $_SERVER['HTTP_IF_NONE_MATCH'] = 'etag'; $engine->etag('etag'); $this->assertTrue(empty($engine->response()->headers()['ETag'])); $this->assertEquals(304, $engine->response()->status()); } public function testLastModifiedSimple() { $engine = new Engine(); $engine->lastModified(1234567890); $this->assertEquals('Fri, 13 Feb 2009 23:31:30 GMT', $engine->response()->headers()['Last-Modified']); } public function testLastModifiedWithHttpIfModifiedSince() { $engine = new Engine; $_SERVER['HTTP_IF_MODIFIED_SINCE'] = 'Fri, 13 Feb 2009 23:31:30 GMT'; $engine->lastModified(1234567890); $this->assertTrue(empty($engine->response()->headers()['Last-Modified'])); $this->assertEquals(304, $engine->response()->status()); } public function testGetUrl() { $engine = new Engine(); $engine->route('/path1/@param:[0-9]{3}', function () { echo 'I win'; }, false, 'path1'); $url = $engine->getUrl('path1', [ 'param' => 123 ]); $this->assertEquals('/path1/123', $url); } public function testGetUrlComplex() { $engine = new Engine(); $engine->route('/item/@item_param:[a-z0-9]{16}/by-status/@token:[a-z0-9]{16}', function () { echo 'I win'; }, false, 'path_item_1'); $url = $engine->getUrl('path_item_1', [ 'item_param' => 1234567890123456, 'token' => 6543210987654321 ]); $this->assertEquals('/item/1234567890123456/by-status/6543210987654321', $url); } public function testGetUrlInsideRoute() { $engine = new Engine(); $engine->route('/path1/@param:[0-9]{3}', function () { echo 'I win'; }, false, 'path1'); $found_url = ''; $engine->route('/path1/@param:[0-9]{3}/path2', function () use ($engine, &$found_url) { // this should pull the param from the first route // since the param names are the same. $found_url = $engine->getUrl('path1'); }); $engine->request()->url = '/path1/123/path2'; $engine->start(); $this->assertEquals('/path1/123', $found_url); } public function testMiddlewareCallableFunction() { $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware(function ($params) { echo 'before' . $params['id']; }); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('before123OK123'); } public function testMiddlewareCallableFunctionReturnFalse() { $engine = new class extends Engine { }; $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware(function ($params) { echo 'before' . $params['id']; return false; }); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('Forbidden'); $this->assertEquals(403, $engine->response()->status()); } public function testMiddlewareClassBefore() { $middleware = new class { public function before($params) { echo 'before' . $params['id']; } }; $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware($middleware); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('before123OK123'); } public function testMiddlewareClassBeforeAndAfter() { $middleware = new class { public function before($params) { echo 'before' . $params['id']; } public function after($params) { echo 'after' . $params['id']; } }; $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware($middleware); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('before123OK123after123'); } public function testMiddlewareClassAfter() { $middleware = new class { public function after($params) { echo 'after' . $params['id']; } }; $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware($middleware); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('OK123after123'); } public function testMiddlewareClassStringNoContainer() { $middleware = new class { public function after($params) { echo 'after' . $params['id']; } }; $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware(get_class($middleware)); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('OK123after123'); } public function testMiddlewareClassStringWithContainer() { $engine = new Engine(); $dice = new \Dice\Dice(); $dice = $dice->addRule('*', [ 'substitutions' => [ Engine::class => $engine ] ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware(ContainerDefault::class); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('I returned before the route was called with the following parameters: {"id":"123"}OK123'); } public function testMiddlewareClassAfterFailedCheck() { $middleware = new class { public function after($params) { echo 'after' . $params['id']; return false; } }; $engine = new class extends Engine { }; $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware($middleware); $engine->request()->url = '/path1/123'; $engine->start(); $this->assertEquals(403, $engine->response()->status()); $this->expectOutputString('Forbidden'); } public function testMiddlewareCallableFunctionMultiple() { $engine = new Engine(); $engine->route('/path1/@id', function ($id) { echo 'OK' . $id; }) ->addMiddleware(function ($params) { echo 'before1' . $params['id']; }) ->addMiddleware(function ($params) { echo 'before2' . $params['id']; }); $engine->request()->url = '/path1/123'; $engine->start(); $this->expectOutputString('before1123before2123OK123'); } // Pay attention to the order on how the middleware is executed in this test. public function testMiddlewareClassCallableRouteMultiple() { $middleware = new class { public function before($params) { echo 'before' . $params['another_id']; } public function after($params) { echo 'after' . $params['id']; } }; $middleware2 = new class { public function before($params) { echo 'before' . $params['id']; } public function after($params) { echo 'after' . $params['id'] . $params['another_id']; } }; $engine = new Engine(); $engine->route('/path1/@id/subpath1/@another_id', function () { echo 'OK'; })->addMiddleware([ $middleware, $middleware2 ]); $engine->request()->url = '/path1/123/subpath1/456'; $engine->start(); $this->expectOutputString('before456before123OKafter123456after123'); } public function testMiddlewareClassGroupRouteMultipleBooyah() { $middleware = new class { public function before($params) { echo 'before' . $params['another_id']; } public function after($params) { echo 'after' . $params['id']; } }; $middleware2 = new class { public function before($params) { echo 'before' . $params['id']; } public function after($params) { echo 'after' . $params['id'] . $params['another_id']; } }; $engine = new Engine(); $engine->group('/path1/@id', function ($router) { $router->map('/subpath1/@another_id', function () { echo 'OK'; }); $router->map('/@cool_id', function () { echo 'OK'; }); }, [ $middleware, $middleware2 ]); $engine->request()->url = '/path1/123/subpath1/456'; $engine->start(); $this->expectOutputString('before456before123OKafter123456after123'); } public function testContainerBadClass() { $engine = new Engine(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("\$containerHandler must be of type callable or instance \\Psr\\Container\\ContainerInterface"); $engine->registerContainerHandler('BadClass'); } public function testContainerDice() { $engine = new Engine(); $dice = new \Dice\Dice(); $dice = $dice->addRules([ PdoWrapper::class => [ 'shared' => true, 'constructParams' => [ 'sqlite::memory:' ] ] ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', Container::class.'->testTheContainer'); $engine->request()->url = '/container'; $engine->start(); $this->expectOutputString('yay! I injected a collection, and it has 1 items'); } public function testContainerDicePdoWrapperTest() { $engine = new Engine(); $dice = new \Dice\Dice(); $dice = $dice->addRules([ PdoWrapper::class => [ 'shared' => true, 'constructParams' => [ 'sqlite::memory:' ] ] ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', Container::class.'->testThePdoWrapper'); $engine->request()->url = '/container'; $engine->start(); $this->expectOutputString('Yay! I injected a PdoWrapper, and it returned the number 5 from the database!'); } public function testContainerDiceFlightEngine() { $engine = new Engine(); $engine->set('test_me_out', 'You got it boss!'); $dice = new \Dice\Dice(); $dice = $dice->addRule('*', [ 'substitutions' => [ Engine::class => $engine ] ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', ContainerDefault::class.'->echoTheContainer'); $engine->request()->url = '/container'; $engine->start(); $this->expectOutputString('You got it boss!'); } public function testContainerDicePdoWrapperTestBadParams() { $engine = new Engine(); $dice = new \Dice\Dice(); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', Container::class.'->testThePdoWrapper'); $engine->request()->url = '/container'; // php 7.4 will throw a PDO exception, but php 8 will throw an ErrorException if(version_compare(PHP_VERSION, '8.1.0') >= 0) { $this->expectException(ErrorException::class); $this->expectExceptionMessageMatches("/Passing null to parameter/"); } elseif(version_compare(PHP_VERSION, '8.0.0') >= 0) { $this->expectException(PDOException::class); $this->expectExceptionMessageMatches("/must be a valid data source name/"); } else { $this->expectException(PDOException::class); $this->expectExceptionMessageMatches("/invalid data source name/"); } $engine->start(); } public function testContainerDiceBadClass() { $engine = new Engine(); $dice = new \Dice\Dice(); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', 'BadClass->testTheContainer'); $engine->request()->url = '/container'; $this->expectException(Exception::class); $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); $engine->start(); } public function testContainerDiceBadMethod() { $engine = new Engine(); $dice = new \Dice\Dice(); $dice = $dice->addRules([ PdoWrapper::class => [ 'shared' => true, 'constructParams' => [ 'sqlite::memory:' ] ] ]); $engine->registerContainerHandler(function ($class, $params) use ($dice) { return $dice->create($class, $params); }); $engine->route('/container', Container::class.'->badMethod'); $engine->request()->url = '/container'; $this->expectException(Exception::class); $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); $engine->start(); } public function testContainerPsr11() { $engine = new Engine(); $container = new \League\Container\Container(); $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); $container->add(Collection::class); $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); $engine->registerContainerHandler($container); $engine->route('/container', Container::class.'->testTheContainer'); $engine->request()->url = '/container'; $engine->start(); $this->expectOutputString('yay! I injected a collection, and it has 1 items'); } public function testContainerPsr11ClassNotFound() { $engine = new Engine(); $container = new \League\Container\Container(); $container->add(Container::class)->addArgument(Collection::class); $container->add(Collection::class); $engine->registerContainerHandler($container); $engine->route('/container', 'BadClass->testTheContainer'); $engine->request()->url = '/container'; $this->expectException(Exception::class); $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); $engine->start(); } public function testContainerPsr11MethodNotFound() { $engine = new Engine(); $container = new \League\Container\Container(); $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); $container->add(Collection::class); $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); $engine->registerContainerHandler($container); $engine->route('/container', Container::class.'->badMethod'); $engine->request()->url = '/container'; $this->expectException(Exception::class); $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); $engine->start(); } public function testRouteFoundButBadMethod() { $engine = new class extends Engine { public function getLoader() { return $this->loader; } }; // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { public function setRealHeader( string $header_string, bool $replace = true, int $response_code = 0 ): self { return $this; } }; }); $engine->route('POST /path1/@id', function ($id) { echo 'OK' . $id; }); $engine->route('GET /path2/@id', function ($id) { echo 'OK' . $id; }); $engine->route('PATCH /path3/@id', function ($id) { echo 'OK' . $id; }); $engine->request()->url = '/path1/123'; $engine->request()->method = 'GET'; $engine->start(); $this->expectOutputString('Method Not Allowed'); $this->assertEquals(405, $engine->response()->status()); $this->assertEquals('Method Not Allowed', $engine->response()->getBody()); } public function testDownload() { $engine = new class extends Engine { public function getLoader() { return $this->loader; } }; // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { public function setRealHeader( string $header_string, bool $replace = true, int $response_code = 0 ): self { return $this; } }; }); $tmpfile = tmpfile(); fwrite($tmpfile, 'I am a teapot'); $streamPath = stream_get_meta_data($tmpfile)['uri']; $this->expectOutputString('I am a teapot'); $engine->download($streamPath); } public function testDownloadBadPath() { $engine = new Engine(); $this->expectException(Exception::class); $this->expectExceptionMessage("/path/to/nowhere cannot be found."); $engine->download('/path/to/nowhere'); } }