From c171769e12bf84e6600b1854d88b65067c89a764 Mon Sep 17 00:00:00 2001 From: Jim Parry Date: Mon, 15 Oct 2018 06:57:56 +0800 Subject: [PATCH 1/7] Add headerEmited (or not) assertions to CUUnitTestCase --- README.md | 9 +- application/Config/Boot/development.php | 46 ++++---- application/Config/Boot/production.php | 28 ++--- application/Config/Boot/testing.php | 46 ++++---- system/Test/CIUnitTestCase.php | 70 +++++++++++- tests/system/Test/TestCaseEmissionsTest.php | 106 ++++++++++++++++++ .../Test/{TestTest.php => TestCaseTest.php} | 9 +- user_guide_src/source/testing/overview.rst | 32 +++++- 8 files changed, 275 insertions(+), 71 deletions(-) create mode 100644 tests/system/Test/TestCaseEmissionsTest.php rename tests/system/Test/{TestTest.php => TestCaseTest.php} (91%) diff --git a/README.md b/README.md index 0b18eafbc1f7..887ad6c9b57a 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,15 @@ Please read the [*Contributing to CodeIgniter*](https://github.com/bcit-ci/CodeI ## Server Requirements PHP version 7.1 or higher is required, with the following extensions installed: -- intl +- [intl](http://php.net/manual/en/intl.requirements.php) +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library +Additionally, make sure that the following extensions are enabled in your PHP: + +- json (enabled by default - don't turn it off) +- [mbstring](http://php.net/manual/en/mbstring.installation.php) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) +- xml (enabled by default - don't turn it off) ## Running CodeIgniter Tests Information on running CodeIgniter test suite can be found in the [README.md](tests/README.md) file in the tests directory. diff --git a/application/Config/Boot/development.php b/application/Config/Boot/development.php index 25f61516c87f..4942b166e901 100644 --- a/application/Config/Boot/development.php +++ b/application/Config/Boot/development.php @@ -1,33 +1,33 @@ app) + if ( ! $this->app) { $this->app = $this->createApplication(); } @@ -104,7 +107,8 @@ public function assertEventTriggered(string $eventName): bool foreach (Events::getPerformanceLogs() as $log) { - if ($log['event'] !== $eventName) continue; + if ($log['event'] !== $eventName) + continue; $found = true; break; @@ -114,6 +118,59 @@ public function assertEventTriggered(string $eventName): bool return $found; } + /** + * Hooks into xdebug's headers capture, looking for a specific header + * emitted + * + * @param string $header The leading portion of the header we are looking for + * @param bool $ignoreCase + * + * @return bool + * @throws \Exception + */ + public function assertHeaderEmitted(string $header, bool $ignoreCase = false): bool + { + $found = false; + + foreach (xdebug_get_headers() as $emitted) + { + $found = $ignoreCase ? + (stripos($emitted, $header) === 0) : + (strpos($emitted, $header) === 0); + if ($found) + break; + } + + return $found ? $found : $this->fail("Didn't find header for {$header}"); + } + + /** + * Hooks into xdebug's headers capture, looking for a specific header + * emitted + * + * @param string $header The leading portion of the header we don't want to find + * @param bool $ignoreCase + * + * @return bool + * @throws \Exception + */ + public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): bool + { + $found = false; + + foreach (xdebug_get_headers() as $emitted) + { + $found = $ignoreCase ? + (stripos($emitted, $header) === 0) : + (strpos($emitted, $header) === 0); + if ($found) + break; + } + + $success = ! $found; + return $success ? $success : $this->fail("Found header for {$header}"); + } + /** * Loads up an instance of CodeIgniter * and gets the environment setup. @@ -122,12 +179,12 @@ public function assertEventTriggered(string $eventName): bool */ protected function createApplication() { - $systemPath = realpath(__DIR__.'/../'); + $systemPath = realpath(__DIR__ . '/../'); - require_once $systemPath.'/'.$this->configPath.'/Paths.php'; + require_once $systemPath . '/' . $this->configPath . '/Paths.php'; $paths = $this->adjustPaths(new \Config\Paths()); - $app = require $systemPath.'/bootstrap.php'; + $app = require $systemPath . '/bootstrap.php'; return $app; } @@ -162,4 +219,5 @@ protected function adjustPaths(Paths $paths) return $paths; } + } diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php new file mode 100644 index 000000000000..4a3b7f036d5e --- /dev/null +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -0,0 +1,106 @@ +pretend(FALSE); + + $body = 'Hello'; + $expected = $body; + + // what do we think we're about to send? + $response->setCookie('foo', 'bar'); + $this->assertTrue($response->hasCookie('foo')); + $this->assertTrue($response->hasCookie('foo', 'bar')); + + // send it + $response->setBody($body); + + ob_start(); + $response->send(); + $output = ob_get_clean(); // what really was sent + // and what actually got sent?; test both ways + $actual = $response->getBody(); // what we thought was sent + + $this->assertEquals($expected, $actual); + $this->assertEquals($expected, $output); + + $this->assertHeaderEmitted("Set-Cookie: foo=bar;"); + $this->assertHeaderEmitted("set-cookie: FOO=bar", true); + } + + /** + * This needs to be run as a separate process, since phpunit + * has already captured the "normal" output, and we will get + * a "Cannot modify headers" message if we try to change + * headers or cookies now. + * + * Furthermore, this test needs to flush the output buffering + * that might be in progress, and start our own output buffer + * capture. + * + * This test includes a basic sanity check, to make sure that + * the body we thought would be sent actually was. + * + * @runInSeparateProcess + */ + public function testHeaderNotEmitted() + { + $response = new Response(new App()); + $response->pretend(FALSE); + + $body = 'Hello'; + $expected = $body; + + // what do we think we're about to send? + $response->setCookie('foo', 'bar'); + $this->assertTrue($response->hasCookie('foo')); + $this->assertTrue($response->hasCookie('foo', 'bar')); + + // send it + $response->setBody($body); + + ob_start(); + $response->send(); + $output = ob_get_clean(); // what really was sent + // and what actually got sent?; test both ways + $actual = $response->getBody(); // what we thought was sent + + $this->assertEquals($expected, $actual); + $this->assertEquals($expected, $output); + + $this->assertHeaderNotEmitted("Set-Cookie: pop=corn", true); + } + +} diff --git a/tests/system/Test/TestTest.php b/tests/system/Test/TestCaseTest.php similarity index 91% rename from tests/system/Test/TestTest.php rename to tests/system/Test/TestCaseTest.php index cdc3773de356..66fb12cbb26e 100644 --- a/tests/system/Test/TestTest.php +++ b/tests/system/Test/TestCaseTest.php @@ -1,10 +1,14 @@ -stream_filter); } - } diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 574d4a7a502a..e081a8867699 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -103,7 +103,7 @@ Ensure that something you expected to be logged actually was:: **assertEventTriggered($eventName)** -Ensure that an event you excpected to be triggered actually was:: +Ensure that an event you expected to be triggered actually was:: Events::on('foo', function($arg) use(&$result) { $result = $arg; @@ -113,6 +113,36 @@ Ensure that an event you excpected to be triggered actually was:: $this->assertEventTriggered('foo'); +**assertHeaderEmitted($header, $ignoreCase=false)** + +Ensure that a header or cookie was actually emitted:: + + $response->setCookie('foo', 'bar'); + + ob_start(); + $this->response->send(); + $output = ob_get_clean(); // in case you want to check the adtual body + + $this->assertHeaderEmitted("Set-Cookie: foo=bar"); + +Note: the test case with this should be `run as a separate process +in PHPunit `_. + +**assertHeaderNotEmitted($header, $ignoreCase=false)** + +Ensure that a header or cookie was actually emitted:: + + $response->setCookie('foo', 'bar'); + + ob_start(); + $this->response->send(); + $output = ob_get_clean(); // in case you want to check the adtual body + + $this->assertHeaderNotEmitted("Set-Cookie: banana"); + +Note: the test case with this should be `run as a separate process +in PHPunit `_. + Accessing Protected/Private Properties -------------------------------------- From 38bfd71115107e854409cc2e5d747d3fdfab95b7 Mon Sep 17 00:00:00 2001 From: Jim Parry Date: Mon, 15 Oct 2018 15:02:55 +0800 Subject: [PATCH 2/7] Debugging testcase emissions --- tests/system/Test/TestCaseEmissionsTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 4a3b7f036d5e..7b5822db2956 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -46,16 +46,12 @@ public function testHeaderEmitted() // send it $response->setBody($body); - - ob_start(); $response->send(); - $output = ob_get_clean(); // what really was sent + // and what actually got sent?; test both ways $actual = $response->getBody(); // what we thought was sent $this->assertEquals($expected, $actual); - $this->assertEquals($expected, $output); - $this->assertHeaderEmitted("Set-Cookie: foo=bar;"); $this->assertHeaderEmitted("set-cookie: FOO=bar", true); } From 23f4a9ca8e1fd63c18214dd5df67c33a088ae729 Mon Sep 17 00:00:00 2001 From: Jim Parry Date: Mon, 15 Oct 2018 17:02:34 +0800 Subject: [PATCH 3/7] Debugging index page spewed by unit tests --- tests/system/Test/TestCaseEmissionsTest.php | 74 ++++++++++++--------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 7b5822db2956..58cf982ba2f2 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -14,8 +14,22 @@ */ class TestCaseEmissionsTest extends \CIUnitTestCase { - //-------------------------------------------------------------------- + public function setUp() + { +// while( count( ob_list_handlers() ) > 0 ) +// { +// ob_end_clean(); +// } + ob_start(); + } + + public function tearDown() + { + ob_end_clean(); + } + + //-------------------------------------------------------------------- /** * This needs to be run as a separate process, since phpunit * has already captured the "normal" output, and we will get @@ -47,9 +61,10 @@ public function testHeaderEmitted() // send it $response->setBody($body); $response->send(); - + // and what actually got sent?; test both ways $actual = $response->getBody(); // what we thought was sent +// $buffer = ob_get_clean(); $this->assertEquals($expected, $actual); $this->assertHeaderEmitted("Set-Cookie: foo=bar;"); @@ -71,32 +86,31 @@ public function testHeaderEmitted() * * @runInSeparateProcess */ - public function testHeaderNotEmitted() - { - $response = new Response(new App()); - $response->pretend(FALSE); - - $body = 'Hello'; - $expected = $body; - - // what do we think we're about to send? - $response->setCookie('foo', 'bar'); - $this->assertTrue($response->hasCookie('foo')); - $this->assertTrue($response->hasCookie('foo', 'bar')); - - // send it - $response->setBody($body); - - ob_start(); - $response->send(); - $output = ob_get_clean(); // what really was sent - // and what actually got sent?; test both ways - $actual = $response->getBody(); // what we thought was sent - - $this->assertEquals($expected, $actual); - $this->assertEquals($expected, $output); - - $this->assertHeaderNotEmitted("Set-Cookie: pop=corn", true); - } - +// public function testHeaderNotEmitted() +// { +// $response = new Response(new App()); +// $response->pretend(FALSE); +// +// $body = 'Hello'; +// $expected = $body; +// +// // what do we think we're about to send? +// $response->setCookie('foo', 'bar'); +// $this->assertTrue($response->hasCookie('foo')); +// $this->assertTrue($response->hasCookie('foo', 'bar')); +// +// // send it +// $response->setBody($body); +// +// ob_start(); +// $response->send(); +// $output = ob_get_clean(); // what really was sent +// // and what actually got sent?; test both ways +// $actual = $response->getBody(); // what we thought was sent +// +// $this->assertEquals($expected, $actual); +// $this->assertEquals($expected, $output); +// +// $this->assertHeaderNotEmitted("Set-Cookie: pop=corn", true); +// } } From caf9ac9993f2894d3df6d5a1c6fabbd2f313e725 Mon Sep 17 00:00:00 2001 From: Jim Parry Date: Wed, 17 Oct 2018 11:35:02 +0800 Subject: [PATCH 4/7] Added flag to ob_start() but travis-ci still blows up --- composer.json | 3 ++- tests/system/Test/TestCaseEmissionsTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1fb81a127063..758262967aa8 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "php": ">=7.1", "zendframework/zend-escaper": "^2.5", "kint-php/kint": "^2.1", - "ext-intl": "*" + "ext-intl": "*", + "ext-curl": "*" }, "require-dev": { "phpunit/phpunit": "^7.0", diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 58cf982ba2f2..858e06118ad4 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -21,7 +21,7 @@ public function setUp() // { // ob_end_clean(); // } - ob_start(); + ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); } public function tearDown() From 8f7cd91bb54876b52db1e419a9298304a0d2caf5 Mon Sep 17 00:00:00 2001 From: Jim Parry Date: Fri, 19 Oct 2018 13:34:50 +0800 Subject: [PATCH 5/7] Isolating output buffering issues --- tests/system/Test/TestCaseEmissionsTest.php | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 858e06118ad4..63f08595b424 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -15,19 +15,19 @@ class TestCaseEmissionsTest extends \CIUnitTestCase { - public function setUp() - { -// while( count( ob_list_handlers() ) > 0 ) +// public function setUp() +// { +// while( count( ob_list_handlers() ) > 1 ) // { // ob_end_clean(); // } - ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); - } - - public function tearDown() - { - ob_end_clean(); - } +// ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); +// } +// +// public function tearDown() +// { +// ob_end_clean(); +// } //-------------------------------------------------------------------- /** @@ -57,18 +57,24 @@ public function testHeaderEmitted() $response->setCookie('foo', 'bar'); $this->assertTrue($response->hasCookie('foo')); $this->assertTrue($response->hasCookie('foo', 'bar')); - - // send it + $response->setBody($body); + +//echo 'ob level at '.ob_get_level(); +ob_end_clean(); +ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); +//$buffer = ob_get_clean(); // flush previous + // send it +// $buffer = ob_get_clean(); $response->send(); - + $buffer = ob_end_clean(); // and what actually got sent?; test both ways $actual = $response->getBody(); // what we thought was sent -// $buffer = ob_get_clean(); $this->assertEquals($expected, $actual); $this->assertHeaderEmitted("Set-Cookie: foo=bar;"); $this->assertHeaderEmitted("set-cookie: FOO=bar", true); + } /** From 973f99b0243cebc0bfc1b0e7404ea8ce26a5d2ff Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Fri, 26 Oct 2018 07:42:55 -0700 Subject: [PATCH 6/7] Header testing works --- application/Config/Boot/development.php | 1 - system/Test/CIUnitTestCase.php | 11 +-- tests/system/Test/TestCaseEmissionsTest.php | 93 ++++++++------------- tests/system/Test/TestCaseTest.php | 30 ++++++- 4 files changed, 67 insertions(+), 68 deletions(-) diff --git a/application/Config/Boot/development.php b/application/Config/Boot/development.php index 4942b166e901..61707a9b582e 100644 --- a/application/Config/Boot/development.php +++ b/application/Config/Boot/development.php @@ -1,5 +1,4 @@ fail("Didn't find header for {$header}"); + $this->assertTrue($found,"Didn't find header for {$header}"); } /** @@ -151,10 +149,9 @@ public function assertHeaderEmitted(string $header, bool $ignoreCase = false): b * @param string $header The leading portion of the header we don't want to find * @param bool $ignoreCase * - * @return bool * @throws \Exception */ - public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): bool + public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void { $found = false; @@ -168,7 +165,7 @@ public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false) } $success = ! $found; - return $success ? $success : $this->fail("Found header for {$header}"); + $this->assertTrue($success,"Found header for {$header}"); } /** diff --git a/tests/system/Test/TestCaseEmissionsTest.php b/tests/system/Test/TestCaseEmissionsTest.php index 63f08595b424..a913df224781 100644 --- a/tests/system/Test/TestCaseEmissionsTest.php +++ b/tests/system/Test/TestCaseEmissionsTest.php @@ -1,5 +1,4 @@ 1 ) -// { -// ob_end_clean(); -// } -// ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); -// } -// -// public function tearDown() -// { -// ob_end_clean(); -// } - //-------------------------------------------------------------------- /** * This needs to be run as a separate process, since phpunit @@ -44,37 +29,32 @@ class TestCaseEmissionsTest extends \CIUnitTestCase * the body we thought would be sent actually was. * * @runInSeparateProcess + * @preserveGlobalState disabled */ - public function testHeaderEmitted() + public function testHeadersEmitted() { + $response = new Response(new App()); $response->pretend(FALSE); $body = 'Hello'; - $expected = $body; + $response->setBody($body); - // what do we think we're about to send? $response->setCookie('foo', 'bar'); $this->assertTrue($response->hasCookie('foo')); $this->assertTrue($response->hasCookie('foo', 'bar')); - - $response->setBody($body); - -//echo 'ob level at '.ob_get_level(); -ob_end_clean(); -ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE); -//$buffer = ob_get_clean(); // flush previous + // send it -// $buffer = ob_get_clean(); + ob_start(); $response->send(); - $buffer = ob_end_clean(); - // and what actually got sent?; test both ways - $actual = $response->getBody(); // what we thought was sent - $this->assertEquals($expected, $actual); + $buffer = ob_clean(); + if (ob_get_level() > 0) + ob_end_clean(); + + // and what actually got sent?; test both ways $this->assertHeaderEmitted("Set-Cookie: foo=bar;"); $this->assertHeaderEmitted("set-cookie: FOO=bar", true); - } /** @@ -91,32 +71,29 @@ public function testHeaderEmitted() * the body we thought would be sent actually was. * * @runInSeparateProcess + * @preserveGlobalState disabled */ -// public function testHeaderNotEmitted() -// { -// $response = new Response(new App()); -// $response->pretend(FALSE); -// -// $body = 'Hello'; -// $expected = $body; -// -// // what do we think we're about to send? -// $response->setCookie('foo', 'bar'); -// $this->assertTrue($response->hasCookie('foo')); -// $this->assertTrue($response->hasCookie('foo', 'bar')); -// -// // send it -// $response->setBody($body); -// -// ob_start(); -// $response->send(); -// $output = ob_get_clean(); // what really was sent -// // and what actually got sent?; test both ways -// $actual = $response->getBody(); // what we thought was sent -// -// $this->assertEquals($expected, $actual); -// $this->assertEquals($expected, $output); -// -// $this->assertHeaderNotEmitted("Set-Cookie: pop=corn", true); -// } + public function testHeadersNotEmitted() + { + $response = new Response(new App()); + $response->pretend(FALSE); + + $body = 'Hello'; + $response->setBody($body); + + // what do we think we're about to send? + $response->setCookie('foo', 'bar'); + $this->assertTrue($response->hasCookie('foo')); + $this->assertTrue($response->hasCookie('foo', 'bar')); + + // send it + ob_start(); + $response->send(); + $output = ob_clean(); // what really was sent + if (ob_get_level() > 0) + ob_end_clean(); + + $this->assertHeaderNotEmitted("Set-Cookie: pop=corn", true); + } + } diff --git a/tests/system/Test/TestCaseTest.php b/tests/system/Test/TestCaseTest.php index 66fb12cbb26e..cd17176dda66 100644 --- a/tests/system/Test/TestCaseTest.php +++ b/tests/system/Test/TestCaseTest.php @@ -1,5 +1,4 @@ stream_filter); } + //-------------------------------------------------------------------- + /** + * PHPunit emits headers before we get nominal control of + * the output stream, making header testing awkward, to say + * the least. This test is intended to make sure that this + * is happening as expected. + * + * TestCaseEmissionsTest is intended to circumvent PHPunit, + * and allow us to test our own header emissions. + * + */ + public function testPHPUnitHeadersEmitted() + { + $response = new Response(new App()); + $response->pretend(TRUE); + + $body = 'Hello'; + $response->setBody($body); + + $response->send(); + + // Did PHPunit do its thing? + $this->assertHeaderEmitted("Content-type: text/html;"); + $this->assertHeaderNotEmitted("Set-Cookie: foo=bar;"); + } + } From e0c8fd4d1be8839f4901230e42e81d24f9ca13d0 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Fri, 26 Oct 2018 08:12:11 -0700 Subject: [PATCH 7/7] Update changelog [ci-skip] --- user_guide_src/source/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst index e7d593f90b45..2791adc34e11 100644 --- a/user_guide_src/source/changelog.rst +++ b/user_guide_src/source/changelog.rst @@ -14,6 +14,8 @@ If you open this page on the repo github site, they will link to the PRs in question. application / + - composer.json #1312 + - Config/Boot/development, production, testing #1312 - Config/Paths #1341 - Config/Routes #1281 - Filters/Honeypot #1314 @@ -69,6 +71,7 @@ system / - Router/ - RouteCollection #1285, #1355 - Test/ + - CIUnitTestCase #1312 - FeatureTestCase #1282 - CodeIgniter #1239 #1337 - Common #1291 @@ -103,6 +106,9 @@ tests / - TimeTest #1273, #1316 - Router/ - RouteTest #1285, #1355 + - Test/ + - TestCaseEmissionsTest #1312 + - TestCaseTest #1312 - View/ - ParserTest #1311 - EntityTest #1319 @@ -138,6 +144,8 @@ user_guide_src /source/ - entities #1283 - outgoing/ - response #1340 + - testing/ + - overview #1312 - tutorial... #1265, #1281, #1294 / @@ -146,6 +154,7 @@ user_guide_src /source/ PRs merged: ----------- +- #1312 Add headerEmitted assertions to CIUnitTestCase - #1356 Testing/commands - #1355 Handle duplicate HTTP verb and generic rules properly - #1350 Checks if class is instantiable and is a command