diff --git a/.travis.yml b/.travis.yml index ecc69a1167c..93fa24ed984 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ version: ~> 1.0 import: - - silverstripe/silverstripe-travis-shared:config/provision/standard.yml + #- silverstripe/silverstripe-travis-shared:config/provision/standard.yml + - silverstripe/silverstripe-travis-shared:config/provision/self.yml env: global: @@ -15,41 +16,44 @@ jobs: - php: 7.1 env: - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - - PHPCS_TEST=1 + #- REQUIRE_INSTALLER="$REQUIRE_RECIPE" + #- PHPCS_TEST=1 - PHPUNIT_TEST=1 - PHPUNIT_SUITE="framework" - - COMPOSER_INSTALL_ARG="--prefer-lowest" - - php: 7.2 - env: - - DB=PGSQL - - PDO=1 - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - - PHPUNIT_TEST=1 - - PHPUNIT_TEST="framework" - - php: 7.3 - env: - - DB=MYSQL - - PDO=1 - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - - PHPUNIT_TEST=1 - - PHPUNIT_SUITE="framework" - - php: 7.3 - env: - - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - - PHPUNIT_TEST=1 - - PHPUNIT_SUITE="cms" + # - COMPOSER_INSTALL_ARG="--prefer-lowest" +# - php: 7.2 +# env: +# - DB=PGSQL +# - PDO=1 +# - REQUIRE_INSTALLER="$REQUIRE_RECIPE" +# - PHPUNIT_TEST=1 +# - PHPUNIT_TEST="framework" +# - php: 7.3 +# env: +# - DB=MYSQL +# - PDO=1 +# - REQUIRE_INSTALLER="$REQUIRE_RECIPE" +# - PHPUNIT_TEST=1 +# - PHPUNIT_SUITE="framework" +# - REQUIRE_RECIPE_TESTING="dev-two" +# - php: 7.3 +# env: +# - DB=MYSQL +# - REQUIRE_INSTALLER="$REQUIRE_RECIPE" +# - PHPUNIT_TEST=1 +# - PHPUNIT_SUITE="cms" +# - REQUIRE_RECIPE_TESTING="dev-two" - php: 7.4 env: - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" + # - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - PHPUNIT_TEST=1 - PHPUNIT_SUITE="framework" + - REQUIRE_RECIPE_TESTING="dev-two" - php: nightly env: - DB=MYSQL - - REQUIRE_INSTALLER="$REQUIRE_RECIPE" + # - REQUIRE_INSTALLER="$REQUIRE_RECIPE" - PHPUNIT_TEST=1 - PHPUNIT_SUITE="framework" - - COMPOSER_INSTALL_ARG="--ignore-platform-reqs" + - REQUIRE_RECIPE_TESTING="dev-two" diff --git a/composer.json b/composer.json index 520853c7292..de0c6eae7f5 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "silverstripe/framework", + "version": "4.99.99", "type": "silverstripe-vendormodule", "description": "The SilverStripe framework", "homepage": "http://silverstripe.org", @@ -30,8 +31,8 @@ "monolog/monolog": "~1.16", "nikic/php-parser": "^3 || ^4", "psr/container": "1.0.0", - "silverstripe/config": "^1@dev", - "silverstripe/assets": "^1@dev", + "silverstripe/config": "dev-pulls/1/sapphire-test-nine as 1.8.0", + "silverstripe/assets": "dev-pulls/1/sapphire-test-nine as 1.8.0", "silverstripe/vendor-plugin": "^1.4", "sminnee/callbacklist": "^0.1", "swiftmailer/swiftmailer": "~5.4", @@ -52,11 +53,70 @@ "ext-xml": "*" }, "require-dev": { - "sminnee/phpunit": "^5.7.29", - "sminnee/phpunit-mock-objects": "^3.4.9", - "silverstripe/versioned": "^1", + "silverstripe/recipe-testing": "dev-one || dev-two", + "dms/phpunit-arraysubset-asserts": "^0.3.0", + "silverstripe/versioned": "dev-pulls/1/sapphire-test-nine as 1.8.0", "squizlabs/php_codesniffer": "^3.5" }, + "repositories": [ + { + "type": "package", + "package": { + "description": "removed behat-extension from require which includes sminnee/phpunit. removed silverstripe/serve which does not allow php8", + "version": "dev-two", + "dist": { + "url": "https://github.com/silverstripe/recipe-testing/archive/f1299bc6cd7db134d4c873cbddf1aa104f46cabb.zip", + "type": "zip" + }, + "name": "silverstripe/recipe-testing", + "type": "silverstripe-recipe", + "require": { + "silverstripe/recipe-plugin": "^1", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3" + }, + "extra": { + "project-files": [ + "behat.yml", + "*.xml.dist" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true + } + }, + { + "type": "package", + "package": { + "description": "require-dev use silverstripe/recipe-testing instead of phpunit", + "name": "silverstripe/vendor-plugin", + "version": "1.4.99", + "dist": { + "url": "https://github.com/silverstripe/vendor-plugin/archive/bfd57780149fb12d1ddf0082c89b55662b569329.zip", + "type": "zip" + }, + "type": "composer-plugin", + "autoload": { + "psr-4": { + "SilverStripe\\VendorPlugin\\": "src/", + "SilverStripe\\VendorPlugin\\Tests\\": "tests/" + } + }, + "extra": { + "class": "SilverStripe\\VendorPlugin\\VendorPlugin" + }, + "require": { + "composer/installers": "^1.4", + "composer-plugin-api": "^1.1 || ^2", + "php": "^7.1 || ^8" + }, + "require-dev": { + "silverstripe/recipe-testing": "dev-one || dev-two", + "squizlabs/php_codesniffer": "^3" + } + } + } + ], "provide": { "psr/container-implementation": "1.0.0" }, @@ -64,6 +124,11 @@ "expose": [ "client/images", "client/styles" + ], + "project-files-installed": [ + "behat.yml", + "phpcs.xml.dist", + "phpunit.xml.dist" ] }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9f66d310f9e..464914469c6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,15 +4,17 @@ Standard module phpunit configuration. Requires PHPUnit ^5.7 --> - - tests/php - - - tests/php - - - vendor/silverstripe/cms/tests - + + + tests/php + + + tests/php + + + vendor/silverstripe/cms/tests + + . diff --git a/src/Dev/Constraint/SSListContains.php b/src/Dev/Constraint/SSListContains.php index 51eff1785b3..73618484e51 100644 --- a/src/Dev/Constraint/SSListContains.php +++ b/src/Dev/Constraint/SSListContains.php @@ -2,14 +2,16 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\SSListExporter; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; use SilverStripe\View\ViewableData; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -17,7 +19,7 @@ * Constraint for checking if a SS_List contains items matching the given * key-value pairs. */ -class SSListContains extends PHPUnit_Framework_Constraint implements TestOnly +class SSListContains extends Constraint implements TestOnly { /** * @var array @@ -31,9 +33,8 @@ class SSListContains extends PHPUnit_Framework_Constraint implements TestOnly */ protected $hasLeftoverItems = false; - public function __construct($matches) + public function __construct(array $matches) { - parent::__construct(); $this->exporter = new SSListExporter(); $this->matches = $matches; @@ -55,9 +56,9 @@ public function __construct($matches) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -86,7 +87,7 @@ public function evaluate($other, $description = '', $returnResult = false) * @param ViewableData $item * @return bool */ - protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item) + protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item): bool { $success = false; foreach ($this->matches as $key => $match) { @@ -107,7 +108,7 @@ protected function checkIfItemEvaluatesRemainingMatches(ViewableData $item) * * @return string */ - public function toString() + public function toString(): string { $matchToString = function ($key, $value) { return ' "' . $key . '" is "' . $value . '"'; @@ -132,7 +133,10 @@ public function toString() return $this->getStubForToString() . $allMatchesAsString; } - protected function getStubForToString() + /** + * @return string + */ + protected function getStubForToString(): string { return ' contains an item matching '; } diff --git a/src/Dev/Constraint/SSListContainsOnly.php b/src/Dev/Constraint/SSListContainsOnly.php index 0be563ac9a6..4555d0bd69e 100644 --- a/src/Dev/Constraint/SSListContainsOnly.php +++ b/src/Dev/Constraint/SSListContainsOnly.php @@ -2,12 +2,14 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -40,9 +42,9 @@ class SSListContainsOnly extends SSListContains implements TestOnly * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -71,7 +73,7 @@ public function evaluate($other, $description = '', $returnResult = false) return null; } - protected function getStubForToString() + protected function getStubForToString(): string { return $this->itemNotMatching ? parent::getStubForToString() diff --git a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php index d17ec07e09b..248840f4a40 100644 --- a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php +++ b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php @@ -2,13 +2,15 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\SSListExporter; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\SS_List; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -16,7 +18,7 @@ * Constraint for checking if every item in a SS_List matches a given match, * e.g. every Member has isActive set to true */ -class SSListContainsOnlyMatchingItems extends PHPUnit_Framework_Constraint implements TestOnly +class SSListContainsOnlyMatchingItems extends Constraint implements TestOnly { /** * @var array @@ -30,7 +32,6 @@ class SSListContainsOnlyMatchingItems extends PHPUnit_Framework_Constraint imple public function __construct($match) { - parent::__construct(); $this->exporter = new SSListExporter(); $this->constraint = new ViewableDataContains($match); @@ -53,9 +54,9 @@ public function __construct($match) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -82,7 +83,7 @@ public function evaluate($other, $description = '', $returnResult = false) * * @return string */ - public function toString() + public function toString(): string { return 'contains only Objects where "' . key($this->match) . '" is "' . current($this->match) . '"'; } diff --git a/src/Dev/Constraint/ViewableDataContains.php b/src/Dev/Constraint/ViewableDataContains.php index 6665c0f46fb..9e32cab8c70 100644 --- a/src/Dev/Constraint/ViewableDataContains.php +++ b/src/Dev/Constraint/ViewableDataContains.php @@ -2,13 +2,16 @@ namespace SilverStripe\Dev\Constraint; -use PHPUnit_Framework_Constraint; -use PHPUnit_Framework_ExpectationFailedException; -use PHPUnit_Util_InvalidArgumentHelper; +// use PHPUnit_Framework_Constraint; +use PHPUnit\Framework\Constraint\Constraint; +// use PHPUnit_Framework_ExpectationFailedException; +use PHPUnit\Framework\ExpectationFailedException; +// use PHPUnit_Util_InvalidArgumentHelper; use SilverStripe\Dev\TestOnly; use SilverStripe\View\ViewableData; +use SilverStripe\Dev\SapphireTest; -if (!class_exists(PHPUnit_Framework_Constraint::class)) { +if (!class_exists(Constraint::class)) { return; } @@ -16,7 +19,7 @@ * Constraint for checking if a ViewableData (e.g. ArrayData or any DataObject) contains fields matching the given * key-value pairs. */ -class ViewableDataContains extends PHPUnit_Framework_Constraint implements TestOnly +class ViewableDataContains extends Constraint implements TestOnly { /** * @var array @@ -27,12 +30,10 @@ class ViewableDataContains extends PHPUnit_Framework_Constraint implements TestO * ViewableDataContains constructor. * @param array $match */ - public function __construct($match) + public function __construct(array $match) { - parent::__construct(); - if (!is_array($match)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( + throw SapphireTest::createPHPUnitFrameworkException( 1, 'array' ); @@ -57,9 +58,9 @@ public function __construct($match) * * @return null|bool * - * @throws PHPUnit_Framework_ExpectationFailedException + * @throws ExpectationFailedException */ - public function evaluate($other, $description = '', $returnResult = false) + public function evaluate($other, $description = '', $returnResult = false): ?bool { $success = true; @@ -89,7 +90,7 @@ public function evaluate($other, $description = '', $returnResult = false) * * @return string */ - public function toString() + public function toString(): string { return 'contains only Objects where "' . key($this->match) . '" is "' . current($this->match) . '"'; } diff --git a/src/Dev/FunctionalTest.php b/src/Dev/FunctionalTest.php index 0d9e7459d6a..37073732527 100644 --- a/src/Dev/FunctionalTest.php +++ b/src/Dev/FunctionalTest.php @@ -2,7 +2,8 @@ namespace SilverStripe\Dev; -use PHPUnit_Framework_AssertionFailedError; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Constraint\IsEqualCanonicalizing; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\Session; @@ -31,408 +32,420 @@ * } * */ -class FunctionalTest extends SapphireTest implements TestOnly -{ - /** - * Set this to true on your sub-class to disable the use of themes in this test. - * This can be handy for functional testing of modules without having to worry about whether a user has changed - * behaviour by replacing the theme. - * - * @var bool - */ - protected static $disable_themes = false; - /** - * Set this to true on your sub-class to use the draft site by default for every test in this class. - * - * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your ->get() querystring requests instead - * @var bool - */ - protected static $use_draft_site = false; - - /** - * @var TestSession - */ - protected $mainSession = null; - - /** - * CSSContentParser for the most recently requested page. - * - * @var CSSContentParser - */ - protected $cssParser = null; - - /** - * If this is true, then 30x Location headers will be automatically followed. - * If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them. - * However, this will let you inspect the intermediary headers - * - * @var bool - */ - protected $autoFollowRedirection = true; +// IsEqualCanonicalizing is a new class added in PHPUnit 9, this is just a way to +// ensure that we're not using something like PHPUnit 8 +// Note: PHPUnit\Framework\TestCase exists in PHPUnit 5 as a forwards compatible wrapper +if (class_exists(IsEqualCanonicalizing::class)) { /** - * Returns the {@link Session} object for this test + * This is for phpunit 9 * - * @return Session + * If using phpunit 5, see legacy/FunctionalTest.php */ - public function session() + class FunctionalTest extends SapphireTest implements TestOnly { - return $this->mainSession->session(); - } - - protected function setUp() - { - parent::setUp(); - - // Skip calling FunctionalTest directly. - if (static::class == __CLASS__) { - $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + /** + * Set this to true on your sub-class to disable the use of themes in this test. + * This can be handy for functional testing of modules without having to worry about whether a user has changed + * behaviour by replacing the theme. + * + * @var bool + */ + protected static $disable_themes = false; + + /** + * Set this to true on your sub-class to use the draft site by default for every test in this class. + * + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your ->get() querystring requests instead + * @var bool + */ + protected static $use_draft_site = false; + + /** + * @var TestSession + */ + protected $mainSession = null; + + /** + * CSSContentParser for the most recently requested page. + * + * @var CSSContentParser + */ + protected $cssParser = null; + + /** + * If this is true, then 30x Location headers will be automatically followed. + * If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them. + * However, this will let you inspect the intermediary headers + * + * @var bool + */ + protected $autoFollowRedirection = true; + + /** + * Returns the {@link Session} object for this test + * + * @return Session + */ + public function session() + { + return $this->mainSession->session(); } - $this->mainSession = new TestSession(); + protected function setUp(): void + { + parent::setUp(); - // Disable theme, if necessary - if (static::get_disable_themes()) { - SSViewer::config()->update('theme_enabled', false); - } - - // Flush user - $this->logOut(); - - // Switch to draft site, if necessary - // If you rely on this you should be crafting stage-specific urls instead though. - if (static::get_use_draft_site()) { - $this->useDraftSite(); - } + // Skip calling FunctionalTest directly. + if (static::class == __CLASS__) { + $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + } - // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case - // basis. - BasicAuth::protect_entire_site(false); + $this->mainSession = new TestSession(); - SecurityToken::disable(); - } + // Disable theme, if necessary + if (static::get_disable_themes()) { + SSViewer::config()->update('theme_enabled', false); + } - protected function tearDown() - { - SecurityToken::enable(); - unset($this->mainSession); - parent::tearDown(); - } + // Flush user + $this->logOut(); - /** - * Run a test while mocking the base url with the provided value - * @param string $url The base URL to use for this test - * @param callable $callback The test to run - */ - protected function withBaseURL($url, $callback) - { - $oldBase = Config::inst()->get(Director::class, 'alternate_base_url'); - Config::modify()->set(Director::class, 'alternate_base_url', $url); - $callback($this); - Config::modify()->set(Director::class, 'alternate_base_url', $oldBase); - } + // Switch to draft site, if necessary + // If you rely on this you should be crafting stage-specific urls instead though. + if (static::get_use_draft_site()) { + $this->useDraftSite(); + } - /** - * Run a test while mocking the base folder with the provided value - * @param string $folder The base folder to use for this test - * @param callable $callback The test to run - */ - protected function withBaseFolder($folder, $callback) - { - $oldFolder = Config::inst()->get(Director::class, 'alternate_base_folder'); - Config::modify()->set(Director::class, 'alternate_base_folder', $folder); - $callback($this); - Config::modify()->set(Director::class, 'alternate_base_folder', $oldFolder); - } + // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case + // basis. + BasicAuth::protect_entire_site(false); - /** - * Submit a get request - * @uses Director::test() - * - * @param string $url - * @param Session $session - * @param array $headers - * @param array $cookies - * @return HTTPResponse - */ - public function get($url, $session = null, $headers = null, $cookies = null) - { - $this->cssParser = null; - $response = $this->mainSession->get($url, $session, $headers, $cookies); - if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { - $response = $this->mainSession->followRedirection(); + SecurityToken::disable(); } - return $response; - } - /** - * Submit a post request - * - * @uses Director::test() - * @param string $url - * @param array $data - * @param array $headers - * @param Session $session - * @param string $body - * @param array $cookies - * @return HTTPResponse - */ - public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) - { - $this->cssParser = null; - $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies); - if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { - $response = $this->mainSession->followRedirection(); + protected function tearDown(): void + { + SecurityToken::enable(); + unset($this->mainSession); + parent::tearDown(); } - return $response; - } - /** - * Submit the form with the given HTML ID, filling it out with the given data. - * Acts on the most recent response. - * - * Any data parameters have to be present in the form, with exact form field name - * and values, otherwise they are removed from the submission. - * - * Caution: Parameter names have to be formatted - * as they are in the form submission, not as they are interpreted by PHP. - * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two')) - * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two') - * - * @see http://www.simpletest.org/en/form_testing_documentation.html - * - * @param string $formID HTML 'id' attribute of a form (loaded through a previous response) - * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute) - * @param array $data Map of GET/POST data. - * @return HTTPResponse - */ - public function submitForm($formID, $button = null, $data = []) - { - $this->cssParser = null; - $response = $this->mainSession->submitForm($formID, $button, $data); - if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { - $response = $this->mainSession->followRedirection(); + /** + * Run a test while mocking the base url with the provided value + * @param string $url The base URL to use for this test + * @param callable $callback The test to run + */ + protected function withBaseURL($url, $callback) + { + $oldBase = Config::inst()->get(Director::class, 'alternate_base_url'); + Config::modify()->set(Director::class, 'alternate_base_url', $url); + $callback($this); + Config::modify()->set(Director::class, 'alternate_base_url', $oldBase); } - return $response; - } - /** - * Return the most recent content - * - * @return string - */ - public function content() - { - return $this->mainSession->lastContent(); - } + /** + * Run a test while mocking the base folder with the provided value + * @param string $folder The base folder to use for this test + * @param callable $callback The test to run + */ + protected function withBaseFolder($folder, $callback) + { + $oldFolder = Config::inst()->get(Director::class, 'alternate_base_folder'); + Config::modify()->set(Director::class, 'alternate_base_folder', $folder); + $callback($this); + Config::modify()->set(Director::class, 'alternate_base_folder', $oldFolder); + } - /** - * Find an attribute in a SimpleXMLElement object by name. - * @param SimpleXMLElement $object - * @param string $attribute Name of attribute to find - * @return SimpleXMLElement object of the attribute - */ - public function findAttribute($object, $attribute) - { - $found = false; - foreach ($object->attributes() as $a => $b) { - if ($a == $attribute) { - $found = $b; + /** + * Submit a get request + * @uses Director::test() + * + * @param string $url + * @param Session $session + * @param array $headers + * @param array $cookies + * @return HTTPResponse + */ + public function get($url, $session = null, $headers = null, $cookies = null) + { + $this->cssParser = null; + $response = $this->mainSession->get($url, $session, $headers, $cookies); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); } + return $response; } - return $found; - } - /** - * Return a CSSContentParser for the most recent content. - * - * @return CSSContentParser - */ - public function cssParser() - { - if (!$this->cssParser) { - $this->cssParser = new CSSContentParser($this->mainSession->lastContent()); + /** + * Submit a post request + * + * @uses Director::test() + * @param string $url + * @param array $data + * @param array $headers + * @param Session $session + * @param string $body + * @param array $cookies + * @return HTTPResponse + */ + public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) + { + $this->cssParser = null; + $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); + } + return $response; } - return $this->cssParser; - } - /** - * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. - * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag - * will be examined. The assertion fails if one of the expectedMatches fails to appear. - * - * Note:   characters are stripped from the content; make sure that your assertions take this into account. - * - * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' - * @param array|string $expectedMatches The content of at least one of the matched tags - * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError - */ - public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null) - { - if (is_string($expectedMatches)) { - $expectedMatches = [$expectedMatches]; + /** + * Submit the form with the given HTML ID, filling it out with the given data. + * Acts on the most recent response. + * + * Any data parameters have to be present in the form, with exact form field name + * and values, otherwise they are removed from the submission. + * + * Caution: Parameter names have to be formatted + * as they are in the form submission, not as they are interpreted by PHP. + * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two')) + * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two') + * + * @see http://www.simpletest.org/en/form_testing_documentation.html + * + * @param string $formID HTML 'id' attribute of a form (loaded through a previous response) + * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute) + * @param array $data Map of GET/POST data. + * @return HTTPResponse + */ + public function submitForm($formID, $button = null, $data = []) + { + $this->cssParser = null; + $response = $this->mainSession->submitForm($formID, $button, $data); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); + } + return $response; } - $items = $this->cssParser()->getBySelector($selector); + /** + * Return the most recent content + * + * @return string + */ + public function content() + { + return $this->mainSession->lastContent(); + } - $actuals = []; - if ($items) { - foreach ($items as $item) { - $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true; + /** + * Find an attribute in a SimpleXMLElement object by name. + * @param SimpleXMLElement $object + * @param string $attribute Name of attribute to find + * @return SimpleXMLElement object of the attribute + */ + public function findAttribute($object, $attribute) + { + $found = false; + foreach ($object->attributes() as $a => $b) { + if ($a == $attribute) { + $found = $b; + } } + return $found; } - $message = $message ?: - "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" - . implode("'\n'", $expectedMatches) . "'\n\n" - . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; - - foreach ($expectedMatches as $match) { - $this->assertTrue(isset($actuals[$match]), $message); + /** + * Return a CSSContentParser for the most recent content. + * + * @return CSSContentParser + */ + public function cssParser() + { + if (!$this->cssParser) { + $this->cssParser = new CSSContentParser($this->mainSession->lastContent()); + } + return $this->cssParser; } - } - /** - * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. - * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag - * will be examined. The assertion fails if one of the expectedMatches fails to appear. - * - * Note:   characters are stripped from the content; make sure that your assertions take this into account. - * - * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' - * @param array|string $expectedMatches The content of *all* matching tags as an array - * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError - */ - public function assertExactMatchBySelector($selector, $expectedMatches, $message = null) - { - if (is_string($expectedMatches)) { - $expectedMatches = [$expectedMatches]; - } + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of at least one of the matched tags + * @param string $message + * @throws AssertionFailedError + */ + public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } - $items = $this->cssParser()->getBySelector($selector); + $items = $this->cssParser()->getBySelector($selector); - $actuals = []; - if ($items) { - foreach ($items as $item) { - $actuals[] = trim(preg_replace('/\s+/', ' ', (string)$item)); + $actuals = []; + if ($items) { + foreach ($items as $item) { + $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true; + } } - } - $message = $message ?: - "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" + $message = $message ?: + "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n" - . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; - - $this->assertTrue($expectedMatches == $actuals, $message); - } + . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; - /** - * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. - * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag - * will be examined. The assertion fails if one of the expectedMatches fails to appear. - * - * Note:   characters are stripped from the content; make sure that your assertions take this into account. - * - * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' - * @param array|string $expectedMatches The content of at least one of the matched tags - * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError - */ - public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null) - { - if (is_string($expectedMatches)) { - $expectedMatches = [$expectedMatches]; + foreach ($expectedMatches as $match) { + $this->assertTrue(isset($actuals[$match]), $message); + } } - $items = $this->cssParser()->getBySelector($selector); + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of *all* matching tags as an array + * @param string $message + * @throws AssertionFailedError + */ + public function assertExactMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } + + $items = $this->cssParser()->getBySelector($selector); - $actuals = []; - if ($items) { - /** @var SimpleXMLElement $item */ - foreach ($items as $item) { - $actuals[$item->asXML()] = true; + $actuals = []; + if ($items) { + foreach ($items as $item) { + $actuals[] = trim(preg_replace('/\s+/', ' ', (string)$item)); + } } - } - $message = $message ?: - "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" - . implode("'\n'", $expectedMatches) . "'\n\n" - . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; + $message = $message ?: + "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; - foreach ($expectedMatches as $match) { - $this->assertTrue(isset($actuals[$match]), $message); + $this->assertTrue($expectedMatches == $actuals, $message); } - } - /** - * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. - * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag - * will be examined. The assertion fails if one of the expectedMatches fails to appear. - * - * Note:   characters are stripped from the content; make sure that your assertions take this into account. - * - * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' - * @param array|string $expectedMatches The content of *all* matched tags as an array - * @param string $message - * @throws PHPUnit_Framework_AssertionFailedError - */ - public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null) - { - $items = $this->cssParser()->getBySelector($selector); + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of at least one of the matched tags + * @param string $message + * @throws AssertionFailedError + */ + public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } - $actuals = []; - if ($items) { - /** @var SimpleXMLElement $item */ - foreach ($items as $item) { - $actuals[] = $item->asXML(); + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + /** @var SimpleXMLElement $item */ + foreach ($items as $item) { + $actuals[$item->asXML()] = true; + } + } + + $message = $message ?: + "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; + + foreach ($expectedMatches as $match) { + $this->assertTrue(isset($actuals[$match]), $message); } } - $message = $message ?: - "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" - . implode("'\n'", $expectedMatches) . "'\n\n" - . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of *all* matched tags as an array + * @param string $message + * @throws AssertionFailedError + */ + public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null) + { + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + /** @var SimpleXMLElement $item */ + foreach ($items as $item) { + $actuals[] = $item->asXML(); + } + } - $this->assertTrue($expectedMatches == $actuals, $message); - } + $message = $message ?: + "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; - /** - * Use the draft (stage) site for testing. - * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering - * your test. - * - * @deprecated 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite - * @param bool $enabled toggle the use of the draft site - */ - public function useDraftSite($enabled = true) - { - Deprecation::notice('5.0', 'Use ?stage=Stage querystring arguments instead of useDraftSite'); - if ($enabled) { - $this->session()->set('readingMode', 'Stage.Stage'); - $this->session()->set('unsecuredDraftSite', true); - } else { - $this->session()->clear('readingMode'); - $this->session()->clear('unsecuredDraftSite'); + $this->assertTrue($expectedMatches == $actuals, $message); } - } - /** - * @return bool - */ - public static function get_disable_themes() - { - return static::$disable_themes; - } + /** + * Use the draft (stage) site for testing. + * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering + * your test. + * + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite + * @param bool $enabled toggle the use of the draft site + */ + public function useDraftSite($enabled = true) + { + Deprecation::notice('5.0', 'Use ?stage=Stage querystring arguments instead of useDraftSite'); + if ($enabled) { + $this->session()->set('readingMode', 'Stage.Stage'); + $this->session()->set('unsecuredDraftSite', true); + } else { + $this->session()->clear('readingMode'); + $this->session()->clear('unsecuredDraftSite'); + } + } - /** - * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead - * @return bool - */ - public static function get_use_draft_site() - { - return static::$use_draft_site; + /** + * @return bool + */ + public static function get_disable_themes() + { + return static::$disable_themes; + } + + /** + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead + * @return bool + */ + public static function get_use_draft_site() + { + return static::$use_draft_site; + } } } diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index fb987006b55..4d9d2237e0c 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -3,10 +3,13 @@ namespace SilverStripe\Dev; use Exception; +use InvalidArgumentException; use LogicException; -use PHPUnit_Framework_Constraint_Not; -use PHPUnit_Framework_TestCase; -use PHPUnit_Util_InvalidArgumentHelper; +use PHPUnit\Framework\Constraint\LogicalNot; +use PHPUnit\Framework\Constraint\IsEqualCanonicalizing; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Exception as PHPUnitFrameworkException; +use PHPUnit\Util\Test as TestUtil; use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\Control\CLIRequestBuilder; use SilverStripe\Control\Controller; @@ -39,1281 +42,1433 @@ use SilverStripe\Security\Security; use SilverStripe\View\SSViewer; -if (!class_exists(PHPUnit_Framework_TestCase::class)) { - return; -} - -/** - * Test case class for the Sapphire framework. - * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier - * to work with. - * - * This class should not be used anywhere outside of unit tests, as phpunit may not be installed - * in production sites. - */ -class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly -{ - /** - * Path to fixture data for this test run. - * If passed as an array, multiple fixture files will be loaded. - * Please note that you won't be able to refer with "=>" notation - * between the fixtures, they act independent of each other. - * - * @var string|array - */ - protected static $fixture_file = null; - - /** - * @deprecated 4.0..5.0 Use FixtureTestState instead - * @var FixtureFactory - */ - protected $fixtureFactory; - - /** - * @var Boolean If set to TRUE, this will force a test database to be generated - * in {@link setUp()}. Note that this flag is overruled by the presence of a - * {@link $fixture_file}, which always forces a database build. - * - * @var bool - */ - protected $usesDatabase = null; - - /** - * This test will cleanup its state via transactions. - * If set to false a full schema is forced between tests, but at a performance cost. - * - * @var bool - */ - protected $usesTransactions = true; - - /** - * @var bool - */ - protected static $is_running_test = false; - - /** - * By default, setUp() does not require default records. Pass - * class names in here, and the require/augment default records - * function will be called on them. - * - * @var array - */ - protected $requireDefaultRecordsFrom = []; +// IsEqualCanonicalizing is a new class added in PHPUnit 9, this is just a way to +// ensure that we're not using something like PHPUnit 8 +// Note: PHPUnit\Framework\TestCase exists in PHPUnit 5 as a forwards compatible wrapper +if (class_exists(IsEqualCanonicalizing::class)) { /** - * A list of extensions that can't be applied during the execution of this run. If they are - * applied, they will be temporarily removed and a database migration called. + * This is for phpunit 9 * - * The keys of the are the classes that the extensions can't be applied the extensions to, and - * the values are an array of illegal extensions on that class. + * If using phpunit 5, see legacy/SapphireTest.php * - * Set a class to `*` to remove all extensions (unadvised) + * Test case class for the Sapphire framework. + * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier + * to work with. * - * @var array + * This class should not be used anywhere outside of unit tests, as phpunit may not be installed + * in production sites. */ - protected static $illegal_extensions = []; + class SapphireTest extends TestCase implements TestOnly + { + /** + * Path to fixture data for this test run. + * If passed as an array, multiple fixture files will be loaded. + * Please note that you won't be able to refer with "=>" notation + * between the fixtures, they act independent of each other. + * + * @var string|array + */ + protected static $fixture_file = null; + + /** + * @deprecated 4.0..5.0 Use FixtureTestState instead + * @var FixtureFactory + */ + protected $fixtureFactory; + + /** + * @var Boolean If set to TRUE, this will force a test database to be generated + * in {@link setUp()}. Note that this flag is overruled by the presence of a + * {@link $fixture_file}, which always forces a database build. + * + * @var bool + */ + protected $usesDatabase = null; + + /** + * This test will cleanup its state via transactions. + * If set to false a full schema is forced between tests, but at a performance cost. + * + * @var bool + */ + protected $usesTransactions = true; + + /** + * @var bool + */ + protected static $is_running_test = false; + + /** + * By default, setUp() does not require default records. Pass + * class names in here, and the require/augment default records + * function will be called on them. + * + * @var array + */ + protected $requireDefaultRecordsFrom = []; + + /** + * A list of extensions that can't be applied during the execution of this run. If they are + * applied, they will be temporarily removed and a database migration called. + * + * The keys of the are the classes that the extensions can't be applied the extensions to, and + * the values are an array of illegal extensions on that class. + * + * Set a class to `*` to remove all extensions (unadvised) + * + * @var array + */ + protected static $illegal_extensions = []; + + /** + * A list of extensions that must be applied during the execution of this run. If they are + * not applied, they will be temporarily added and a database migration called. + * + * The keys of the are the classes to apply the extensions to, and the values are an array + * of required extensions on that class. + * + * Example: + * + * array("MyTreeDataObject" => array("Versioned", "Hierarchy")) + * + * + * @var array + */ + protected static $required_extensions = []; + + /** + * By default, the test database won't contain any DataObjects that have the interface TestOnly. + * This variable lets you define additional TestOnly DataObjects to set up for this test. + * Set it to an array of DataObject subclass names. + * + * @var array + */ + protected static $extra_dataobjects = []; + + /** + * List of class names of {@see Controller} objects to register routes for + * Controllers must implement Link() method + * + * @var array + */ + protected static $extra_controllers = []; + + /** + * We need to disabling backing up of globals to avoid overriding + * the few globals SilverStripe relies on, like $lang for the i18n subsystem. + * + * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html + */ + protected $backupGlobals = false; + + /** + * State management container for SapphireTest + * + * @var SapphireTestState + */ + protected static $state = null; + + /** + * Temp database helper + * + * @var TempDatabase + */ + protected static $tempDB = null; + + /** + * @return TempDatabase + */ + public static function tempDB() + { + if (!class_exists(TempDatabase::class)) { + return null; + } - /** - * A list of extensions that must be applied during the execution of this run. If they are - * not applied, they will be temporarily added and a database migration called. - * - * The keys of the are the classes to apply the extensions to, and the values are an array - * of required extensions on that class. - * - * Example: - * - * array("MyTreeDataObject" => array("Versioned", "Hierarchy")) - * - * - * @var array - */ - protected static $required_extensions = []; + if (!static::$tempDB) { + static::$tempDB = TempDatabase::create(); + } + return static::$tempDB; + } - /** - * By default, the test database won't contain any DataObjects that have the interface TestOnly. - * This variable lets you define additional TestOnly DataObjects to set up for this test. - * Set it to an array of DataObject subclass names. - * - * @var array - */ - protected static $extra_dataobjects = []; + /** + * Gets illegal extensions for this class + * + * @return array + */ + public static function getIllegalExtensions() + { + return static::$illegal_extensions; + } - /** - * List of class names of {@see Controller} objects to register routes for - * Controllers must implement Link() method - * - * @var array - */ - protected static $extra_controllers = []; + /** + * Gets required extensions for this class + * + * @return array + */ + public static function getRequiredExtensions() + { + return static::$required_extensions; + } - /** - * We need to disabling backing up of globals to avoid overriding - * the few globals SilverStripe relies on, like $lang for the i18n subsystem. - * - * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html - */ - protected $backupGlobals = false; + /** + * Check if test bootstrapping has been performed. Must not be relied on + * outside of unit tests. + * + * @return bool + */ + protected static function is_running_test() + { + return self::$is_running_test; + } - /** - * State management container for SapphireTest - * - * @var SapphireTestState - */ - protected static $state = null; + /** + * Set test running state + * + * @param bool $bool + */ + protected static function set_is_running_test($bool) + { + self::$is_running_test = $bool; + } - /** - * Temp database helper - * - * @var TempDatabase - */ - protected static $tempDB = null; + /** + * @return String + */ + public static function get_fixture_file() + { + return static::$fixture_file; + } - /** - * @return TempDatabase - */ - public static function tempDB() - { - if (!class_exists(TempDatabase::class)) { - return null; + /** + * @return bool + */ + public function getUsesDatabase() + { + return $this->usesDatabase; } - if (!static::$tempDB) { - static::$tempDB = TempDatabase::create(); + /** + * @return bool + */ + public function getUsesTransactions() + { + return $this->usesTransactions; } - return static::$tempDB; - } - /** - * Gets illegal extensions for this class - * - * @return array - */ - public static function getIllegalExtensions() - { - return static::$illegal_extensions; - } + /** + * @return array + */ + public function getRequireDefaultRecordsFrom() + { + return $this->requireDefaultRecordsFrom; + } - /** - * Gets required extensions for this class - * - * @return array - */ - public static function getRequiredExtensions() - { - return static::$required_extensions; - } + /** + * Setup the test. + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUp() before custom setup code + */ + protected function setUp(): void + { + if (!defined('FRAMEWORK_PATH')) { + trigger_error( + 'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', + E_USER_WARNING + ); + } - /** - * Check if test bootstrapping has been performed. Must not be relied on - * outside of unit tests. - * - * @return bool - */ - protected static function is_running_test() - { - return self::$is_running_test; - } + // Call state helpers + static::$state->setUp($this); - /** - * Set test running state - * - * @param bool $bool - */ - protected static function set_is_running_test($bool) - { - self::$is_running_test = $bool; - } + // We cannot run the tests on this abstract class. + if (static::class == __CLASS__) { + $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + } - /** - * @return String - */ - public static function get_fixture_file() - { - return static::$fixture_file; - } + // i18n needs to be set to the defaults or tests fail + if (class_exists(i18n::class)) { + i18n::set_locale(i18n::config()->uninherited('default_locale')); + } - /** - * @return bool - */ - public function getUsesDatabase() - { - return $this->usesDatabase; - } + // Set default timezone consistently to avoid NZ-specific dependencies + date_default_timezone_set('UTC'); - /** - * @return bool - */ - public function getUsesTransactions() - { - return $this->usesTransactions; - } + if (class_exists(Member::class)) { + Member::set_password_validator(null); + } - /** - * @return array - */ - public function getRequireDefaultRecordsFrom() - { - return $this->requireDefaultRecordsFrom; - } + if (class_exists(Cookie::class)) { + Cookie::config()->update('report_errors', false); + } - /** - * Setup the test. - * Always sets up in order: - * - Reset php state - * - Nest - * - Custom state helpers - * - * User code should call parent::setUp() before custom setup code - */ - protected function setUp() - { - if (!defined('FRAMEWORK_PATH')) { - trigger_error( - 'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', - E_USER_WARNING - ); - } + if (class_exists(RootURLController::class)) { + RootURLController::reset(); + } - // Call state helpers - static::$state->setUp($this); + if (class_exists(Security::class)) { + Security::clear_database_is_ready(); + } - // We cannot run the tests on this abstract class. - if (static::class == __CLASS__) { - $this->markTestSkipped(sprintf('Skipping %s ', static::class)); - return; - } + // Set up test routes + $this->setUpRoutes(); - // i18n needs to be set to the defaults or tests fail - if (class_exists(i18n::class)) { - i18n::set_locale(i18n::config()->uninherited('default_locale')); - } + $fixtureFiles = $this->getFixturePaths(); - // Set default timezone consistently to avoid NZ-specific dependencies - date_default_timezone_set('UTC'); + if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { + // Assign fixture factory to deprecated prop in case old tests use it over the getter + /** @var FixtureTestState $fixtureState */ + $fixtureState = static::$state->getStateByName('fixtures'); + $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class); - if (class_exists(Member::class)) { - Member::set_password_validator(null); - } + $this->logInWithPermission('ADMIN'); + } - if (class_exists(Cookie::class)) { - Cookie::config()->update('report_errors', false); - } + // turn off template debugging + if (class_exists(SSViewer::class)) { + SSViewer::config()->update('source_file_comments', false); + } - if (class_exists(RootURLController::class)) { - RootURLController::reset(); - } + // Set up the test mailer + if (class_exists(TestMailer::class)) { + Injector::inst()->registerService(new TestMailer(), Mailer::class); + } - if (class_exists(Security::class)) { - Security::clear_database_is_ready(); + if (class_exists(Email::class)) { + Email::config()->remove('send_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); + } } - // Set up test routes - $this->setUpRoutes(); - $fixtureFiles = $this->getFixturePaths(); - - if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { - // Assign fixture factory to deprecated prop in case old tests use it over the getter - /** @var FixtureTestState $fixtureState */ - $fixtureState = static::$state->getStateByName('fixtures'); - $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class); - - $this->logInWithPermission('ADMIN'); - } + /** + * Helper method to determine if the current test should enable a test database + * + * @param $fixtureFiles + * @return bool + */ + protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) + { + $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; - // turn off template debugging - if (class_exists(SSViewer::class)) { - SSViewer::config()->update('source_file_comments', false); + return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) + || $this->currentTestEnablesDatabase(); } - // Set up the test mailer - if (class_exists(TestMailer::class)) { - Injector::inst()->registerService(new TestMailer(), Mailer::class); + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase" + * + * @return bool + */ + protected function currentTestEnablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] !== 'false'; } - if (class_exists(Email::class)) { - Email::config()->remove('send_all_emails_to'); - Email::config()->remove('send_all_emails_from'); - Email::config()->remove('cc_all_emails_to'); - Email::config()->remove('bcc_all_emails_to'); + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase false" + * + * @return bool + */ + protected function currentTestDisablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] === 'false'; } - } - - - - /** - * Helper method to determine if the current test should enable a test database - * - * @param $fixtureFiles - * @return bool - */ - protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) - { - $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; - - return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) - || $this->currentTestEnablesDatabase(); - } - - /** - * Helper method to check, if the current test uses the database. - * This can be switched on with the annotation "@useDatabase" - * - * @return bool - */ - protected function currentTestEnablesDatabase() - { - $annotations = $this->getAnnotations(); - - return array_key_exists('useDatabase', $annotations['method']) - && $annotations['method']['useDatabase'][0] !== 'false'; - } - /** - * Helper method to check, if the current test uses the database. - * This can be switched on with the annotation "@useDatabase false" - * - * @return bool - */ - protected function currentTestDisablesDatabase() - { - $annotations = $this->getAnnotations(); + /** + * Called once per test case ({@link SapphireTest} subclass). + * This is different to {@link setUp()}, which gets called once + * per method. Useful to initialize expensive operations which + * don't change state for any called method inside the test, + * e.g. dynamically adding an extension. See {@link teardownAfterClass()} + * for tearing down the state again. + * + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUpBeforeClass() before custom setup code + * + * @throws Exception + */ + public static function setUpBeforeClass(): void + { + // Start tests + static::start(); + + if (!static::$state) { + throw new Exception('SapphireTest failed to bootstrap!'); + } - return array_key_exists('useDatabase', $annotations['method']) - && $annotations['method']['useDatabase'][0] === 'false'; - } + // Call state helpers + static::$state->setUpOnce(static::class); - /** - * Called once per test case ({@link SapphireTest} subclass). - * This is different to {@link setUp()}, which gets called once - * per method. Useful to initialize expensive operations which - * don't change state for any called method inside the test, - * e.g. dynamically adding an extension. See {@link teardownAfterClass()} - * for tearing down the state again. - * - * Always sets up in order: - * - Reset php state - * - Nest - * - Custom state helpers - * - * User code should call parent::setUpBeforeClass() before custom setup code - * - * @throws Exception - */ - public static function setUpBeforeClass() - { - // Start tests - static::start(); + // Build DB if we have objects + if (class_exists(DataObject::class) && static::getExtraDataObjects()) { + DataObject::reset(); + static::resetDBSchema(true, true); + } + } - if (!static::$state) { - throw new Exception('SapphireTest failed to bootstrap!'); + /** + * tearDown method that's called once per test class rather once per test method. + * + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDownAfterClass() after custom tear down code + */ + public static function tearDownAfterClass(): void + { + // Call state helpers + static::$state->tearDownOnce(static::class); + + // Reset DB schema + static::resetDBSchema(); } - // Call state helpers - static::$state->setUpOnce(static::class); + /** + * @return FixtureFactory|false + * @deprecated 4.0.0:5.0.0 + */ + public function getFixtureFactory() + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class); + } - // Build DB if we have objects - if (class_exists(DataObject::class) && static::getExtraDataObjects()) { - DataObject::reset(); - static::resetDBSchema(true, true); + /** + * Sets a new fixture factory + * @param FixtureFactory $factory + * @return $this + * @deprecated 4.0.0:5.0.0 + */ + public function setFixtureFactory(FixtureFactory $factory) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->setFixtureFactory($factory, static::class); + $this->fixtureFactory = $factory; + return $this; } - } - /** - * tearDown method that's called once per test class rather once per test method. - * - * Always sets up in order: - * - Custom state helpers - * - Unnest - * - Reset php state - * - * User code should call parent::tearDownAfterClass() after custom tear down code - */ - public static function tearDownAfterClass() - { - // Call state helpers - static::$state->tearDownOnce(static::class); + /** + * Get the ID of an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * @return int + */ + protected function idFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $id = $state->getFixtureFactory(static::class)->getId($className, $identifier); + + if (!$id) { + throw new InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } - // Reset DB schema - static::resetDBSchema(); - } + return $id; + } - /** - * @deprecated 4.0.0:5.0.0 - * @return FixtureFactory|false - */ - public function getFixtureFactory() - { - Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - return $state->getFixtureFactory(static::class); - } + /** + * Return all of the IDs in the fixture of a particular class name. + * Will collate all IDs form all fixtures if multiple fixtures are provided. + * + * @param string $className The data class or table name, as specified in your fixture file + * @return array A map of fixture-identifier => object-id + */ + protected function allFixtureIDs($className) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class)->getIds($className); + } - /** - * Sets a new fixture factory - * @deprecated 4.0.0:5.0.0 - * @param FixtureFactory $factory - * @return $this - */ - public function setFixtureFactory(FixtureFactory $factory) - { - Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - $state->setFixtureFactory($factory, static::class); - $this->fixtureFactory = $factory; - return $this; - } + /** + * Get an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * + * @return DataObject + */ + protected function objFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $obj = $state->getFixtureFactory(static::class)->get($className, $identifier); + + if (!$obj) { + throw new InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } - /** - * Get the ID of an object from the fixture. - * - * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work - * @param string $identifier The identifier string, as provided in your fixture file - * @return int - */ - protected function idFromFixture($className, $identifier) - { - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - $id = $state->getFixtureFactory(static::class)->getId($className, $identifier); - - if (!$id) { - throw new \InvalidArgumentException(sprintf( - "Couldn't find object '%s' (class: %s)", - $identifier, - $className - )); + return $obj; } - return $id; - } + /** + * Load a YAML fixture file into the database. + * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. + * Doesn't clear existing fixtures. + * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir + * @deprecated 4.0.0:5.0.0 + * + */ + public function loadFixture($fixtureFile) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); + $fixture->writeInto($this->getFixtureFactory()); + } - /** - * Return all of the IDs in the fixture of a particular class name. - * Will collate all IDs form all fixtures if multiple fixtures are provided. - * - * @param string $className The data class or table name, as specified in your fixture file - * @return array A map of fixture-identifier => object-id - */ - protected function allFixtureIDs($className) - { - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - return $state->getFixtureFactory(static::class)->getIds($className); - } + /** + * Clear all fixtures which were previously loaded through + * {@link loadFixture()} + */ + public function clearFixtures() + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->getFixtureFactory(static::class)->clear(); + } - /** - * Get an object from the fixture. - * - * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work - * @param string $identifier The identifier string, as provided in your fixture file - * - * @return DataObject - */ - protected function objFromFixture($className, $identifier) - { - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - $obj = $state->getFixtureFactory(static::class)->get($className, $identifier); - - if (!$obj) { - throw new \InvalidArgumentException(sprintf( - "Couldn't find object '%s' (class: %s)", - $identifier, - $className - )); + /** + * Useful for writing unit tests without hardcoding folder structures. + * + * @return string Absolute path to current class. + */ + protected function getCurrentAbsolutePath() + { + $filename = ClassLoader::inst()->getItemPath(static::class); + if (!$filename) { + throw new LogicException('getItemPath returned null for ' . static::class + . '. Try adding flush=1 to the test run.'); + } + return dirname($filename); } - return $obj; - } + /** + * @return string File path relative to webroot + */ + protected function getCurrentRelativePath() + { + $base = Director::baseFolder(); + $path = $this->getCurrentAbsolutePath(); + if (substr($path, 0, strlen($base)) == $base) { + $path = preg_replace('/^\/*/', '', substr($path, strlen($base))); + } + return $path; + } - /** - * Load a YAML fixture file into the database. - * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. - * Doesn't clear existing fixtures. - * @deprecated 4.0.0:5.0.0 - * - * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir - */ - public function loadFixture($fixtureFile) - { - Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); - $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); - $fixture->writeInto($this->getFixtureFactory()); - } + /** + * Setup the test. + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDown() after custom tear down code + */ + protected function tearDown(): void + { + // Reset mocked datetime + if (class_exists(DBDatetime::class)) { + DBDatetime::clear_mock_now(); + } - /** - * Clear all fixtures which were previously loaded through - * {@link loadFixture()} - */ - public function clearFixtures() - { - /** @var FixtureTestState $state */ - $state = static::$state->getStateByName('fixtures'); - $state->getFixtureFactory(static::class)->clear(); - } + // Stop the redirection that might have been requested in the test. + // Note: Ideally a clean Controller should be created for each test. + // Now all tests executed in a batch share the same controller. + if (class_exists(Controller::class)) { + $controller = Controller::has_curr() ? Controller::curr() : null; + if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) { + $response->setStatusCode(200); + $response->removeHeader('Location'); + } + } - /** - * Useful for writing unit tests without hardcoding folder structures. - * - * @return string Absolute path to current class. - */ - protected function getCurrentAbsolutePath() - { - $filename = ClassLoader::inst()->getItemPath(static::class); - if (!$filename) { - throw new LogicException('getItemPath returned null for ' . static::class - . '. Try adding flush=1 to the test run.'); + // Call state helpers + static::$state->tearDown($this); } - return dirname($filename); - } - /** - * @return string File path relative to webroot - */ - protected function getCurrentRelativePath() - { - $base = Director::baseFolder(); - $path = $this->getCurrentAbsolutePath(); - if (substr($path, 0, strlen($base)) == $base) { - $path = preg_replace('/^\/*/', '', substr($path, strlen($base))); + public static function assertContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ): void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForNonObjectIdentity; + if ($strict) { + parent::assertContains($needle, $haystack, $message); + } else { + parent::assertContainsEquals($needle, $haystack, $message); + } + } else { + static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase); + } } - return $path; - } - /** - * Setup the test. - * Always sets up in order: - * - Custom state helpers - * - Unnest - * - Reset php state - * - * User code should call parent::tearDown() after custom tear down code - */ - protected function tearDown() - { - // Reset mocked datetime - if (class_exists(DBDatetime::class)) { - DBDatetime::clear_mock_now(); + public static function assertContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ): void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if ($ignoreCase) { + parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringContainsString($needle, $haystack, $message); + } } - // Stop the redirection that might have been requested in the test. - // Note: Ideally a clean Controller should be created for each test. - // Now all tests executed in a batch share the same controller. - if (class_exists(Controller::class)) { - $controller = Controller::has_curr() ? Controller::curr() : null; - if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) { - $response->setStatusCode(200); - $response->removeHeader('Location'); + public static function assertNotContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ): void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if (is_iterable($haystack)) { + $strict = is_object($needle) ? $checkForObjectIdentity : $checkForNonObjectIdentity; + if ($strict) { + parent::assertNotContains($needle, $haystack, $message); + } else { + parent::assertNotContainsEquals($needle, $haystack, $message); + } + } else { + static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase); } } - // Call state helpers - static::$state->tearDown($this); - } - - public static function assertContains( - $needle, - $haystack, - $message = '', - $ignoreCase = false, - $checkForObjectIdentity = true, - $checkForNonObjectIdentity = false - ) { - if ($haystack instanceof DBField) { - $haystack = (string)$haystack; + protected static function assertNotContainsNonIterable( + $needle, + $haystack, + $message = '', + $ignoreCase = false + ): void { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + if ($ignoreCase) { + parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message); + } else { + parent::assertStringNotContainsString($needle, $haystack, $message); + } } - parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); - } - public static function assertNotContains( - $needle, - $haystack, - $message = '', - $ignoreCase = false, - $checkForObjectIdentity = true, - $checkForNonObjectIdentity = false - ) { - if ($haystack instanceof DBField) { - $haystack = (string)$haystack; + /** + * Backwards compatibility for core tests + */ + public static function assertInternalType($expected, $actual, $message = '') + { + switch ($expected) { + case 'numeric': + static::assertIsNumeric($actual, $message); + return; + case 'integer': + case 'int': + static::assertIsInt($actual, $message); + return; + case 'double': + case 'float': + case 'real': + static::assertIsFloat($actual, $message); + return; + case 'string': + static::assertIsString($actual, $message); + return; + case 'boolean': + case 'bool': + static::assertIsBool($actual, $message); + return; + case 'null': + static::assertTrue(is_null($actual), $message); + return; + case 'array': + static::assertIsArray($actual, $message); + return; + case 'object': + static::assertIsObject($actual, $message); + return; + case 'resource': + static::assertIsResource($actual, $message); + return; + case 'resource (closed)': + static::assertIsClosedResource($actual, $message); + return; + case 'scalar': + static::assertIsScalar($actual, $message); + return; + case 'callable': + static::assertIsCallable($actual, $message); + return; + case 'iterable': + static::assertIsIterable($actual, $message); + return; + default: + return false; + } } - parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); - } - /** - * Clear the log of emails sent - * - * @return bool True if emails cleared - */ - public function clearEmails() - { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - if ($mailer instanceof TestMailer) { - $mailer->clearEmails(); - return true; + /** + * Clear the log of emails sent + * + * @return bool True if emails cleared + */ + public function clearEmails() + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + $mailer->clearEmails(); + return true; + } + return false; } - return false; - } - /** - * Search for an email that was sent. - * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. - * @param string $to - * @param string $from - * @param string $subject - * @param string $content - * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', - * 'HtmlContent' - */ - public static function findEmail($to, $from = null, $subject = null, $content = null) - { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - if ($mailer instanceof TestMailer) { - return $mailer->findEmail($to, $from, $subject, $content); + /** + * Search for an email that was sent. + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', + * 'HtmlContent' + */ + public static function findEmail($to, $from = null, $subject = null, $content = null) + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + return $mailer->findEmail($to, $from, $subject, $content); + } + return null; } - return null; - } - /** - * Assert that the matching email was sent since the last call to clearEmails() - * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. - * - * @param string $to - * @param string $from - * @param string $subject - * @param string $content - */ - public static function assertEmailSent($to, $from = null, $subject = null, $content = null) - { - $found = (bool)static::findEmail($to, $from, $subject, $content); + /** + * Assert that the matching email was sent since the last call to clearEmails() + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + */ + public static function assertEmailSent($to, $from = null, $subject = null, $content = null) + { + $found = (bool)static::findEmail($to, $from, $subject, $content); + + $infoParts = ''; + $withParts = []; + if ($to) { + $infoParts .= " to '$to'"; + } + if ($from) { + $infoParts .= " from '$from'"; + } + if ($subject) { + $withParts[] = "subject '$subject'"; + } + if ($content) { + $withParts[] = "content '$content'"; + } + if ($withParts) { + $infoParts .= ' with ' . implode(' and ', $withParts); + } - $infoParts = ''; - $withParts = []; - if ($to) { - $infoParts .= " to '$to'"; - } - if ($from) { - $infoParts .= " from '$from'"; - } - if ($subject) { - $withParts[] = "subject '$subject'"; - } - if ($content) { - $withParts[] = "content '$content'"; - } - if ($withParts) { - $infoParts .= ' with ' . implode(' and ', $withParts); + static::assertTrue( + $found, + "Failed asserting that an email was sent$infoParts." + ); } - static::assertTrue( - $found, - "Failed asserting that an email was sent$infoParts." - ); - } + /** + * Assert that the given {@link SS_List} includes DataObjects matching the given key-value + * pairs. Each match must correspond to 1 distinct record. + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members includes an entry with Email = sam@example.com: + * $this->assertListContains(['Email' => '...@example.com'], $members); + * + * Check that $members includes entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } - /** - * Assert that the given {@link SS_List} includes DataObjects matching the given key-value - * pairs. Each match must correspond to 1 distinct record. - * - * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can - * either pass a single pattern or an array of patterns. - * @param SS_List $list The {@link SS_List} to test. - * @param string $message - * - * Examples - * -------- - * Check that $members includes an entry with Email = sam@example.com: - * $this->assertListContains(['Email' => '...@example.com'], $members); - * - * Check that $members includes entries with Email = sam@example.com and with - * Email = ingo@example.com: - * $this->assertListContains([ - * ['Email' => '...@example.com'], - * ['Email' => 'i...@example.com'], - * ], $members); - */ - public static function assertListContains($matches, SS_List $list, $message = '') - { - if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( - 1, - 'array' + static::assertThat( + $list, + new SSListContains( + $matches + ), + $message ); } - static::assertThat( - $list, - new SSListContains( - $matches - ), - $message - ); - } + /** + * @param $matches + * @param $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListContains() instead + * + */ + public function assertDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListContains() instead'); + static::assertListContains($matches, $dataObjectSet); + } - /** - * @deprecated 4.0.0:5.0.0 Use assertListContains() instead - * - * @param $matches - * @param $dataObjectSet - */ - public function assertDOSContains($matches, $dataObjectSet) - { - Deprecation::notice('5.0', 'Use assertListContains() instead'); - return static::assertListContains($matches, $dataObjectSet); - } + /** + * Asserts that no items in a given list appear in the given dataobject list + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members doesn't have an entry with Email = sam@example.com: + * $this->assertListNotContains(['Email' => '...@example.com'], $members); + * + * Check that $members doesn't have entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListNotContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListNotContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } - /** - * Asserts that no items in a given list appear in the given dataobject list - * - * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can - * either pass a single pattern or an array of patterns. - * @param SS_List $list The {@link SS_List} to test. - * @param string $message - * - * Examples - * -------- - * Check that $members doesn't have an entry with Email = sam@example.com: - * $this->assertListNotContains(['Email' => '...@example.com'], $members); - * - * Check that $members doesn't have entries with Email = sam@example.com and with - * Email = ingo@example.com: - * $this->assertListNotContains([ - * ['Email' => '...@example.com'], - * ['Email' => 'i...@example.com'], - * ], $members); - */ - public static function assertListNotContains($matches, SS_List $list, $message = '') - { - if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( - 1, - 'array' + $constraint = new LogicalNot( + new SSListContains( + $matches + ) + ); + + static::assertThat( + $list, + $constraint, + $message ); } - $constraint = new PHPUnit_Framework_Constraint_Not( - new SSListContains( - $matches - ) - ); - - static::assertThat( - $list, - $constraint, - $message - ); - } + /** + * @param $matches + * @param $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead + * + */ + public static function assertNotDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListNotContains() instead'); + static::assertListNotContains($matches, $dataObjectSet); + } - /** - * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead - * - * @param $matches - * @param $dataObjectSet - */ - public static function assertNotDOSContains($matches, $dataObjectSet) - { - Deprecation::notice('5.0', 'Use assertListNotContains() instead'); - return static::assertListNotContains($matches, $dataObjectSet); - } + /** + * Assert that the given {@link SS_List} includes only DataObjects matching the given + * key-value pairs. Each match must correspond to 1 distinct record. + * + * Example + * -------- + * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't + * matter: + * $this->assertListEquals([ + * ['FirstName' =>'Sam', 'Surname' => 'Minnee'], + * ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], + * ], $members); + * + * @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListEquals($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } - /** - * Assert that the given {@link SS_List} includes only DataObjects matching the given - * key-value pairs. Each match must correspond to 1 distinct record. - * - * Example - * -------- - * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't - * matter: - * $this->assertListEquals([ - * ['FirstName' =>'Sam', 'Surname' => 'Minnee'], - * ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], - * ], $members); - * - * @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can - * either pass a single pattern or an array of patterns. - * @param mixed $list The {@link SS_List} to test. - * @param string $message - */ - public static function assertListEquals($matches, SS_List $list, $message = '') - { - if (!is_array($matches)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( - 1, - 'array' + static::assertThat( + $list, + new SSListContainsOnly( + $matches + ), + $message ); } - static::assertThat( - $list, - new SSListContainsOnly( - $matches - ), - $message - ); - } + /** + * @param $matches + * @param SS_List $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead + * + */ + public function assertDOSEquals($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListEquals() instead'); + static::assertListEquals($matches, $dataObjectSet); + } - /** - * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead - * - * @param $matches - * @param SS_List $dataObjectSet - */ - public function assertDOSEquals($matches, $dataObjectSet) - { - Deprecation::notice('5.0', 'Use assertListEquals() instead'); - return static::assertListEquals($matches, $dataObjectSet); - } + /** + * Assert that the every record in the given {@link SS_List} matches the given key-value + * pairs. + * + * Example + * -------- + * Check that every entry in $members has a Status of 'Active': + * $this->assertListAllMatch(['Status' => 'Active'], $members); + * + * @param mixed $match The pattern to match. The pattern is a map of key-value pairs. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListAllMatch($match, SS_List $list, $message = '') + { + if (!is_array($match)) { + throw self::createPHPUnitFrameworkException( + 1, + 'array' + ); + } - /** - * Assert that the every record in the given {@link SS_List} matches the given key-value - * pairs. - * - * Example - * -------- - * Check that every entry in $members has a Status of 'Active': - * $this->assertListAllMatch(['Status' => 'Active'], $members); - * - * @param mixed $match The pattern to match. The pattern is a map of key-value pairs. - * @param mixed $list The {@link SS_List} to test. - * @param string $message - */ - public static function assertListAllMatch($match, SS_List $list, $message = '') - { - if (!is_array($match)) { - throw PHPUnit_Util_InvalidArgumentHelper::factory( - 1, - 'array' + static::assertThat( + $list, + new SSListContainsOnlyMatchingItems( + $match + ), + $message ); } - static::assertThat( - $list, - new SSListContainsOnlyMatchingItems( - $match - ), - $message - ); - } - - /** - * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead - * - * @param $match - * @param SS_List $dataObjectSet - */ - public function assertDOSAllMatch($match, SS_List $dataObjectSet) - { - Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); - return static::assertListAllMatch($match, $dataObjectSet); - } - - /** - * Removes sequences of repeated whitespace characters from SQL queries - * making them suitable for string comparison - * - * @param string $sql - * @return string The cleaned and normalised SQL string - */ - protected static function normaliseSQL($sql) - { - return trim(preg_replace('/\s+/m', ' ', $sql)); - } - - /** - * Asserts that two SQL queries are equivalent - * - * @param string $expectedSQL - * @param string $actualSQL - * @param string $message - * @param float|int $delta - * @param integer $maxDepth - * @param boolean $canonicalize - * @param boolean $ignoreCase - */ - public static function assertSQLEquals( - $expectedSQL, - $actualSQL, - $message = '', - $delta = 0, - $maxDepth = 10, - $canonicalize = false, - $ignoreCase = false - ) { - // Normalise SQL queries to remove patterns of repeating whitespace - $expectedSQL = static::normaliseSQL($expectedSQL); - $actualSQL = static::normaliseSQL($actualSQL); - - static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); - } + /** + * @param $match + * @param SS_List $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead + * + */ + public function assertDOSAllMatch($match, SS_List $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); + static::assertListAllMatch($match, $dataObjectSet); + } - /** - * Asserts that a SQL query contains a SQL fragment - * - * @param string $needleSQL - * @param string $haystackSQL - * @param string $message - * @param boolean $ignoreCase - * @param boolean $checkForObjectIdentity - */ - public static function assertSQLContains( - $needleSQL, - $haystackSQL, - $message = '', - $ignoreCase = false, - $checkForObjectIdentity = true - ) { - $needleSQL = static::normaliseSQL($needleSQL); - $haystackSQL = static::normaliseSQL($haystackSQL); - - static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); - } + /** + * Removes sequences of repeated whitespace characters from SQL queries + * making them suitable for string comparison + * + * @param string $sql + * @return string The cleaned and normalised SQL string + */ + protected static function normaliseSQL($sql) + { + return trim(preg_replace('/\s+/m', ' ', $sql)); + } - /** - * Asserts that a SQL query contains a SQL fragment - * - * @param string $needleSQL - * @param string $haystackSQL - * @param string $message - * @param boolean $ignoreCase - * @param boolean $checkForObjectIdentity - */ - public static function assertSQLNotContains( - $needleSQL, - $haystackSQL, - $message = '', - $ignoreCase = false, - $checkForObjectIdentity = true - ) { - $needleSQL = static::normaliseSQL($needleSQL); - $haystackSQL = static::normaliseSQL($haystackSQL); - - static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); - } + /** + * Asserts that two SQL queries are equivalent + * + * @param string $expectedSQL + * @param string $actualSQL + * @param string $message + * @param float|int $delta + * @param integer $maxDepth + * @param boolean $canonicalize + * @param boolean $ignoreCase + */ + public static function assertSQLEquals( + $expectedSQL, + $actualSQL, + $message = '' + ) { + // Normalise SQL queries to remove patterns of repeating whitespace + $expectedSQL = static::normaliseSQL($expectedSQL); + $actualSQL = static::normaliseSQL($actualSQL); + + static::assertEquals($expectedSQL, $actualSQL, $message); + } - /** - * Start test environment - */ - public static function start() - { - if (static::is_running_test()) { - return; + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + if (is_iterable($haystackSQL)) { + /** @var iterable $iterableHaystackSQL */ + $iterableHaystackSQL = $haystackSQL; + static::assertContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } } - // Health check - if (InjectorLoader::inst()->countManifests()) { - throw new LogicException('SapphireTest::start() cannot be called within another application'); + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLNotContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + if (is_iterable($haystackSQL)) { + /** @var iterable $iterableHaystackSQL */ + $iterableHaystackSQL = $haystackSQL; + static::assertNotContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } else { + static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); + } } - static::set_is_running_test(true); - // Test application - $kernel = new TestKernel(BASE_PATH); + /** + * Start test environment + */ + public static function start() + { + if (static::is_running_test()) { + return; + } - if (class_exists(HTTPApplication::class)) { - // Mock request - $_SERVER['argv'] = ['vendor/bin/phpunit', '/']; - $request = CLIRequestBuilder::createFromEnvironment(); + // Health check + if (InjectorLoader::inst()->countManifests()) { + throw new LogicException('SapphireTest::start() cannot be called within another application'); + } + static::set_is_running_test(true); - $app = new HTTPApplication($kernel); - $flush = array_key_exists('flush', $request->getVars()); + // Test application + $kernel = new TestKernel(BASE_PATH); - // Custom application - $res = $app->execute($request, function (HTTPRequest $request) { - // Start session and execute - $request->getSession()->init($request); + if (class_exists(HTTPApplication::class)) { + // Mock request + $_SERVER['argv'] = ['vendor/bin/phpunit', '/']; + $request = CLIRequestBuilder::createFromEnvironment(); - // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class - // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) - DataObject::reset(); + $app = new HTTPApplication($kernel); + $flush = array_key_exists('flush', $request->getVars()); - // Set dummy controller; - $controller = Controller::create(); - $controller->setRequest($request); - $controller->pushCurrent(); - $controller->doInit(); - }, $flush); + // Custom application + $res = $app->execute($request, function (HTTPRequest $request) { + // Start session and execute + $request->getSession()->init($request); - if ($res && $res->isError()) { - throw new LogicException($res->getBody()); - } - } else { - // Allow flush from the command line in the absence of HTTPApplication's special sauce - $flush = false; - foreach ($_SERVER['argv'] as $arg) { - if (preg_match('/^(--)?flush(=1)?$/', $arg)) { - $flush = true; + // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class + // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) + DataObject::reset(); + + // Set dummy controller; + $controller = Controller::create(); + $controller->setRequest($request); + $controller->pushCurrent(); + $controller->doInit(); + }, $flush); + + if ($res && $res->isError()) { + throw new LogicException($res->getBody()); } + } else { + // Allow flush from the command line in the absence of HTTPApplication's special sauce + $flush = false; + foreach ($_SERVER['argv'] as $arg) { + if (preg_match('/^(--)?flush(=1)?$/', $arg)) { + $flush = true; + } + } + $kernel->boot($flush); } - $kernel->boot($flush); - } - - // Register state - static::$state = SapphireTestState::singleton(); - // Register temp DB holder - static::tempDB(); - } - /** - * Reset the testing database's schema, but only if it is active - * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included - * @param bool $forceCreate Force DB to be created if it doesn't exist - */ - public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false) - { - if (!static::$tempDB) { - return; + // Register state + static::$state = SapphireTestState::singleton(); + // Register temp DB holder + static::tempDB(); } - // Check if DB is active before reset - if (!static::$tempDB->isUsed()) { - if (!$forceCreate) { + /** + * Reset the testing database's schema, but only if it is active + * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included + * @param bool $forceCreate Force DB to be created if it doesn't exist + */ + public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false) + { + if (!static::$tempDB) { return; } - static::$tempDB->build(); - } - $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; - static::$tempDB->resetDBSchema((array)$extraDataObjects); - } - - /** - * A wrapper for automatically performing callbacks as a user with a specific permission - * - * @param string|array $permCode - * @param callable $callback - * @return mixed - */ - public function actWithPermission($permCode, $callback) - { - return Member::actAs($this->createMemberWithPermission($permCode), $callback); - } - /** - * Create Member and Group objects on demand with specific permission code - * - * @param string|array $permCode - * @return Member - */ - protected function createMemberWithPermission($permCode) - { - if (is_array($permCode)) { - $permArray = $permCode; - $permCode = implode('.', $permCode); - } else { - $permArray = [$permCode]; + // Check if DB is active before reset + if (!static::$tempDB->isUsed()) { + if (!$forceCreate) { + return; + } + static::$tempDB->build(); + } + $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; + static::$tempDB->resetDBSchema((array)$extraDataObjects); } - // Check cached member - if (isset($this->cache_generatedMembers[$permCode])) { - $member = $this->cache_generatedMembers[$permCode]; - } else { - // Generate group with these permissions - $group = Group::create(); - $group->Title = "$permCode group"; - $group->write(); - - // Create each individual permission - foreach ($permArray as $permArrayItem) { - $permission = Permission::create(); - $permission->Code = $permArrayItem; - $permission->write(); - $group->Permissions()->add($permission); - } + /** + * A wrapper for automatically performing callbacks as a user with a specific permission + * + * @param string|array $permCode + * @param callable $callback + * @return mixed + */ + public function actWithPermission($permCode, $callback) + { + return Member::actAs($this->createMemberWithPermission($permCode), $callback); + } - $member = Member::get()->filter([ - 'Email' => "$permCode@example.org", - ])->first(); - if (!$member) { - $member = Member::create(); + /** + * Create Member and Group objects on demand with specific permission code + * + * @param string|array $permCode + * @return Member + */ + protected function createMemberWithPermission($permCode) + { + if (is_array($permCode)) { + $permArray = $permCode; + $permCode = implode('.', $permCode); + } else { + $permArray = [$permCode]; } - $member->FirstName = $permCode; - $member->Surname = 'User'; - $member->Email = "$permCode@example.org"; - $member->write(); - $group->Members()->add($member); + // Check cached member + if (isset($this->cache_generatedMembers[$permCode])) { + $member = $this->cache_generatedMembers[$permCode]; + } else { + // Generate group with these permissions + $group = Group::create(); + $group->Title = "$permCode group"; + $group->write(); + + // Create each individual permission + foreach ($permArray as $permArrayItem) { + $permission = Permission::create(); + $permission->Code = $permArrayItem; + $permission->write(); + $group->Permissions()->add($permission); + } - $this->cache_generatedMembers[$permCode] = $member; - } - return $member; - } + $member = Member::get()->filter([ + 'Email' => "$permCode@example.org", + ])->first(); + if (!$member) { + $member = Member::create(); + } - /** - * Create a member and group with the given permission code, and log in with it. - * Returns the member ID. - * - * @param string|array $permCode Either a permission, or list of permissions - * @return int Member ID - */ - public function logInWithPermission($permCode = 'ADMIN') - { - $member = $this->createMemberWithPermission($permCode); - $this->logInAs($member); - return $member->ID; - } + $member->FirstName = $permCode; + $member->Surname = 'User'; + $member->Email = "$permCode@example.org"; + $member->write(); + $group->Members()->add($member); - /** - * Log in as the given member - * - * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in - */ - public function logInAs($member) - { - if (is_numeric($member)) { - $member = DataObject::get_by_id(Member::class, $member); - } elseif (!is_object($member)) { - $member = $this->objFromFixture(Member::class, $member); + $this->cache_generatedMembers[$permCode] = $member; + } + return $member; } - Injector::inst()->get(IdentityStore::class)->logIn($member); - } - /** - * Log out the current user - */ - public function logOut() - { - /** @var IdentityStore $store */ - $store = Injector::inst()->get(IdentityStore::class); - $store->logOut(); - } - - /** - * Cache for logInWithPermission() - */ - protected $cache_generatedMembers = []; - - /** - * Test against a theme. - * - * @param string $themeBaseDir themes directory - * @param string $theme Theme name - * @param callable $callback - * @throws Exception - */ - protected function useTestTheme($themeBaseDir, $theme, $callback) - { - Config::nest(); - if (strpos($themeBaseDir, BASE_PATH) === 0) { - $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); + /** + * Create a member and group with the given permission code, and log in with it. + * Returns the member ID. + * + * @param string|array $permCode Either a permission, or list of permissions + * @return int Member ID + */ + public function logInWithPermission($permCode = 'ADMIN') + { + $member = $this->createMemberWithPermission($permCode); + $this->logInAs($member); + return $member->ID; } - SSViewer::config()->update('theme_enabled', true); - SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']); - try { - $callback(); - } finally { - Config::unnest(); + /** + * Log in as the given member + * + * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in + */ + public function logInAs($member) + { + if (is_numeric($member)) { + $member = DataObject::get_by_id(Member::class, $member); + } elseif (!is_object($member)) { + $member = $this->objFromFixture(Member::class, $member); + } + Injector::inst()->get(IdentityStore::class)->logIn($member); } - } - /** - * Get fixture paths for this test - * - * @return array List of paths - */ - protected function getFixturePaths() - { - $fixtureFile = static::get_fixture_file(); - if (empty($fixtureFile)) { - return []; + /** + * Log out the current user + */ + public function logOut() + { + /** @var IdentityStore $store */ + $store = Injector::inst()->get(IdentityStore::class); + $store->logOut(); } - $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; + /** + * Cache for logInWithPermission() + */ + protected $cache_generatedMembers = []; + + /** + * Test against a theme. + * + * @param string $themeBaseDir themes directory + * @param string $theme Theme name + * @param callable $callback + * @throws Exception + */ + protected function useTestTheme($themeBaseDir, $theme, $callback) + { + Config::nest(); + if (strpos($themeBaseDir, BASE_PATH) === 0) { + $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); + } + SSViewer::config()->update('theme_enabled', true); + SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']); - return array_map(function ($fixtureFilePath) { - return $this->resolveFixturePath($fixtureFilePath); - }, $fixtureFiles); - } + try { + $callback(); + } finally { + Config::unnest(); + } + } - /** - * Return all extra objects to scaffold for this test - * @return array - */ - public static function getExtraDataObjects() - { - return static::$extra_dataobjects; - } + /** + * Get fixture paths for this test + * + * @return array List of paths + */ + protected function getFixturePaths() + { + $fixtureFile = static::get_fixture_file(); + if (empty($fixtureFile)) { + return []; + } - /** - * Get additional controller classes to register routes for - * - * @return array - */ - public static function getExtraControllers() - { - return static::$extra_controllers; - } + $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; - /** - * Map a fixture path to a physical file - * - * @param string $fixtureFilePath - * @return string - */ - protected function resolveFixturePath($fixtureFilePath) - { - // support loading via composer name path. - if (strpos($fixtureFilePath, ':') !== false) { - return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); + return array_map(function ($fixtureFilePath) { + return $this->resolveFixturePath($fixtureFilePath); + }, $fixtureFiles); } - // Support fixture paths relative to the test class, rather than relative to webroot - // String checking is faster than file_exists() calls. - $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); - if ($resolvedPath) { - return $resolvedPath; + /** + * Return all extra objects to scaffold for this test + * @return array + */ + public static function getExtraDataObjects() + { + return static::$extra_dataobjects; } - // Check if file exists relative to base dir - $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); - if ($resolvedPath) { - return $resolvedPath; + /** + * Get additional controller classes to register routes for + * + * @return array + */ + public static function getExtraControllers() + { + return static::$extra_controllers; } - return $fixtureFilePath; - } + /** + * Map a fixture path to a physical file + * + * @param string $fixtureFilePath + * @return string + */ + protected function resolveFixturePath($fixtureFilePath) + { + // support loading via composer name path. + if (strpos($fixtureFilePath, ':') !== false) { + return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); + } - protected function setUpRoutes() - { - if (!class_exists(Director::class)) { - return; + // Support fixture paths relative to the test class, rather than relative to webroot + // String checking is faster than file_exists() calls. + $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + // Check if file exists relative to base dir + $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + return $fixtureFilePath; } - // Get overridden routes - $rules = $this->getExtraRoutes(); + protected function setUpRoutes() + { + if (!class_exists(Director::class)) { + return; + } + + // Get overridden routes + $rules = $this->getExtraRoutes(); - // Add all other routes - foreach (Director::config()->uninherited('rules') as $route => $rule) { - if (!isset($rules[$route])) { - $rules[$route] = $rule; + // Add all other routes + foreach (Director::config()->uninherited('rules') as $route => $rule) { + if (!isset($rules[$route])) { + $rules[$route] = $rule; + } } + + // Add default catch-all rule + $rules['$Controller//$Action/$ID/$OtherID'] = '*'; + + // Add controller-name auto-routing + Director::config()->set('rules', $rules); } - // Add default catch-all rule - $rules['$Controller//$Action/$ID/$OtherID'] = '*'; + /** + * Get extra routes to merge into Director.rules + * + * @return array + */ + protected function getExtraRoutes() + { + $rules = []; + foreach ($this->getExtraControllers() as $class) { + $controllerInst = Controller::singleton($class); + $link = Director::makeRelative($controllerInst->Link()); + $route = rtrim($link, '/') . '//$Action/$ID/$OtherID'; + $rules[$route] = $class; + } + return $rules; + } - // Add controller-name auto-routing - Director::config()->set('rules', $rules); - } + // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 === + + /** + * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory() + * + * @param $argument + * @param $type + * @param $value + */ + public static function createPHPUnitFrameworkException($argument, $type, $value = null) + { + $stack = debug_backtrace(false); + + return new PHPUnitFrameworkException( + sprintf( + 'Argument #%d%sof %s::%s() must be a %s', + $argument, + $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', + $stack[1]['class'], + $stack[1]['function'], + $type + ) + ); + } - /** - * Get extra routes to merge into Director.rules - * - * @return array - */ - protected function getExtraRoutes() - { - $rules = []; - foreach ($this->getExtraControllers() as $class) { - $controllerInst = Controller::singleton($class); - $link = Director::makeRelative($controllerInst->Link()); - $route = rtrim($link, '/') . '//$Action/$ID/$OtherID'; - $rules[$route] = $class; + /** + * Returns the annotations for this test. + * + * @return array + */ + public function getAnnotations() + { + return TestUtil::parseTestMethodAnnotations( + get_class($this), + $this->getName(false) + ); } - return $rules; } } diff --git a/src/Dev/_legacy/FunctionalTest.php b/src/Dev/_legacy/FunctionalTest.php new file mode 100644 index 00000000000..6922a5b78b0 --- /dev/null +++ b/src/Dev/_legacy/FunctionalTest.php @@ -0,0 +1,448 @@ + + * public function testMyForm() { + * // Visit a URL + * $this->get("your/url"); + * + * // Submit a form on the page that you get in response + * $this->submitForm("MyForm_ID", "action_dologin", array("Email" => "invalid email ^&*&^")); + * + * // Validate the content that is returned + * $this->assertExactMatchBySelector("#MyForm_ID p.error", array("That email address is invalid.")); + * } + * + */ + +if (!class_exists(PHPUnit_Framework_TestCase::class)) { + return; +} + +/** + * This is for phpunit 5.7 / php <=7.2 + * TODO: deprecated? + */ +class FunctionalTest extends SapphireTest implements TestOnly +{ + /** + * Set this to true on your sub-class to disable the use of themes in this test. + * This can be handy for functional testing of modules without having to worry about whether a user has changed + * behaviour by replacing the theme. + * + * @var bool + */ + protected static $disable_themes = false; + + /** + * Set this to true on your sub-class to use the draft site by default for every test in this class. + * + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your ->get() querystring requests instead + * @var bool + */ + protected static $use_draft_site = false; + + /** + * @var TestSession + */ + protected $mainSession = null; + + /** + * CSSContentParser for the most recently requested page. + * + * @var CSSContentParser + */ + protected $cssParser = null; + + /** + * If this is true, then 30x Location headers will be automatically followed. + * If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them. + * However, this will let you inspect the intermediary headers + * + * @var bool + */ + protected $autoFollowRedirection = true; + + /** + * Returns the {@link Session} object for this test + * + * @return Session + */ + public function session() + { + return $this->mainSession->session(); + } + + protected function setUp(): void + { + parent::setUp(); + + // Skip calling FunctionalTest directly. + if (static::class == __CLASS__) { + $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + } + + $this->mainSession = new TestSession(); + + // Disable theme, if necessary + if (static::get_disable_themes()) { + SSViewer::config()->update('theme_enabled', false); + } + + // Flush user + $this->logOut(); + + // Switch to draft site, if necessary + // If you rely on this you should be crafting stage-specific urls instead though. + if (static::get_use_draft_site()) { + $this->useDraftSite(); + } + + // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case + // basis. + BasicAuth::protect_entire_site(false); + + SecurityToken::disable(); + } + + protected function tearDown(): void + { + SecurityToken::enable(); + unset($this->mainSession); + parent::tearDown(); + } + + /** + * Run a test while mocking the base url with the provided value + * @param string $url The base URL to use for this test + * @param callable $callback The test to run + */ + protected function withBaseURL($url, $callback) + { + $oldBase = Config::inst()->get(Director::class, 'alternate_base_url'); + Config::modify()->set(Director::class, 'alternate_base_url', $url); + $callback($this); + Config::modify()->set(Director::class, 'alternate_base_url', $oldBase); + } + + /** + * Run a test while mocking the base folder with the provided value + * @param string $folder The base folder to use for this test + * @param callable $callback The test to run + */ + protected function withBaseFolder($folder, $callback) + { + $oldFolder = Config::inst()->get(Director::class, 'alternate_base_folder'); + Config::modify()->set(Director::class, 'alternate_base_folder', $folder); + $callback($this); + Config::modify()->set(Director::class, 'alternate_base_folder', $oldFolder); + } + + /** + * Submit a get request + * @uses Director::test() + * + * @param string $url + * @param Session $session + * @param array $headers + * @param array $cookies + * @return HTTPResponse + */ + public function get($url, $session = null, $headers = null, $cookies = null) + { + $this->cssParser = null; + $response = $this->mainSession->get($url, $session, $headers, $cookies); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); + } + return $response; + } + + /** + * Submit a post request + * + * @uses Director::test() + * @param string $url + * @param array $data + * @param array $headers + * @param Session $session + * @param string $body + * @param array $cookies + * @return HTTPResponse + */ + public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) + { + $this->cssParser = null; + $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); + } + return $response; + } + + /** + * Submit the form with the given HTML ID, filling it out with the given data. + * Acts on the most recent response. + * + * Any data parameters have to be present in the form, with exact form field name + * and values, otherwise they are removed from the submission. + * + * Caution: Parameter names have to be formatted + * as they are in the form submission, not as they are interpreted by PHP. + * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two')) + * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two') + * + * @see http://www.simpletest.org/en/form_testing_documentation.html + * + * @param string $formID HTML 'id' attribute of a form (loaded through a previous response) + * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute) + * @param array $data Map of GET/POST data. + * @return HTTPResponse + */ + public function submitForm($formID, $button = null, $data = []) + { + $this->cssParser = null; + $response = $this->mainSession->submitForm($formID, $button, $data); + if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { + $response = $this->mainSession->followRedirection(); + } + return $response; + } + + /** + * Return the most recent content + * + * @return string + */ + public function content() + { + return $this->mainSession->lastContent(); + } + + /** + * Find an attribute in a SimpleXMLElement object by name. + * @param SimpleXMLElement $object + * @param string $attribute Name of attribute to find + * @return SimpleXMLElement object of the attribute + */ + public function findAttribute($object, $attribute) + { + $found = false; + foreach ($object->attributes() as $a => $b) { + if ($a == $attribute) { + $found = $b; + } + } + return $found; + } + + /** + * Return a CSSContentParser for the most recent content. + * + * @return CSSContentParser + */ + public function cssParser() + { + if (!$this->cssParser) { + $this->cssParser = new CSSContentParser($this->mainSession->lastContent()); + } + return $this->cssParser; + } + + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of at least one of the matched tags + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } + + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + foreach ($items as $item) { + $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true; + } + } + + $message = $message ?: + "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; + + foreach ($expectedMatches as $match) { + $this->assertTrue(isset($actuals[$match]), $message); + } + } + + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of *all* matching tags as an array + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public function assertExactMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } + + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + foreach ($items as $item) { + $actuals[] = trim(preg_replace('/\s+/', ' ', (string)$item)); + } + } + + $message = $message ?: + "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; + + $this->assertTrue($expectedMatches == $actuals, $message); + } + + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of at least one of the matched tags + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null) + { + if (is_string($expectedMatches)) { + $expectedMatches = [$expectedMatches]; + } + + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + /** @var SimpleXMLElement $item */ + foreach ($items as $item) { + $actuals[$item->asXML()] = true; + } + } + + $message = $message ?: + "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"; + + foreach ($expectedMatches as $match) { + $this->assertTrue(isset($actuals[$match]), $message); + } + } + + /** + * Assert that the most recently queried page contains a number of content tags specified by a CSS selector. + * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag + * will be examined. The assertion fails if one of the expectedMatches fails to appear. + * + * Note:   characters are stripped from the content; make sure that your assertions take this into account. + * + * @param string $selector A basic CSS selector, e.g. 'li.jobs h3' + * @param array|string $expectedMatches The content of *all* matched tags as an array + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null) + { + $items = $this->cssParser()->getBySelector($selector); + + $actuals = []; + if ($items) { + /** @var SimpleXMLElement $item */ + foreach ($items as $item) { + $actuals[] = $item->asXML(); + } + } + + $message = $message ?: + "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" + . implode("'\n'", $expectedMatches) . "'\n\n" + . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"; + + $this->assertTrue($expectedMatches == $actuals, $message); + } + + /** + * Use the draft (stage) site for testing. + * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering + * your test. + * + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite + * @param bool $enabled toggle the use of the draft site + */ + public function useDraftSite($enabled = true) + { + Deprecation::notice('5.0', 'Use ?stage=Stage querystring arguments instead of useDraftSite'); + if ($enabled) { + $this->session()->set('readingMode', 'Stage.Stage'); + $this->session()->set('unsecuredDraftSite', true); + } else { + $this->session()->clear('readingMode'); + $this->session()->clear('unsecuredDraftSite'); + } + } + + /** + * @return bool + */ + public static function get_disable_themes() + { + return static::$disable_themes; + } + + /** + * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead + * @return bool + */ + public static function get_use_draft_site() + { + return static::$use_draft_site; + } +} diff --git a/src/Dev/_legacy/SapphireTest.php b/src/Dev/_legacy/SapphireTest.php new file mode 100644 index 00000000000..8a998205574 --- /dev/null +++ b/src/Dev/_legacy/SapphireTest.php @@ -0,0 +1,1401 @@ +expectException(PHPUnit_Framework_Error::class); + } + + public function expectErrorMessage(string $message): void + { + $this->expectExceptionMessage($message); + } + + public function expectErrorMessageMatches(string $regularExpression): void + { + $this->expectExceptionMessageMatches($regularExpression); + } + + public function expectWarning(): void + { + $this->expectException(PHPUnit_Framework_Error_Warning::class); + } + + public function expectWarningMessage(string $message): void + { + $this->expectExceptionMessage($message); + } + + public function expectWarningMessageMatches(string $regularExpression): void + { + $this->expectExceptionMessageMatches($regularExpression); + } + + public function expectNotice(): void + { + $this->expectException(PHPUnit_Framework_Error_Notice::class); + } + + public function expectNoticeMessage(string $message): void + { + $this->expectExceptionMessage($message); + } + + public function expectNoticeMessageMatches(string $regularExpression): void + { + $this->expectExceptionMessageMatches($regularExpression); + } + + public function expectDeprecation(): void + { + $this->expectException(PHPUnit_Framework_Error_Deprecation::class); + } + + public function expectDeprecationMessage(string $message): void + { + $this->expectExceptionMessage($message); + } + + public function expectDeprecationMessageMatches(string $regularExpression): void + { + $this->expectExceptionMessageMatches($regularExpression); + } + + public function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void + { + $this->assertRegExp($pattern, $string, $message); + } + + public function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void + { + $this->assertNotRegExp($pattern, $string, $message); + } + + public function assertFileDoesNotExist(string $filename, string $message = ''): void + { + $this->assertFileNotExists($filename, $message); + } + + // ===== + + /** + * Path to fixture data for this test run. + * If passed as an array, multiple fixture files will be loaded. + * Please note that you won't be able to refer with "=>" notation + * between the fixtures, they act independent of each other. + * + * @var string|array + */ + protected static $fixture_file = null; + + /** + * @deprecated 4.0..5.0 Use FixtureTestState instead + * @var FixtureFactory + */ + protected $fixtureFactory; + + /** + * @var Boolean If set to TRUE, this will force a test database to be generated + * in {@link setUp()}. Note that this flag is overruled by the presence of a + * {@link $fixture_file}, which always forces a database build. + * + * @var bool + */ + protected $usesDatabase = null; + + /** + * This test will cleanup its state via transactions. + * If set to false a full schema is forced between tests, but at a performance cost. + * + * @var bool + */ + protected $usesTransactions = true; + + /** + * @var bool + */ + protected static $is_running_test = false; + + /** + * By default, setUp() does not require default records. Pass + * class names in here, and the require/augment default records + * function will be called on them. + * + * @var array + */ + protected $requireDefaultRecordsFrom = []; + + /** + * A list of extensions that can't be applied during the execution of this run. If they are + * applied, they will be temporarily removed and a database migration called. + * + * The keys of the are the classes that the extensions can't be applied the extensions to, and + * the values are an array of illegal extensions on that class. + * + * Set a class to `*` to remove all extensions (unadvised) + * + * @var array + */ + protected static $illegal_extensions = []; + + /** + * A list of extensions that must be applied during the execution of this run. If they are + * not applied, they will be temporarily added and a database migration called. + * + * The keys of the are the classes to apply the extensions to, and the values are an array + * of required extensions on that class. + * + * Example: + * + * array("MyTreeDataObject" => array("Versioned", "Hierarchy")) + * + * + * @var array + */ + protected static $required_extensions = []; + + /** + * By default, the test database won't contain any DataObjects that have the interface TestOnly. + * This variable lets you define additional TestOnly DataObjects to set up for this test. + * Set it to an array of DataObject subclass names. + * + * @var array + */ + protected static $extra_dataobjects = []; + + /** + * List of class names of {@see Controller} objects to register routes for + * Controllers must implement Link() method + * + * @var array + */ + protected static $extra_controllers = []; + + /** + * We need to disabling backing up of globals to avoid overriding + * the few globals SilverStripe relies on, like $lang for the i18n subsystem. + * + * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html + */ + protected $backupGlobals = false; + + /** + * State management container for SapphireTest + * + * @var SapphireTestState + */ + protected static $state = null; + + /** + * Temp database helper + * + * @var TempDatabase + */ + protected static $tempDB = null; + + /** + * @return TempDatabase + */ + public static function tempDB() + { + if (!class_exists(TempDatabase::class)) { + return null; + } + + if (!static::$tempDB) { + static::$tempDB = TempDatabase::create(); + } + return static::$tempDB; + } + + /** + * Gets illegal extensions for this class + * + * @return array + */ + public static function getIllegalExtensions() + { + return static::$illegal_extensions; + } + + /** + * Gets required extensions for this class + * + * @return array + */ + public static function getRequiredExtensions() + { + return static::$required_extensions; + } + + /** + * Check if test bootstrapping has been performed. Must not be relied on + * outside of unit tests. + * + * @return bool + */ + protected static function is_running_test() + { + return self::$is_running_test; + } + + /** + * Set test running state + * + * @param bool $bool + */ + protected static function set_is_running_test($bool) + { + self::$is_running_test = $bool; + } + + /** + * @return String + */ + public static function get_fixture_file() + { + return static::$fixture_file; + } + + /** + * @return bool + */ + public function getUsesDatabase() + { + return $this->usesDatabase; + } + + /** + * @return bool + */ + public function getUsesTransactions() + { + return $this->usesTransactions; + } + + /** + * @return array + */ + public function getRequireDefaultRecordsFrom() + { + return $this->requireDefaultRecordsFrom; + } + + /** + * Setup the test. + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUp() before custom setup code + */ + protected function setUp(): void + { + if (!defined('FRAMEWORK_PATH')) { + trigger_error( + 'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', + E_USER_WARNING + ); + } + + // Call state helpers + static::$state->setUp($this); + + // We cannot run the tests on this abstract class. + if (static::class == __CLASS__) { + $this->markTestSkipped(sprintf('Skipping %s ', static::class)); + return; + } + + // i18n needs to be set to the defaults or tests fail + if (class_exists(i18n::class)) { + i18n::set_locale(i18n::config()->uninherited('default_locale')); + } + + // Set default timezone consistently to avoid NZ-specific dependencies + date_default_timezone_set('UTC'); + + if (class_exists(Member::class)) { + Member::set_password_validator(null); + } + + if (class_exists(Cookie::class)) { + Cookie::config()->update('report_errors', false); + } + + if (class_exists(RootURLController::class)) { + RootURLController::reset(); + } + + if (class_exists(Security::class)) { + Security::clear_database_is_ready(); + } + + // Set up test routes + $this->setUpRoutes(); + + $fixtureFiles = $this->getFixturePaths(); + + if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { + // Assign fixture factory to deprecated prop in case old tests use it over the getter + /** @var FixtureTestState $fixtureState */ + $fixtureState = static::$state->getStateByName('fixtures'); + $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class); + + $this->logInWithPermission('ADMIN'); + } + + // turn off template debugging + if (class_exists(SSViewer::class)) { + SSViewer::config()->update('source_file_comments', false); + } + + // Set up the test mailer + if (class_exists(TestMailer::class)) { + Injector::inst()->registerService(new TestMailer(), Mailer::class); + } + + if (class_exists(Email::class)) { + Email::config()->remove('send_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); + } + } + + + /** + * Helper method to determine if the current test should enable a test database + * + * @param $fixtureFiles + * @return bool + */ + protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) + { + $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; + + return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) + || $this->currentTestEnablesDatabase(); + } + + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase" + * + * @return bool + */ + protected function currentTestEnablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] !== 'false'; + } + + /** + * Helper method to check, if the current test uses the database. + * This can be switched on with the annotation "@useDatabase false" + * + * @return bool + */ + protected function currentTestDisablesDatabase() + { + $annotations = $this->getAnnotations(); + + return array_key_exists('useDatabase', $annotations['method']) + && $annotations['method']['useDatabase'][0] === 'false'; + } + + /** + * Called once per test case ({@link SapphireTest} subclass). + * This is different to {@link setUp()}, which gets called once + * per method. Useful to initialize expensive operations which + * don't change state for any called method inside the test, + * e.g. dynamically adding an extension. See {@link teardownAfterClass()} + * for tearing down the state again. + * + * Always sets up in order: + * - Reset php state + * - Nest + * - Custom state helpers + * + * User code should call parent::setUpBeforeClass() before custom setup code + * + * @throws Exception + */ + public static function setUpBeforeClass(): void + { + // Start tests + static::start(); + + if (!static::$state) { + throw new Exception('SapphireTest failed to bootstrap!'); + } + + // Call state helpers + static::$state->setUpOnce(static::class); + + // Build DB if we have objects + if (class_exists(DataObject::class) && static::getExtraDataObjects()) { + DataObject::reset(); + static::resetDBSchema(true, true); + } + } + + /** + * tearDown method that's called once per test class rather once per test method. + * + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDownAfterClass() after custom tear down code + */ + public static function tearDownAfterClass(): void + { + // Call state helpers + static::$state->tearDownOnce(static::class); + + // Reset DB schema + static::resetDBSchema(); + } + + /** + * @return FixtureFactory|false + * @deprecated 4.0.0:5.0.0 + */ + public function getFixtureFactory() + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class); + } + + /** + * Sets a new fixture factory + * @param FixtureFactory $factory + * @return $this + * @deprecated 4.0.0:5.0.0 + */ + public function setFixtureFactory(FixtureFactory $factory) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->setFixtureFactory($factory, static::class); + $this->fixtureFactory = $factory; + return $this; + } + + /** + * Get the ID of an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * @return int + */ + protected function idFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $id = $state->getFixtureFactory(static::class)->getId($className, $identifier); + + if (!$id) { + throw new \InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } + + return $id; + } + + /** + * Return all of the IDs in the fixture of a particular class name. + * Will collate all IDs form all fixtures if multiple fixtures are provided. + * + * @param string $className The data class or table name, as specified in your fixture file + * @return array A map of fixture-identifier => object-id + */ + protected function allFixtureIDs($className) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + return $state->getFixtureFactory(static::class)->getIds($className); + } + + /** + * Get an object from the fixture. + * + * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work + * @param string $identifier The identifier string, as provided in your fixture file + * + * @return DataObject + */ + protected function objFromFixture($className, $identifier) + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $obj = $state->getFixtureFactory(static::class)->get($className, $identifier); + + if (!$obj) { + throw new \InvalidArgumentException(sprintf( + "Couldn't find object '%s' (class: %s)", + $identifier, + $className + )); + } + + return $obj; + } + + /** + * Load a YAML fixture file into the database. + * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. + * Doesn't clear existing fixtures. + * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir + * @deprecated 4.0.0:5.0.0 + * + */ + public function loadFixture($fixtureFile) + { + Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); + $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); + $fixture->writeInto($this->getFixtureFactory()); + } + + /** + * Clear all fixtures which were previously loaded through + * {@link loadFixture()} + */ + public function clearFixtures() + { + /** @var FixtureTestState $state */ + $state = static::$state->getStateByName('fixtures'); + $state->getFixtureFactory(static::class)->clear(); + } + + /** + * Useful for writing unit tests without hardcoding folder structures. + * + * @return string Absolute path to current class. + */ + protected function getCurrentAbsolutePath() + { + $filename = ClassLoader::inst()->getItemPath(static::class); + if (!$filename) { + throw new LogicException('getItemPath returned null for ' . static::class + . '. Try adding flush=1 to the test run.'); + } + return dirname($filename); + } + + /** + * @return string File path relative to webroot + */ + protected function getCurrentRelativePath() + { + $base = Director::baseFolder(); + $path = $this->getCurrentAbsolutePath(); + if (substr($path, 0, strlen($base)) == $base) { + $path = preg_replace('/^\/*/', '', substr($path, strlen($base))); + } + return $path; + } + + /** + * Setup the test. + * Always sets up in order: + * - Custom state helpers + * - Unnest + * - Reset php state + * + * User code should call parent::tearDown() after custom tear down code + */ + protected function tearDown(): void + { + // Reset mocked datetime + if (class_exists(DBDatetime::class)) { + DBDatetime::clear_mock_now(); + } + + // Stop the redirection that might have been requested in the test. + // Note: Ideally a clean Controller should be created for each test. + // Now all tests executed in a batch share the same controller. + if (class_exists(Controller::class)) { + $controller = Controller::has_curr() ? Controller::curr() : null; + if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) { + $response->setStatusCode(200); + $response->removeHeader('Location'); + } + } + + // Call state helpers + static::$state->tearDown($this); + } + + public static function assertContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ) { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); + } + + public static function assertNotContains( + $needle, + $haystack, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true, + $checkForNonObjectIdentity = false + ) { + if ($haystack instanceof DBField) { + $haystack = (string)$haystack; + } + parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); + } + + /** + * Clear the log of emails sent + * + * @return bool True if emails cleared + */ + public function clearEmails() + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + $mailer->clearEmails(); + return true; + } + return false; + } + + /** + * Search for an email that was sent. + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', + * 'HtmlContent' + */ + public static function findEmail($to, $from = null, $subject = null, $content = null) + { + /** @var Mailer $mailer */ + $mailer = Injector::inst()->get(Mailer::class); + if ($mailer instanceof TestMailer) { + return $mailer->findEmail($to, $from, $subject, $content); + } + return null; + } + + /** + * Assert that the matching email was sent since the last call to clearEmails() + * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. + * + * @param string $to + * @param string $from + * @param string $subject + * @param string $content + */ + public static function assertEmailSent($to, $from = null, $subject = null, $content = null) + { + $found = (bool)static::findEmail($to, $from, $subject, $content); + + $infoParts = ''; + $withParts = []; + if ($to) { + $infoParts .= " to '$to'"; + } + if ($from) { + $infoParts .= " from '$from'"; + } + if ($subject) { + $withParts[] = "subject '$subject'"; + } + if ($content) { + $withParts[] = "content '$content'"; + } + if ($withParts) { + $infoParts .= ' with ' . implode(' and ', $withParts); + } + + static::assertTrue( + $found, + "Failed asserting that an email was sent$infoParts." + ); + } + + + /** + * Assert that the given {@link SS_List} includes DataObjects matching the given key-value + * pairs. Each match must correspond to 1 distinct record. + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members includes an entry with Email = sam@example.com: + * $this->assertListContains(['Email' => '...@example.com'], $members); + * + * Check that $members includes entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContains( + $matches + ), + $message + ); + } + + /** + * @param $matches + * @param $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListContains() instead + * + */ + public function assertDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListContains() instead'); + return static::assertListContains($matches, $dataObjectSet); + } + + /** + * Asserts that no items in a given list appear in the given dataobject list + * + * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param SS_List $list The {@link SS_List} to test. + * @param string $message + * + * Examples + * -------- + * Check that $members doesn't have an entry with Email = sam@example.com: + * $this->assertListNotContains(['Email' => '...@example.com'], $members); + * + * Check that $members doesn't have entries with Email = sam@example.com and with + * Email = ingo@example.com: + * $this->assertListNotContains([ + * ['Email' => '...@example.com'], + * ['Email' => 'i...@example.com'], + * ], $members); + */ + public static function assertListNotContains($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory( + 1, + 'array' + ); + } + + $constraint = new PHPUnit_Framework_Constraint_Not( + new SSListContains( + $matches + ) + ); + + static::assertThat( + $list, + $constraint, + $message + ); + } + + /** + * @param $matches + * @param $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead + * + */ + public static function assertNotDOSContains($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListNotContains() instead'); + return static::assertListNotContains($matches, $dataObjectSet); + } + + /** + * Assert that the given {@link SS_List} includes only DataObjects matching the given + * key-value pairs. Each match must correspond to 1 distinct record. + * + * Example + * -------- + * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't + * matter: + * $this->assertListEquals([ + * ['FirstName' =>'Sam', 'Surname' => 'Minnee'], + * ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], + * ], $members); + * + * @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can + * either pass a single pattern or an array of patterns. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListEquals($matches, SS_List $list, $message = '') + { + if (!is_array($matches)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContainsOnly( + $matches + ), + $message + ); + } + + /** + * @param $matches + * @param SS_List $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead + * + */ + public function assertDOSEquals($matches, $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListEquals() instead'); + return static::assertListEquals($matches, $dataObjectSet); + } + + + /** + * Assert that the every record in the given {@link SS_List} matches the given key-value + * pairs. + * + * Example + * -------- + * Check that every entry in $members has a Status of 'Active': + * $this->assertListAllMatch(['Status' => 'Active'], $members); + * + * @param mixed $match The pattern to match. The pattern is a map of key-value pairs. + * @param mixed $list The {@link SS_List} to test. + * @param string $message + */ + public static function assertListAllMatch($match, SS_List $list, $message = '') + { + if (!is_array($match)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory( + 1, + 'array' + ); + } + + static::assertThat( + $list, + new SSListContainsOnlyMatchingItems( + $match + ), + $message + ); + } + + /** + * @param $match + * @param SS_List $dataObjectSet + * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead + * + */ + public function assertDOSAllMatch($match, SS_List $dataObjectSet) + { + Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); + return static::assertListAllMatch($match, $dataObjectSet); + } + + /** + * Removes sequences of repeated whitespace characters from SQL queries + * making them suitable for string comparison + * + * @param string $sql + * @return string The cleaned and normalised SQL string + */ + protected static function normaliseSQL($sql) + { + return trim(preg_replace('/\s+/m', ' ', $sql)); + } + + /** + * Asserts that two SQL queries are equivalent + * + * @param string $expectedSQL + * @param string $actualSQL + * @param string $message + * @param float|int $delta + * @param integer $maxDepth + * @param boolean $canonicalize + * @param boolean $ignoreCase + */ + public static function assertSQLEquals( + $expectedSQL, + $actualSQL, + $message = '', + $delta = 0, + $maxDepth = 10, + $canonicalize = false, + $ignoreCase = false + ) { + // Normalise SQL queries to remove patterns of repeating whitespace + $expectedSQL = static::normaliseSQL($expectedSQL); + $actualSQL = static::normaliseSQL($actualSQL); + + static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); + } + + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + + static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } + + /** + * Asserts that a SQL query contains a SQL fragment + * + * @param string $needleSQL + * @param string $haystackSQL + * @param string $message + * @param boolean $ignoreCase + * @param boolean $checkForObjectIdentity + */ + public static function assertSQLNotContains( + $needleSQL, + $haystackSQL, + $message = '', + $ignoreCase = false, + $checkForObjectIdentity = true + ) { + $needleSQL = static::normaliseSQL($needleSQL); + $haystackSQL = static::normaliseSQL($haystackSQL); + + static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); + } + + /** + * Start test environment + */ + public static function start() + { + if (static::is_running_test()) { + return; + } + + // Health check + if (InjectorLoader::inst()->countManifests()) { + throw new LogicException('SapphireTest::start() cannot be called within another application'); + } + static::set_is_running_test(true); + + // Test application + $kernel = new TestKernel(BASE_PATH); + + if (class_exists(HTTPApplication::class)) { + // Mock request + $_SERVER['argv'] = ['vendor/bin/phpunit', '/']; + $request = CLIRequestBuilder::createFromEnvironment(); + + $app = new HTTPApplication($kernel); + $flush = array_key_exists('flush', $request->getVars()); + + // Custom application + $res = $app->execute($request, function (HTTPRequest $request) { + // Start session and execute + $request->getSession()->init($request); + + // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class + // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) + DataObject::reset(); + + // Set dummy controller; + $controller = Controller::create(); + $controller->setRequest($request); + $controller->pushCurrent(); + $controller->doInit(); + }, $flush); + + if ($res && $res->isError()) { + throw new LogicException($res->getBody()); + } + } else { + // Allow flush from the command line in the absence of HTTPApplication's special sauce + $flush = false; + foreach ($_SERVER['argv'] as $arg) { + if (preg_match('/^(--)?flush(=1)?$/', $arg)) { + $flush = true; + } + } + $kernel->boot($flush); + } + + // Register state + static::$state = SapphireTestState::singleton(); + // Register temp DB holder + static::tempDB(); + } + + /** + * Reset the testing database's schema, but only if it is active + * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included + * @param bool $forceCreate Force DB to be created if it doesn't exist + */ + public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false) + { + if (!static::$tempDB) { + return; + } + + // Check if DB is active before reset + if (!static::$tempDB->isUsed()) { + if (!$forceCreate) { + return; + } + static::$tempDB->build(); + } + $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; + static::$tempDB->resetDBSchema((array)$extraDataObjects); + } + + /** + * A wrapper for automatically performing callbacks as a user with a specific permission + * + * @param string|array $permCode + * @param callable $callback + * @return mixed + */ + public function actWithPermission($permCode, $callback) + { + return Member::actAs($this->createMemberWithPermission($permCode), $callback); + } + + /** + * Create Member and Group objects on demand with specific permission code + * + * @param string|array $permCode + * @return Member + */ + protected function createMemberWithPermission($permCode) + { + if (is_array($permCode)) { + $permArray = $permCode; + $permCode = implode('.', $permCode); + } else { + $permArray = [$permCode]; + } + + // Check cached member + if (isset($this->cache_generatedMembers[$permCode])) { + $member = $this->cache_generatedMembers[$permCode]; + } else { + // Generate group with these permissions + $group = Group::create(); + $group->Title = "$permCode group"; + $group->write(); + + // Create each individual permission + foreach ($permArray as $permArrayItem) { + $permission = Permission::create(); + $permission->Code = $permArrayItem; + $permission->write(); + $group->Permissions()->add($permission); + } + + $member = Member::get()->filter([ + 'Email' => "$permCode@example.org", + ])->first(); + if (!$member) { + $member = Member::create(); + } + + $member->FirstName = $permCode; + $member->Surname = 'User'; + $member->Email = "$permCode@example.org"; + $member->write(); + $group->Members()->add($member); + + $this->cache_generatedMembers[$permCode] = $member; + } + return $member; + } + + /** + * Create a member and group with the given permission code, and log in with it. + * Returns the member ID. + * + * @param string|array $permCode Either a permission, or list of permissions + * @return int Member ID + */ + public function logInWithPermission($permCode = 'ADMIN') + { + $member = $this->createMemberWithPermission($permCode); + $this->logInAs($member); + return $member->ID; + } + + /** + * Log in as the given member + * + * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in + */ + public function logInAs($member) + { + if (is_numeric($member)) { + $member = DataObject::get_by_id(Member::class, $member); + } elseif (!is_object($member)) { + $member = $this->objFromFixture(Member::class, $member); + } + Injector::inst()->get(IdentityStore::class)->logIn($member); + } + + /** + * Log out the current user + */ + public function logOut() + { + /** @var IdentityStore $store */ + $store = Injector::inst()->get(IdentityStore::class); + $store->logOut(); + } + + /** + * Cache for logInWithPermission() + */ + protected $cache_generatedMembers = []; + + /** + * Test against a theme. + * + * @param string $themeBaseDir themes directory + * @param string $theme Theme name + * @param callable $callback + * @throws Exception + */ + protected function useTestTheme($themeBaseDir, $theme, $callback) + { + Config::nest(); + if (strpos($themeBaseDir, BASE_PATH) === 0) { + $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); + } + SSViewer::config()->update('theme_enabled', true); + SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']); + + try { + $callback(); + } finally { + Config::unnest(); + } + } + + /** + * Get fixture paths for this test + * + * @return array List of paths + */ + protected function getFixturePaths() + { + $fixtureFile = static::get_fixture_file(); + if (empty($fixtureFile)) { + return []; + } + + $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; + + return array_map(function ($fixtureFilePath) { + return $this->resolveFixturePath($fixtureFilePath); + }, $fixtureFiles); + } + + /** + * Return all extra objects to scaffold for this test + * @return array + */ + public static function getExtraDataObjects() + { + return static::$extra_dataobjects; + } + + /** + * Get additional controller classes to register routes for + * + * @return array + */ + public static function getExtraControllers() + { + return static::$extra_controllers; + } + + /** + * Map a fixture path to a physical file + * + * @param string $fixtureFilePath + * @return string + */ + protected function resolveFixturePath($fixtureFilePath) + { + // support loading via composer name path. + if (strpos($fixtureFilePath, ':') !== false) { + return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); + } + + // Support fixture paths relative to the test class, rather than relative to webroot + // String checking is faster than file_exists() calls. + $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + // Check if file exists relative to base dir + $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); + if ($resolvedPath) { + return $resolvedPath; + } + + return $fixtureFilePath; + } + + protected function setUpRoutes() + { + if (!class_exists(Director::class)) { + return; + } + + // Get overridden routes + $rules = $this->getExtraRoutes(); + + // Add all other routes + foreach (Director::config()->uninherited('rules') as $route => $rule) { + if (!isset($rules[$route])) { + $rules[$route] = $rule; + } + } + + // Add default catch-all rule + $rules['$Controller//$Action/$ID/$OtherID'] = '*'; + + // Add controller-name auto-routing + Director::config()->set('rules', $rules); + } + + /** + * Get extra routes to merge into Director.rules + * + * @return array + */ + protected function getExtraRoutes() + { + $rules = []; + foreach ($this->getExtraControllers() as $class) { + $controllerInst = Controller::singleton($class); + $link = Director::makeRelative($controllerInst->Link()); + $route = rtrim($link, '/') . '//$Action/$ID/$OtherID'; + $rules[$route] = $class; + } + return $rules; + } +} diff --git a/tests/php/Control/ControllerTest.php b/tests/php/Control/ControllerTest.php index 4a7efb2855d..f35e34b90bf 100644 --- a/tests/php/Control/ControllerTest.php +++ b/tests/php/Control/ControllerTest.php @@ -42,7 +42,7 @@ class ControllerTest extends FunctionalTest UnsecuredController::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); Director::config()->update('alternate_base_url', '/'); @@ -264,12 +264,10 @@ public function testAllowedActions() }); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid allowed_action '*' - */ public function testWildcardAllowedActions() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid allowed_action '*'"); $this->get('AccessWildcardSecuredController'); } diff --git a/tests/php/Control/CookieTest.php b/tests/php/Control/CookieTest.php index d08bb975461..8f2a8eaaa2b 100644 --- a/tests/php/Control/CookieTest.php +++ b/tests/php/Control/CookieTest.php @@ -10,7 +10,7 @@ class CookieTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); Injector::inst()->registerService(new CookieJar($_COOKIE), 'SilverStripe\\Control\\Cookie_Backend'); diff --git a/tests/php/Control/DirectorTest.php b/tests/php/Control/DirectorTest.php index 115b24c9a95..e6fdc26c872 100644 --- a/tests/php/Control/DirectorTest.php +++ b/tests/php/Control/DirectorTest.php @@ -29,7 +29,7 @@ class DirectorTest extends SapphireTest private $originalEnvType; - protected function setUp() + protected function setUp(): void { parent::setUp(); Director::config()->set('alternate_base_url', 'http://www.mysite.com:9090/'); @@ -44,10 +44,10 @@ protected function setUp() $this->expectedRedirect = null; } - protected function tearDown(...$args) + protected function tearDown(): void { Environment::setEnv('SS_ENVIRONMENT_TYPE', $this->originalEnvType); - parent::tearDown(...$args); + parent::tearDown(); } protected function getExtraRoutes() diff --git a/tests/php/Control/Email/SwiftPluginTest.php b/tests/php/Control/Email/SwiftPluginTest.php index da809552da8..25adb7bbea7 100644 --- a/tests/php/Control/Email/SwiftPluginTest.php +++ b/tests/php/Control/Email/SwiftPluginTest.php @@ -9,7 +9,7 @@ class SwiftPluginTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Control/HTTPCacheControlIntegrationTest.php b/tests/php/Control/HTTPCacheControlIntegrationTest.php index 94b0f152793..40d8fffcabb 100644 --- a/tests/php/Control/HTTPCacheControlIntegrationTest.php +++ b/tests/php/Control/HTTPCacheControlIntegrationTest.php @@ -16,7 +16,7 @@ class HTTPCacheControlIntegrationTest extends FunctionalTest RuleController::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); HTTPCacheControlMiddleware::config() diff --git a/tests/php/Control/HTTPRequestTest.php b/tests/php/Control/HTTPRequestTest.php index 52ce61b4923..d9778f8a4d3 100644 --- a/tests/php/Control/HTTPRequestTest.php +++ b/tests/php/Control/HTTPRequestTest.php @@ -52,11 +52,10 @@ public function testWildCardMatch() /** * This test just asserts a warning is given if there is more than one wildcard parameter. Note that this isn't an * enforcement of an API and we an add new behaviour in the future to allow many wildcard params if we want to - * - * @expectedException \PHPUnit_Framework_Error_Warning */ public function testWildCardWithFurtherParams() { + $this->expectWarning(); $request = new HTTPRequest('GET', 'admin/crm/test'); // all parameters after the first wildcard parameter are ignored $request->match('admin/$Action/$@/$Other/$*', true); diff --git a/tests/php/Control/HTTPTest.php b/tests/php/Control/HTTPTest.php index 018c59ce1a8..d52ede388ac 100644 --- a/tests/php/Control/HTTPTest.php +++ b/tests/php/Control/HTTPTest.php @@ -19,7 +19,7 @@ */ class HTTPTest extends FunctionalTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); // Set to disabled at null forcing level @@ -167,12 +167,10 @@ public function testDeprecatedCacheControlHandlingOnMaxAge() $this->assertContains('max-age=99', $header); } - /** - * @expectedException \LogicException - * @expectedExceptionMessageRegExp /Found unsupported legacy directives in HTTP\.cache_control: unknown/ - */ public function testDeprecatedCacheControlHandlingThrowsWithUnknownDirectives() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessageMatches('/Found unsupported legacy directives in HTTP\.cache_control: unknown/'); /** @var Config */ Config::modify()->set( HTTP::class, diff --git a/tests/php/Control/IPUtilsTest.php b/tests/php/Control/IPUtilsTest.php index bc82835d1d4..d872ce42118 100644 --- a/tests/php/Control/IPUtilsTest.php +++ b/tests/php/Control/IPUtilsTest.php @@ -17,14 +17,14 @@ class IPUtilsTest extends SapphireTest { /** - * @dataProvider testIPv4Provider + * @dataProvider iPv4Provider */ public function testIPv4($matches, $remoteAddr, $cidr) { $this->assertSame($matches, IPUtils::checkIP($remoteAddr, $cidr)); } - public function testIPv4Provider() + public function iPv4Provider() { return [ [true, '192.168.1.1', '192.168.1.1'], @@ -43,7 +43,7 @@ public function testIPv4Provider() } /** - * @dataProvider testIPv6Provider + * @dataProvider iPv6Provider */ public function testIPv6($matches, $remoteAddr, $cidr) { @@ -54,7 +54,7 @@ public function testIPv6($matches, $remoteAddr, $cidr) $this->assertSame($matches, IPUtils::checkIP($remoteAddr, $cidr)); } - public function testIPv6Provider() + public function iPv6Provider() { return [ [true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], @@ -71,11 +71,13 @@ public function testIPv6Provider() } /** - * @expectedException \RuntimeException * @requires extension sockets */ public function testAnIPv6WithOptionDisabledIPv6() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage(''); + $this->expectExceptionMessageMatches(''); if (defined('AF_INET6')) { $this->markTestSkipped('Only works when PHP is compiled with the option "disable-ipv6".'); } diff --git a/tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php b/tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php index e4e3caa56f8..4d9774e67c7 100644 --- a/tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php +++ b/tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php @@ -16,7 +16,7 @@ class CanonicalURLMiddlewareTest extends SapphireTest */ protected $middleware; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php index 1a8b3e4b24b..5f6cf7cd4be 100644 --- a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php +++ b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php @@ -8,7 +8,7 @@ class HTTPCacheControlMiddlewareTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); // Set to disabled at null forcing level diff --git a/tests/php/Control/Middleware/RateLimitMiddlewareTest.php b/tests/php/Control/Middleware/RateLimitMiddlewareTest.php index 23da5723914..23bda6331b7 100644 --- a/tests/php/Control/Middleware/RateLimitMiddlewareTest.php +++ b/tests/php/Control/Middleware/RateLimitMiddlewareTest.php @@ -17,7 +17,7 @@ class RateLimitMiddlewareTest extends FunctionalTest TestController::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); DBDatetime::set_mock_now('2017-09-27 00:00:00'); diff --git a/tests/php/Control/RSS/RSSFeedTest.php b/tests/php/Control/RSS/RSSFeedTest.php index 9e41abca07a..496b6ce7da1 100644 --- a/tests/php/Control/RSS/RSSFeedTest.php +++ b/tests/php/Control/RSS/RSSFeedTest.php @@ -99,7 +99,7 @@ public function testRenderWithTemplate() $this->assertNotContains('Test Custom Template', $content); } - protected function setUp() + protected function setUp(): void { parent::setUp(); Config::modify()->set(Director::class, 'alternate_base_url', '/'); @@ -116,7 +116,7 @@ function () { ); } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); $_SERVER['HTTP_HOST'] = self::$original_host; diff --git a/tests/php/Control/SessionTest.php b/tests/php/Control/SessionTest.php index 79c74d682f9..a1024e703c6 100644 --- a/tests/php/Control/SessionTest.php +++ b/tests/php/Control/SessionTest.php @@ -18,10 +18,10 @@ class SessionTest extends SapphireTest */ protected $session = null; - protected function setUp() + protected function setUp(): void { $this->session = new Session([]); - return parent::setUp(); + parent::setUp(); } /** @@ -107,11 +107,11 @@ public function testStartUsesSecureCookieNameWithHttpsAndCookieSecureOn() /** * @runInSeparateProcess * @preserveGlobalState disabled - * @expectedException BadMethodCallException - * @expectedExceptionMessage Session has already started */ public function testStartErrorsWhenStartingTwice() { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Session has already started'); $req = new HTTPRequest('GET', '/'); $session = new Session(null); // unstarted session $session->start($req); diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest.php b/tests/php/Control/SimpleResourceURLGeneratorTest.php index bfe1a768fe4..93d92dd3a39 100644 --- a/tests/php/Control/SimpleResourceURLGeneratorTest.php +++ b/tests/php/Control/SimpleResourceURLGeneratorTest.php @@ -11,7 +11,7 @@ class SimpleResourceURLGeneratorTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); Director::config()->set( diff --git a/tests/php/Core/Cache/CacheTest.php b/tests/php/Core/Cache/CacheTest.php index d034ed977b8..1853acf881f 100644 --- a/tests/php/Core/Cache/CacheTest.php +++ b/tests/php/Core/Cache/CacheTest.php @@ -13,7 +13,7 @@ class CacheTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Core/Cache/RateLimiterTest.php b/tests/php/Core/Cache/RateLimiterTest.php index ec2a80527a9..011020067b7 100644 --- a/tests/php/Core/Cache/RateLimiterTest.php +++ b/tests/php/Core/Cache/RateLimiterTest.php @@ -11,7 +11,7 @@ class RateLimiterTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); DBDatetime::set_mock_now('2017-09-27 00:00:00'); diff --git a/tests/php/Core/ClassInfoTest.php b/tests/php/Core/ClassInfoTest.php index 29721599bd9..840e658894a 100644 --- a/tests/php/Core/ClassInfoTest.php +++ b/tests/php/Core/ClassInfoTest.php @@ -3,6 +3,7 @@ namespace SilverStripe\Core\Tests; use DateTime; +use Exception; use ReflectionException; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Tests\ClassInfoTest\BaseClass; @@ -42,7 +43,7 @@ class ClassInfoTest extends SapphireTest ExtendTest3::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); ClassInfo::reset_db_cache(); @@ -106,8 +107,8 @@ public function testClassName() public function testNonClassName() { - $this->expectException(ReflectionException::class); - $this->expectExceptionMessageRegExp('/Class "?IAmAClassThatDoesNotExist"? does not exist/'); + $this->expectException(Exception::class); + $this->expectExceptionMessageMatches('/Class "?IAmAClassThatDoesNotExist"? does not exist/'); $this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist')); } diff --git a/tests/php/Core/ConvertTest.php b/tests/php/Core/ConvertTest.php index b3dec037ab4..6d7d1608638 100644 --- a/tests/php/Core/ConvertTest.php +++ b/tests/php/Core/ConvertTest.php @@ -19,14 +19,14 @@ class ConvertTest extends SapphireTest private $previousLocaleSetting = null; - public function setUp() + protected function setUp(): void { parent::setUp(); // clear the previous locale setting $this->previousLocaleSetting = null; } - public function tearDown() + protected function tearDown(): void { parent::tearDown(); // If a test sets the locale, reset it on teardown diff --git a/tests/php/Core/CoreTest.php b/tests/php/Core/CoreTest.php index 9e82804f5b7..f2a546e55b1 100644 --- a/tests/php/Core/CoreTest.php +++ b/tests/php/Core/CoreTest.php @@ -15,7 +15,7 @@ class CoreTest extends SapphireTest protected $tempPath; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->tempPath = Director::baseFolder() . DIRECTORY_SEPARATOR . 'silverstripe-cache'; @@ -51,7 +51,7 @@ public function testGetTempPathInProject() } } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); $user = TempFolder::getTempFolderUsername(); diff --git a/tests/php/Core/Injector/InjectorTest.php b/tests/php/Core/Injector/InjectorTest.php index ac04d988e84..ee3a103bb22 100644 --- a/tests/php/Core/Injector/InjectorTest.php +++ b/tests/php/Core/Injector/InjectorTest.php @@ -2,7 +2,6 @@ namespace SilverStripe\Core\Tests\Injector; -use InvalidArgumentException; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Factory; use SilverStripe\Core\Injector\Injector; @@ -44,14 +43,14 @@ class InjectorTest extends SapphireTest protected $nestingLevel = 0; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->nestingLevel = 0; } - protected function tearDown() + protected function tearDown(): void { while ($this->nestingLevel > 0) { @@ -910,11 +909,9 @@ public function testMethods() ); } - /** - * @expectedException InvalidArgumentException - */ public function testNonExistentMethods() { + $this->expectException(\InvalidArgumentException::class); $injector = new Injector(); $config = [ 'TestService' => [ @@ -929,11 +926,9 @@ public function testNonExistentMethods() $item = $injector->get('TestService'); } - /** - * @expectedException InvalidArgumentException - */ public function testProtectedMethods() { + $this->expectException(\InvalidArgumentException::class); $injector = new Injector(); $config = [ 'TestService' => [ @@ -948,11 +943,9 @@ public function testProtectedMethods() $item = $injector->get('TestService'); } - /** - * @expectedException InvalidArgumentException - */ public function testTooManyArrayValues() { + $this->expectException(\InvalidArgumentException::class); $injector = new Injector(); $config = [ 'TestService' => [ @@ -967,11 +960,9 @@ public function testTooManyArrayValues() $item = $injector->get('TestService'); } - /** - * @expectedException \SilverStripe\Core\Injector\InjectorNotFoundException - */ public function testGetThrowsOnNotFound() { + $this->expectException(InjectorNotFoundException::class); $injector = new Injector(); $injector->get('UnknownService'); } diff --git a/tests/php/Core/Manifest/ClassLoaderTest.php b/tests/php/Core/Manifest/ClassLoaderTest.php index 1525fdc3bbe..d0e3d9f8528 100644 --- a/tests/php/Core/Manifest/ClassLoaderTest.php +++ b/tests/php/Core/Manifest/ClassLoaderTest.php @@ -32,7 +32,7 @@ class ClassLoaderTest extends SapphireTest */ protected $testManifest2; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Core/Manifest/ClassManifestErrorHandlerTest.php b/tests/php/Core/Manifest/ClassManifestErrorHandlerTest.php index 0a9ec22a5cf..075c35d9357 100644 --- a/tests/php/Core/Manifest/ClassManifestErrorHandlerTest.php +++ b/tests/php/Core/Manifest/ClassManifestErrorHandlerTest.php @@ -7,12 +7,10 @@ class ClassManifestErrorHandlerTest extends SapphireTest { - /** - * @expectedException \PhpParser\Error - * @expectedExceptionMessage my error in /my/path - */ public function testIncludesPathname() { + $this->expectException(Error::class); + $this->expectExceptionMessage('my error in /my/path'); $h = new ClassManifestErrorHandler('/my/path'); $e = new Error('my error'); $h->handleError($e); diff --git a/tests/php/Core/Manifest/ClassManifestTest.php b/tests/php/Core/Manifest/ClassManifestTest.php index 40955062f4a..736f767321a 100644 --- a/tests/php/Core/Manifest/ClassManifestTest.php +++ b/tests/php/Core/Manifest/ClassManifestTest.php @@ -27,7 +27,7 @@ class ClassManifestTest extends SapphireTest */ protected $manifestTests; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Core/Manifest/ConfigManifestTest.php b/tests/php/Core/Manifest/ConfigManifestTest.php index fd2c5520798..1aaa76f1415 100644 --- a/tests/php/Core/Manifest/ConfigManifestTest.php +++ b/tests/php/Core/Manifest/ConfigManifestTest.php @@ -13,7 +13,7 @@ class ConfigManifestTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -22,7 +22,7 @@ protected function setUp() ModuleLoader::inst()->pushManifest($moduleManifest); } - protected function tearDown() + protected function tearDown(): void { ModuleLoader::inst()->popManifest(); parent::tearDown(); @@ -225,7 +225,7 @@ public function testMultipleRules() 'Fragment is included if both blocks succeed.' ); } - + public function testExtensionLoaded() { $config = $this->getConfigFixtureValue('ExtensionLoaded'); diff --git a/tests/php/Core/Manifest/ManifestFileFinderTest.php b/tests/php/Core/Manifest/ManifestFileFinderTest.php index 1eb87bf3bcc..8efd3a1de00 100644 --- a/tests/php/Core/Manifest/ManifestFileFinderTest.php +++ b/tests/php/Core/Manifest/ManifestFileFinderTest.php @@ -41,7 +41,7 @@ public function assertFinderFinds(ManifestFileFinder $finder, $base, $expect, $m sort($expect); sort($found); - $this->assertEquals($expect, $found, $message); + $this->assertEquals($expect, $found, $message ?: ''); } public function testBasicOperation() diff --git a/tests/php/Core/Manifest/ModuleManifestTest.php b/tests/php/Core/Manifest/ModuleManifestTest.php index e77adbf997b..0106aa5c415 100644 --- a/tests/php/Core/Manifest/ModuleManifestTest.php +++ b/tests/php/Core/Manifest/ModuleManifestTest.php @@ -18,7 +18,7 @@ class ModuleManifestTest extends SapphireTest */ protected $manifest; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Core/Manifest/ModuleResourceTest.php b/tests/php/Core/Manifest/ModuleResourceTest.php index d0d629fe860..c733b636c0f 100644 --- a/tests/php/Core/Manifest/ModuleResourceTest.php +++ b/tests/php/Core/Manifest/ModuleResourceTest.php @@ -18,7 +18,7 @@ class ModuleResourceTest extends SapphireTest */ protected $manifest; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Core/Manifest/NamespacedClassManifestTest.php b/tests/php/Core/Manifest/NamespacedClassManifestTest.php index fa80e9c63b0..29ddb7c0824 100644 --- a/tests/php/Core/Manifest/NamespacedClassManifestTest.php +++ b/tests/php/Core/Manifest/NamespacedClassManifestTest.php @@ -25,7 +25,7 @@ class NamespacedClassManifestTest extends SapphireTest */ protected $manifest; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -35,7 +35,7 @@ protected function setUp() ClassLoader::inst()->pushManifest($this->manifest, false); } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); ClassLoader::inst()->popManifest(); @@ -43,8 +43,9 @@ protected function tearDown() public function testClassInfoIsCorrect() { + $class = 'SilverStripe\\Framework\\Tests\\ClassI'; $this->assertContains( - 'SilverStripe\\Framework\\Tests\\ClassI', + $class, ClassInfo::implementorsOf(PermissionProvider::class) ); @@ -54,7 +55,12 @@ public function testClassInfoIsCorrect() $method = new ReflectionMethod($this->manifest, 'coalesceDescendants'); $method->setAccessible(true); $method->invoke($this->manifest, ModelAdmin::class); - $this->assertContains('SilverStripe\\Framework\\Tests\\ClassI', ClassInfo::subclassesFor(ModelAdmin::class)); + $classes = ClassInfo::subclassesFor(ModelAdmin::class); + $this->assertContains( + $class, + $classes, + $class . ' not contained in [' . implode(',', $classes) . ']' + ); } public function testGetItemPath() diff --git a/tests/php/Core/Manifest/PrioritySorterTest.php b/tests/php/Core/Manifest/PrioritySorterTest.php index 8c1f2c345e4..8166fd3b8fa 100644 --- a/tests/php/Core/Manifest/PrioritySorterTest.php +++ b/tests/php/Core/Manifest/PrioritySorterTest.php @@ -12,7 +12,7 @@ class PrioritySorterTest extends SapphireTest */ protected $sorter; - public function setUp() + protected function setUp(): void { parent::setUp(); $modules = [ diff --git a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php index 043bdd5c337..206cd3fd892 100644 --- a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php +++ b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php @@ -33,7 +33,7 @@ class ThemeResourceLoaderTest extends SapphireTest /** * Set up manifest before each test */ - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -60,7 +60,7 @@ protected function setUp() ThemeResourceLoader::flush(); } - protected function tearDown() + protected function tearDown(): void { ModuleLoader::inst()->popManifest(); parent::tearDown(); diff --git a/tests/php/Core/MemoryLimitTest.php b/tests/php/Core/MemoryLimitTest.php index 842e77645f5..562e349ce16 100644 --- a/tests/php/Core/MemoryLimitTest.php +++ b/tests/php/Core/MemoryLimitTest.php @@ -12,7 +12,7 @@ class MemoryLimitTest extends SapphireTest protected $origMemLimit; protected $origTimeLimit; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -29,7 +29,7 @@ protected function setUp() } } - protected function tearDown() + protected function tearDown(): void { if (!in_array('suhosin', get_loaded_extensions())) { ini_set('memory_limit', $this->origMemLimit); diff --git a/tests/php/Core/ObjectTest.php b/tests/php/Core/ObjectTest.php index ab4ceae4ffd..cba0b11973a 100644 --- a/tests/php/Core/ObjectTest.php +++ b/tests/php/Core/ObjectTest.php @@ -31,7 +31,7 @@ class ObjectTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); Injector::inst()->unregisterObjects([ diff --git a/tests/php/Core/PhpSyntaxTest.php b/tests/php/Core/PhpSyntaxTest.php index 465b2d5ae9b..10d51fd3f6b 100644 --- a/tests/php/Core/PhpSyntaxTest.php +++ b/tests/php/Core/PhpSyntaxTest.php @@ -9,7 +9,7 @@ */ class PhpSyntaxTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->markTestSkipped('This needs to be written to include only core php files, not test/thirdparty files'); diff --git a/tests/php/Dev/BulkLoaderResultTest.php b/tests/php/Dev/BulkLoaderResultTest.php index be0aa2f8a5b..c619eed72bc 100644 --- a/tests/php/Dev/BulkLoaderResultTest.php +++ b/tests/php/Dev/BulkLoaderResultTest.php @@ -13,7 +13,7 @@ class BulkLoaderResultTest extends SapphireTest Player::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); Player::create(['Name' => 'Vincent', 'Status' => 'Available'])->write(); diff --git a/tests/php/Dev/CLIDebugViewTest.php b/tests/php/Dev/CLIDebugViewTest.php index 7d8d06661e0..20241dcafc0 100644 --- a/tests/php/Dev/CLIDebugViewTest.php +++ b/tests/php/Dev/CLIDebugViewTest.php @@ -10,7 +10,7 @@ class CLIDebugViewTest extends SapphireTest { protected $caller = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Dev/CSVParserTest.php b/tests/php/Dev/CSVParserTest.php index 0b2a72687b3..4b2cb62a3b9 100644 --- a/tests/php/Dev/CSVParserTest.php +++ b/tests/php/Dev/CSVParserTest.php @@ -15,7 +15,7 @@ class CSVParserTest extends SapphireTest */ protected $csvPath = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->csvPath = __DIR__ . '/CsvBulkLoaderTest/csv/'; diff --git a/tests/php/Dev/CsvBulkLoaderTest.php b/tests/php/Dev/CsvBulkLoaderTest.php index c7740fd22c8..89b0737551d 100644 --- a/tests/php/Dev/CsvBulkLoaderTest.php +++ b/tests/php/Dev/CsvBulkLoaderTest.php @@ -31,7 +31,7 @@ class CsvBulkLoaderTest extends SapphireTest */ protected $csvPath = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->csvPath = __DIR__ . '/CsvBulkLoaderTest/csv/'; diff --git a/tests/php/Dev/DebugViewTest.php b/tests/php/Dev/DebugViewTest.php index edafe8bffb2..b0158646a6a 100644 --- a/tests/php/Dev/DebugViewTest.php +++ b/tests/php/Dev/DebugViewTest.php @@ -10,7 +10,7 @@ class DebugViewTest extends SapphireTest { protected $caller = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Dev/DeprecationTest.php b/tests/php/Dev/DeprecationTest.php index 194d08dafa0..a3a4f0e4ce8 100644 --- a/tests/php/Dev/DeprecationTest.php +++ b/tests/php/Dev/DeprecationTest.php @@ -12,7 +12,7 @@ class DeprecationTest extends SapphireTest static $originalVersionInfo; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -21,7 +21,7 @@ protected function setUp() Deprecation::set_enabled(true); } - protected function tearDown() + protected function tearDown(): void { Deprecation::restore_settings(self::$originalVersionInfo); parent::tearDown(); @@ -33,11 +33,9 @@ public function testLesserVersionTriggersNoNotice() $this->assertNull(Deprecation::notice('2.0', 'Deprecation test failed')); } - /** - * @expectedException PHPUnit_Framework_Error - */ public function testEqualVersionTriggersNotice() { + $this->expectError(); Deprecation::notification_version('2.0.0'); Deprecation::notice('2.0.0', 'Deprecation test passed'); } @@ -49,11 +47,9 @@ public function testBetaVersionDoesntTriggerNoticeWhenDeprecationDoesntSpecifyRe $this->assertNull(Deprecation::notice('2.0.0', 'Deprecation test failed')); } - /** - * @expectedException PHPUnit_Framework_Error - */ public function testGreaterVersionTriggersNotice() { + $this->expectError(); Deprecation::notification_version('3.0.0'); Deprecation::notice('2.0', 'Deprecation test passed'); } @@ -65,11 +61,9 @@ public function testNonMatchingModuleNotifcationVersionDoesntAffectNotice() $this->callThatOriginatesFromFramework(); } - /** - * @expectedException PHPUnit_Framework_Error - */ public function testMatchingModuleNotifcationVersionAffectsNotice() { + $this->expectError(); Deprecation::notification_version('1.0.0'); Deprecation::notification_version('3.0.0', 'silverstripe/framework'); $this->callThatOriginatesFromFramework(); @@ -83,32 +77,26 @@ public function testMethodNameCalculation() ); } - /** - * @expectedException PHPUnit_Framework_Error - * @expectedExceptionMessage DeprecationTest->testScopeMethod is deprecated. Method scope - */ public function testScopeMethod() { + $this->expectError(); + $this->expectErrorMessage('DeprecationTest->testScopeMethod is deprecated. Method scope'); Deprecation::notification_version('2.0.0'); Deprecation::notice('2.0.0', 'Method scope', Deprecation::SCOPE_METHOD); } - /** - * @expectedException PHPUnit_Framework_Error - * @expectedExceptionMessage DeprecationTest is deprecated. Class scope - */ public function testScopeClass() { + $this->expectError(); + $this->expectErrorMessage('DeprecationTest is deprecated. Class scope'); Deprecation::notification_version('2.0.0'); Deprecation::notice('2.0.0', 'Class scope', Deprecation::SCOPE_CLASS); } - /** - * @expectedException PHPUnit_Framework_Error - * @expectedExceptionMessage Global scope - */ public function testScopeGlobal() { + $this->expectError(); + $this->expectErrorMessage('Global scope'); Deprecation::notification_version('2.0.0'); Deprecation::notice('2.0.0', 'Global scope', Deprecation::SCOPE_GLOBAL); } diff --git a/tests/php/Dev/DevAdminControllerTest.php b/tests/php/Dev/DevAdminControllerTest.php index 9f12b5a33a4..353d4292430 100644 --- a/tests/php/Dev/DevAdminControllerTest.php +++ b/tests/php/Dev/DevAdminControllerTest.php @@ -14,7 +14,7 @@ class DevAdminControllerTest extends FunctionalTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Dev/FixtureBlueprintTest.php b/tests/php/Dev/FixtureBlueprintTest.php index b51946d96dc..ec950ac0ff7 100644 --- a/tests/php/Dev/FixtureBlueprintTest.php +++ b/tests/php/Dev/FixtureBlueprintTest.php @@ -144,12 +144,10 @@ public function testCreateWithRelationship() $this->assertNotNull($obj2->HasManyRelation()->find('ID', $relation2->ID)); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage No fixture definitions found - */ public function testCreateWithInvalidRelationName() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No fixture definitions found'); $blueprint = new FixtureBlueprint(TestDataObject::class); $obj = $blueprint->createObject( @@ -165,12 +163,10 @@ public function testCreateWithInvalidRelationName() ); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage No fixture definitions found - */ public function testCreateWithInvalidRelationIdentifier() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No fixture definitions found'); $blueprint = new FixtureBlueprint(TestDataObject::class); $obj = $blueprint->createObject( @@ -186,12 +182,10 @@ public function testCreateWithInvalidRelationIdentifier() ); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Invalid format - */ public function testCreateWithInvalidRelationFormat() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid format'); $factory = new FixtureFactory(); $blueprint = new FixtureBlueprint(TestDataObject::class); diff --git a/tests/php/Dev/SSListExporterTest.php b/tests/php/Dev/SSListExporterTest.php index d4df477edf7..c236f209cd3 100644 --- a/tests/php/Dev/SSListExporterTest.php +++ b/tests/php/Dev/SSListExporterTest.php @@ -18,7 +18,7 @@ class SSListExporterTest extends SapphireTest */ private $exporter; - public function setUp() + protected function setUp(): void { parent::setUp(); $this->exporter = new SSListExporter(); diff --git a/tests/php/Dev/SapphireTestTest.php b/tests/php/Dev/SapphireTestTest.php index a29e7582131..3b571c3c1ae 100644 --- a/tests/php/Dev/SapphireTestTest.php +++ b/tests/php/Dev/SapphireTestTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Dev\Tests; +use PHPUnit\Framework\ExpectationFailedException; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\ArrayList; use SilverStripe\Security\Member; @@ -122,11 +123,10 @@ private function generateArrayListFromItems($itemsForList) * @param $itemsForList * * @testdox assertion assertListAllMatch fails when not all items are matching - * - * @expectedException \PHPUnit_Framework_ExpectationFailedException */ public function testAssertListAllMatchFailsWhenNotMatchingAllItems($match, $itemsForList) { + $this->expectException(ExpectationFailedException::class); $list = $this->generateArrayListFromItems($itemsForList); $this->assertListAllMatch($match, $list); @@ -156,11 +156,10 @@ public function testAssertListContains($matches, $itemsForList) * * @param $matches * @param $itemsForList array - * - * @expectedException \PHPUnit_Framework_ExpectationFailedException */ public function testAssertListContainsFailsIfListDoesNotContainMatch($matches, $itemsForList) { + $this->expectException(ExpectationFailedException::class); $list = $this->generateArrayListFromItems($itemsForList); $list->push(Member::create(['FirstName' => 'Foo', 'Surname' => 'Foo'])); $list->push(Member::create(['FirstName' => 'Bar', 'Surname' => 'Bar'])); @@ -191,11 +190,10 @@ public function testAssertListNotContains($matches, $itemsForList) * @param $itemsForList * * @testdox assertion assertListNotContains throws a exception when a matching item is found in the list - * - * @expectedException \PHPUnit_Framework_ExpectationFailedException */ public function testAssertListNotContainsFailsWhenListContainsAMatch($matches, $itemsForList) { + $this->expectException(ExpectationFailedException::class); $list = $this->generateArrayListFromItems($itemsForList); $list->push(Member::create(['FirstName' => 'Foo', 'Surname' => 'Foo'])); $list->push(Member::create(['FirstName' => 'Bar', 'Surname' => 'Bar'])); @@ -224,11 +222,10 @@ public function testAssertListEquals($matches, $itemsForList) * * @param $matches * @param $itemsForList - * - * @expectedException \PHPUnit_Framework_ExpectationFailedException */ public function testAssertListEqualsFailsOnNonEqualLists($matches, $itemsForList) { + $this->expectException(ExpectationFailedException::class); $list = $this->generateArrayListFromItems($itemsForList); $this->assertListEquals($matches, $list); diff --git a/tests/php/Dev/YamlFixtureTest.php b/tests/php/Dev/YamlFixtureTest.php index 4349940dec4..72dcadb1ae6 100644 --- a/tests/php/Dev/YamlFixtureTest.php +++ b/tests/php/Dev/YamlFixtureTest.php @@ -45,11 +45,9 @@ public function testStringFixture() $this->assertNull($obj->getFixtureFile()); } - /** - * @expectedException InvalidArgumentException - */ public function testFailsWithInvalidFixturePath() { + $this->expectException(InvalidArgumentException::class); $invalidPath = ltrim(FRAMEWORK_DIR . '/tests/testing/invalid.yml', '/'); $obj = Injector::inst()->create(YamlFixture::class, $invalidPath); } diff --git a/tests/php/Forms/CheckboxSetFieldMultiEnumTest.php b/tests/php/Forms/CheckboxSetFieldMultiEnumTest.php index 8cbbf813f4d..13c5c42385f 100644 --- a/tests/php/Forms/CheckboxSetFieldMultiEnumTest.php +++ b/tests/php/Forms/CheckboxSetFieldMultiEnumTest.php @@ -26,7 +26,7 @@ public static function getExtraDataObjects() } } - public function setUp() + protected function setUp(): void { if (!(DB::get_conn() instanceof MySQLDatabase)) { $this->markTestSkipped('DBMultiEnum only supported by MySQL'); @@ -35,7 +35,7 @@ public function setUp() parent::setUp(); } - public function tearDown() + protected function tearDown(): void { if (!(DB::get_conn() instanceof MySQLDatabase)) { return; diff --git a/tests/php/Forms/CompositeFieldTest.php b/tests/php/Forms/CompositeFieldTest.php index 6bc72cda452..622951222af 100644 --- a/tests/php/Forms/CompositeFieldTest.php +++ b/tests/php/Forms/CompositeFieldTest.php @@ -163,7 +163,7 @@ public function testGetAttributesReturnsEmptyTitleForFieldSets() public function testCollateDataFieldsThrowsErrorOnDuplicateChildren() { $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageRegExp( + $this->expectExceptionMessageMatches( "/a field called 'Test' appears twice in your form.*TextField.*TextField/" ); diff --git a/tests/php/Forms/ConfirmedPasswordFieldTest.php b/tests/php/Forms/ConfirmedPasswordFieldTest.php index 2f4face19c6..49bea2d0739 100644 --- a/tests/php/Forms/ConfirmedPasswordFieldTest.php +++ b/tests/php/Forms/ConfirmedPasswordFieldTest.php @@ -16,7 +16,7 @@ class ConfirmedPasswordFieldTest extends SapphireTest { protected $usesDatabase = true; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/DateFieldDisabledTest.php b/tests/php/Forms/DateFieldDisabledTest.php index 6fffdcd1364..b2ce9b60afb 100644 --- a/tests/php/Forms/DateFieldDisabledTest.php +++ b/tests/php/Forms/DateFieldDisabledTest.php @@ -12,7 +12,7 @@ */ class DateFieldDisabledTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); i18n::set_locale('en_NZ'); diff --git a/tests/php/Forms/DateFieldTest.php b/tests/php/Forms/DateFieldTest.php index 87065fc8a94..435332a3a21 100644 --- a/tests/php/Forms/DateFieldTest.php +++ b/tests/php/Forms/DateFieldTest.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms\Tests; use IntlDateFormatter; +use LogicException; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField_Disabled; @@ -16,7 +17,7 @@ */ class DateFieldTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); i18n::set_locale('en_NZ'); @@ -192,36 +193,30 @@ public function testMDYFormat() ); } - /** - * @expectedException \LogicException - * @expectedExceptionMessageRegExp /Please opt-out .* if using setDateFormat/ - */ public function testHtml5WithCustomFormatThrowsException() { + $this->expectException(LogicException::class); + $this->expectExceptionMessageMatches('/Please opt-out .* if using setDateFormat/'); $dateField = new DateField('Date', 'Date'); $dateField->setValue('2010-03-31'); $dateField->setDateFormat('d/M/y'); $dateField->Value(); } - /** - * @expectedException \LogicException - * @expectedExceptionMessageRegExp /Please opt-out .* if using setDateLength/ - */ public function testHtml5WithCustomDateLengthThrowsException() { + $this->expectException(LogicException::class); + $this->expectExceptionMessageMatches('/Please opt-out .* if using setDateLength/'); $dateField = new DateField('Date', 'Date'); $dateField->setValue('2010-03-31'); $dateField->setDateLength(IntlDateFormatter::MEDIUM); $dateField->Value(); } - /** - * @expectedException \LogicException - * @expectedExceptionMessageRegExp /Please opt-out .* if using setLocale/ - */ public function testHtml5WithCustomLocaleThrowsException() { + $this->expectException(LogicException::class); + $this->expectExceptionMessageMatches('/Please opt-out .* if using setLocale/'); $dateField = new DateField('Date', 'Date'); $dateField->setValue('2010-03-31'); $dateField->setLocale('de_DE'); diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index ec36a0ab803..6fa5e39a76b 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -17,7 +17,7 @@ class DatetimeFieldTest extends SapphireTest { protected $timezone = null; - protected function setUp() + protected function setUp(): void { parent::setUp(); i18n::set_locale('en_NZ'); @@ -26,7 +26,7 @@ protected function setUp() $this->timezone = date_default_timezone_get(); } - protected function tearDown() + protected function tearDown(): void { DBDatetime::clear_mock_now(); date_default_timezone_set($this->timezone); @@ -146,7 +146,7 @@ public function testSetValueWithLocalised() $this->assertEquals($datetimeField->dataValue(), '2003-03-29 23:00:00'); // Some localisation packages exclude the ',' in default medium format - $this->assertRegExp( + $this->assertMatchesRegularExpression( '#29/03/2003(,)? 11:00:00 (PM|pm)#', $datetimeField->Value(), 'User value is formatted, and in user timezone' @@ -490,12 +490,10 @@ public function testPerformReadonlyTransformation() $this->assertTrue($result->isReadonly()); } - /** - * @expectedException \BadMethodCallException - * @expectedExceptionMessage Can't change timezone after setting a value - */ public function testSetTimezoneThrowsExceptionWhenChangingTimezoneAfterSettingValue() { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage("Can't change timezone after setting a value"); date_default_timezone_set('Europe/Berlin'); $field = new DatetimeField('Datetime', 'Time', '2003-03-29 23:59:38'); $field->setTimezone('Pacific/Auckland'); diff --git a/tests/php/Forms/DefaultFormFactoryTest.php b/tests/php/Forms/DefaultFormFactoryTest.php index c8ab715f33e..8e4f26f6e27 100644 --- a/tests/php/Forms/DefaultFormFactoryTest.php +++ b/tests/php/Forms/DefaultFormFactoryTest.php @@ -9,12 +9,10 @@ class DefaultFormFactoryTest extends SapphireTest { - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessageRegExp /Missing required context/ - */ public function testGetFormThrowsExceptionOnMissingContext() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Missing required context/'); $factory = new DefaultFormFactory(); $factory->getForm(); } diff --git a/tests/php/Forms/FormFactoryTest.php b/tests/php/Forms/FormFactoryTest.php index 67242c3e9a6..8cac9edc463 100644 --- a/tests/php/Forms/FormFactoryTest.php +++ b/tests/php/Forms/FormFactoryTest.php @@ -28,7 +28,7 @@ public static function getExtraDataObjects() return []; } - public function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/FormFieldTest.php b/tests/php/Forms/FormFieldTest.php index e6333e43f41..1cef4ec60e8 100644 --- a/tests/php/Forms/FormFieldTest.php +++ b/tests/php/Forms/FormFieldTest.php @@ -2,10 +2,10 @@ namespace SilverStripe\Forms\Tests; +use LogicException; use ReflectionClass; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Convert; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\FieldList; @@ -443,11 +443,9 @@ public function testLinkWithForm() $this->assertSame('foo/field/Test/bar', $field->Link('bar')); } - /** - * @expectedException \LogicException - */ public function testLinkWithoutForm() { + $this->expectException(LogicException::class); $field = new FormField('Test'); $field->Link('bar'); } diff --git a/tests/php/Forms/FormSchemaTest.php b/tests/php/Forms/FormSchemaTest.php index 7af4d8d70e1..8abb38f13ce 100644 --- a/tests/php/Forms/FormSchemaTest.php +++ b/tests/php/Forms/FormSchemaTest.php @@ -20,7 +20,7 @@ */ class FormSchemaTest extends SapphireTest { - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/FormTest.php b/tests/php/Forms/FormTest.php index 8ee42df4bb1..4a96a5235de 100644 --- a/tests/php/Forms/FormTest.php +++ b/tests/php/Forms/FormTest.php @@ -56,7 +56,7 @@ class FormTest extends FunctionalTest protected static $disable_themes = true; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/GridField/GridFieldActionMenuTest.php b/tests/php/Forms/GridField/GridFieldActionMenuTest.php index 51794715916..4404b62542a 100644 --- a/tests/php/Forms/GridField/GridFieldActionMenuTest.php +++ b/tests/php/Forms/GridField/GridFieldActionMenuTest.php @@ -55,7 +55,7 @@ class GridFieldActionMenuTest extends SapphireTest Permissions::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = new DataList(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldDataColumnsTest.php b/tests/php/Forms/GridField/GridFieldDataColumnsTest.php index c1f5ca9d5cf..c508f8e72c4 100644 --- a/tests/php/Forms/GridField/GridFieldDataColumnsTest.php +++ b/tests/php/Forms/GridField/GridFieldDataColumnsTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests\GridField; +use InvalidArgumentException; use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Security\Member; use SilverStripe\Dev\SapphireTest; @@ -41,11 +42,10 @@ public function testGridFieldCustomDisplayFields() /** * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::setDisplayFields * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields - * - * @expectedException \InvalidArgumentException */ public function testGridFieldDisplayFieldsWithBadArguments() { + $this->expectException(InvalidArgumentException::class); $obj = new GridField('testfield', 'testfield', Member::get()); $columns = $obj->getConfig()->getComponentByType(GridFieldDataColumns::class); $columns->setDisplayFields(new stdClass()); diff --git a/tests/php/Forms/GridField/GridFieldDeleteActionTest.php b/tests/php/Forms/GridField/GridFieldDeleteActionTest.php index 4f2005b03a8..bb01599a767 100644 --- a/tests/php/Forms/GridField/GridFieldDeleteActionTest.php +++ b/tests/php/Forms/GridField/GridFieldDeleteActionTest.php @@ -56,7 +56,7 @@ class GridFieldDeleteActionTest extends SapphireTest Permissions::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = new DataList(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldEditButtonTest.php b/tests/php/Forms/GridField/GridFieldEditButtonTest.php index d5a14b90202..33ea0b7d266 100644 --- a/tests/php/Forms/GridField/GridFieldEditButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldEditButtonTest.php @@ -52,7 +52,7 @@ class GridFieldEditButtonTest extends SapphireTest Permissions::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = new DataList(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldExportButtonTest.php b/tests/php/Forms/GridField/GridFieldExportButtonTest.php index 222d5f6575c..3496332b4cd 100644 --- a/tests/php/Forms/GridField/GridFieldExportButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldExportButtonTest.php @@ -35,7 +35,7 @@ class GridFieldExportButtonTest extends SapphireTest NoView::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php index 91e1a14f3da..32292ed3ad1 100644 --- a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php +++ b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php @@ -50,7 +50,7 @@ class GridFieldFilterHeaderTest extends SapphireTest Mom::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = new DataList(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldLazyLoaderTest.php b/tests/php/Forms/GridField/GridFieldLazyLoaderTest.php index 553f6ae0364..3b627acbb5f 100644 --- a/tests/php/Forms/GridField/GridFieldLazyLoaderTest.php +++ b/tests/php/Forms/GridField/GridFieldLazyLoaderTest.php @@ -44,7 +44,7 @@ class GridFieldLazyLoaderTest extends SapphireTest Team::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = DataList::create(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldPaginatorTest.php b/tests/php/Forms/GridField/GridFieldPaginatorTest.php index f892d421d43..4504143c62e 100644 --- a/tests/php/Forms/GridField/GridFieldPaginatorTest.php +++ b/tests/php/Forms/GridField/GridFieldPaginatorTest.php @@ -49,7 +49,7 @@ class GridFieldPaginatorTest extends FunctionalTest Cheerleader::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->list = new DataList(Team::class); diff --git a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php index d428a80d35d..59e73a494ca 100644 --- a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php @@ -19,7 +19,7 @@ class GridFieldPrintButtonTest extends SapphireTest TestObject::class, ]; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/tests/php/Forms/GridField/GridFieldTest.php b/tests/php/Forms/GridField/GridFieldTest.php index ee82cfd0e2e..a7c018d9a9b 100644 --- a/tests/php/Forms/GridField/GridFieldTest.php +++ b/tests/php/Forms/GridField/GridFieldTest.php @@ -2,6 +2,8 @@ namespace SilverStripe\Forms\Tests\GridField; +use InvalidArgumentException; +use LogicException; use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\FieldList; @@ -135,11 +137,10 @@ public function testGridFieldModelClass() /** * @covers \SilverStripe\Forms\GridField\GridField::getModelClass - * - * @expectedException \LogicException */ public function testGridFieldModelClassThrowsException() { + $this->expectException(LogicException::class); $obj = new GridField('testfield', 'testfield', ArrayList::create()); $obj->getModelClass(); } @@ -260,11 +261,10 @@ public function testGetColumnContent() /** * @skipUpgrade * @covers \SilverStripe\Forms\GridField\GridField::getColumnContent - * - * @expectedException \InvalidArgumentException */ public function testGetColumnContentBadArguments() { + $this->expectException(InvalidArgumentException::class); $list = new ArrayList( [ new Member(["ID" => 1, "Email" => "test@example.org"]) @@ -307,11 +307,10 @@ public function testGetColumnAttributes() /** * @covers \SilverStripe\Forms\GridField\GridField::getColumnAttributes - * - * @expectedException \InvalidArgumentException */ public function testGetColumnAttributesBadArguments() { + $this->expectException(InvalidArgumentException::class); $list = new ArrayList( [ new Member(["ID" => 1, "Email" => "test@example.org"]) @@ -322,11 +321,9 @@ public function testGetColumnAttributesBadArguments() $obj->getColumnAttributes($list->first(), 'Non-existing'); } - /** - * @expectedException \LogicException - */ public function testGetColumnAttributesBadResponseFromComponent() { + $this->expectException(LogicException::class); $list = new ArrayList( [ new Member(["ID" => 1, "Email" => "test@example.org"]) @@ -355,11 +352,10 @@ public function testGetColumnMetadata() /** * @covers \SilverStripe\Forms\GridField\GridField::getColumnMetadata - * - * @expectedException \LogicException */ public function testGetColumnMetadataBadResponseFromComponent() { + $this->expectException(LogicException::class); $list = new ArrayList( [ new Member(["ID" => 1, "Email" => "test@example.org"]) @@ -372,11 +368,10 @@ public function testGetColumnMetadataBadResponseFromComponent() /** * @covers \SilverStripe\Forms\GridField\GridField::getColumnMetadata - * - * @expectedException \InvalidArgumentException */ public function testGetColumnMetadataBadArguments() { + $this->expectException(InvalidArgumentException::class); $list = ArrayList::create(); $config = GridFieldConfig::create()->addComponent(new Component); $obj = new GridField('testfield', 'testfield', $list, $config); @@ -385,11 +380,10 @@ public function testGetColumnMetadataBadArguments() /** * @covers \SilverStripe\Forms\GridField\GridField::handleAction - * - * @expectedException \InvalidArgumentException */ public function testHandleActionBadArgument() { + $this->expectException(InvalidArgumentException::class); $obj = new GridField('testfield', 'testfield'); $obj->handleAlterAction('prft', [], []); } @@ -526,11 +520,10 @@ public function testGridFieldCustomFragmentsNesting() /** * Test that circular dependencies throw an exception - * - * @expectedException \LogicException */ public function testGridFieldCustomFragmentsCircularDependencyThrowsException() { + $this->expectException(LogicException::class); $config = GridFieldConfig::create()->addComponents( new HTMLFragments( [ diff --git a/tests/php/Forms/GroupedDropdownFieldTest.php b/tests/php/Forms/GroupedDropdownFieldTest.php index 68b4a2b72ec..ccd06f7f50d 100644 --- a/tests/php/Forms/GroupedDropdownFieldTest.php +++ b/tests/php/Forms/GroupedDropdownFieldTest.php @@ -82,7 +82,7 @@ public function testEmptyString() ] ); - $this->assertRegExp( + $this->assertMatchesRegularExpression( '/