| Total Complexity | 162 | 
| Total Lines | 1409 | 
| Duplicated Lines | 0 % | 
| Changes | 2 | ||
| Bugs | 0 | Features | 0 | 
Complex classes like SapphireTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use SapphireTest, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 62 | class SapphireTest extends TestCase implements TestOnly | ||
| 63 |     { | ||
| 64 | /** | ||
| 65 | * Path to fixture data for this test run. | ||
| 66 | * If passed as an array, multiple fixture files will be loaded. | ||
| 67 | * Please note that you won't be able to refer with "=>" notation | ||
| 68 | * between the fixtures, they act independent of each other. | ||
| 69 | * | ||
| 70 | * @var string|array | ||
| 71 | */ | ||
| 72 | protected static $fixture_file = null; | ||
| 73 | |||
| 74 | /** | ||
| 75 | * @deprecated 4.0..5.0 Use FixtureTestState instead | ||
| 76 | * @var FixtureFactory | ||
| 77 | */ | ||
| 78 | protected $fixtureFactory; | ||
| 79 | |||
| 80 | /** | ||
| 81 | * @var Boolean If set to TRUE, this will force a test database to be generated | ||
| 82 |          * in {@link setUp()}. Note that this flag is overruled by the presence of a | ||
| 83 |          * {@link $fixture_file}, which always forces a database build. | ||
| 84 | * | ||
| 85 | * @var bool | ||
| 86 | */ | ||
| 87 | protected $usesDatabase = null; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * This test will cleanup its state via transactions. | ||
| 91 | * If set to false a full schema is forced between tests, but at a performance cost. | ||
| 92 | * | ||
| 93 | * @var bool | ||
| 94 | */ | ||
| 95 | protected $usesTransactions = true; | ||
| 96 | |||
| 97 | /** | ||
| 98 | * @var bool | ||
| 99 | */ | ||
| 100 | protected static $is_running_test = false; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * By default, setUp() does not require default records. Pass | ||
| 104 | * class names in here, and the require/augment default records | ||
| 105 | * function will be called on them. | ||
| 106 | * | ||
| 107 | * @var array | ||
| 108 | */ | ||
| 109 | protected $requireDefaultRecordsFrom = []; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * A list of extensions that can't be applied during the execution of this run. If they are | ||
| 113 | * applied, they will be temporarily removed and a database migration called. | ||
| 114 | * | ||
| 115 | * The keys of the are the classes that the extensions can't be applied the extensions to, and | ||
| 116 | * the values are an array of illegal extensions on that class. | ||
| 117 | * | ||
| 118 | * Set a class to `*` to remove all extensions (unadvised) | ||
| 119 | * | ||
| 120 | * @var array | ||
| 121 | */ | ||
| 122 | protected static $illegal_extensions = []; | ||
| 123 | |||
| 124 | /** | ||
| 125 | * A list of extensions that must be applied during the execution of this run. If they are | ||
| 126 | * not applied, they will be temporarily added and a database migration called. | ||
| 127 | * | ||
| 128 | * The keys of the are the classes to apply the extensions to, and the values are an array | ||
| 129 | * of required extensions on that class. | ||
| 130 | * | ||
| 131 | * Example: | ||
| 132 | * <code> | ||
| 133 |          * array("MyTreeDataObject" => array("Versioned", "Hierarchy")) | ||
| 134 | * </code> | ||
| 135 | * | ||
| 136 | * @var array | ||
| 137 | */ | ||
| 138 | protected static $required_extensions = []; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * By default, the test database won't contain any DataObjects that have the interface TestOnly. | ||
| 142 | * This variable lets you define additional TestOnly DataObjects to set up for this test. | ||
| 143 | * Set it to an array of DataObject subclass names. | ||
| 144 | * | ||
| 145 | * @var array | ||
| 146 | */ | ||
| 147 | protected static $extra_dataobjects = []; | ||
| 148 | |||
| 149 | /** | ||
| 150 |          * List of class names of {@see Controller} objects to register routes for | ||
| 151 | * Controllers must implement Link() method | ||
| 152 | * | ||
| 153 | * @var array | ||
| 154 | */ | ||
| 155 | protected static $extra_controllers = []; | ||
| 156 | |||
| 157 | /** | ||
| 158 | * We need to disabling backing up of globals to avoid overriding | ||
| 159 | * the few globals SilverStripe relies on, like $lang for the i18n subsystem. | ||
| 160 | * | ||
| 161 | * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html | ||
| 162 | */ | ||
| 163 | protected $backupGlobals = false; | ||
| 164 | |||
| 165 | /** | ||
| 166 | * State management container for SapphireTest | ||
| 167 | * | ||
| 168 | * @var SapphireTestState | ||
| 169 | */ | ||
| 170 | protected static $state = null; | ||
| 171 | |||
| 172 | /** | ||
| 173 | * Temp database helper | ||
| 174 | * | ||
| 175 | * @var TempDatabase | ||
| 176 | */ | ||
| 177 | protected static $tempDB = null; | ||
| 178 | |||
| 179 | /** | ||
| 180 | * @return TempDatabase | ||
| 181 | */ | ||
| 182 | public static function tempDB() | ||
| 183 |         { | ||
| 184 |             if (!class_exists(TempDatabase::class)) { | ||
| 185 | return null; | ||
| 186 | } | ||
| 187 | |||
| 188 |             if (!static::$tempDB) { | ||
| 189 | static::$tempDB = TempDatabase::create(); | ||
| 190 | } | ||
| 191 | return static::$tempDB; | ||
| 192 | } | ||
| 193 | |||
| 194 | /** | ||
| 195 | * Gets illegal extensions for this class | ||
| 196 | * | ||
| 197 | * @return array | ||
| 198 | */ | ||
| 199 | public static function getIllegalExtensions() | ||
| 200 |         { | ||
| 201 | return static::$illegal_extensions; | ||
| 202 | } | ||
| 203 | |||
| 204 | /** | ||
| 205 | * Gets required extensions for this class | ||
| 206 | * | ||
| 207 | * @return array | ||
| 208 | */ | ||
| 209 | public static function getRequiredExtensions() | ||
| 210 |         { | ||
| 211 | return static::$required_extensions; | ||
| 212 | } | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Check if test bootstrapping has been performed. Must not be relied on | ||
| 216 | * outside of unit tests. | ||
| 217 | * | ||
| 218 | * @return bool | ||
| 219 | */ | ||
| 220 | protected static function is_running_test() | ||
| 221 |         { | ||
| 222 | return self::$is_running_test; | ||
| 223 | } | ||
| 224 | |||
| 225 | /** | ||
| 226 | * Set test running state | ||
| 227 | * | ||
| 228 | * @param bool $bool | ||
| 229 | */ | ||
| 230 | protected static function set_is_running_test($bool) | ||
| 231 |         { | ||
| 232 | self::$is_running_test = $bool; | ||
| 233 | } | ||
| 234 | |||
| 235 | /** | ||
| 236 | * @return String | ||
| 237 | */ | ||
| 238 | public static function get_fixture_file() | ||
| 239 |         { | ||
| 240 | return static::$fixture_file; | ||
| 241 | } | ||
| 242 | |||
| 243 | /** | ||
| 244 | * @return bool | ||
| 245 | */ | ||
| 246 | public function getUsesDatabase() | ||
| 247 |         { | ||
| 248 | return $this->usesDatabase; | ||
| 249 | } | ||
| 250 | |||
| 251 | /** | ||
| 252 | * @return bool | ||
| 253 | */ | ||
| 254 | public function getUsesTransactions() | ||
| 255 |         { | ||
| 256 | return $this->usesTransactions; | ||
| 257 | } | ||
| 258 | |||
| 259 | /** | ||
| 260 | * @return array | ||
| 261 | */ | ||
| 262 | public function getRequireDefaultRecordsFrom() | ||
| 263 |         { | ||
| 264 | return $this->requireDefaultRecordsFrom; | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * Setup the test. | ||
| 269 | * Always sets up in order: | ||
| 270 | * - Reset php state | ||
| 271 | * - Nest | ||
| 272 | * - Custom state helpers | ||
| 273 | * | ||
| 274 | * User code should call parent::setUp() before custom setup code | ||
| 275 | */ | ||
| 276 | protected function setUp(): void | ||
| 277 |         { | ||
| 278 |             if (!defined('FRAMEWORK_PATH')) { | ||
| 279 | trigger_error( | ||
| 280 | 'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', | ||
| 281 | E_USER_WARNING | ||
| 282 | ); | ||
| 283 | } | ||
| 284 | |||
| 285 | // Call state helpers | ||
| 286 | static::$state->setUp($this); | ||
| 287 | |||
| 288 | // We cannot run the tests on this abstract class. | ||
| 289 |             if (static::class == __CLASS__) { | ||
| 290 |                 $this->markTestSkipped(sprintf('Skipping %s ', static::class)); | ||
| 291 | } | ||
| 292 | |||
| 293 | // i18n needs to be set to the defaults or tests fail | ||
| 294 |             if (class_exists(i18n::class)) { | ||
| 295 |                 i18n::set_locale(i18n::config()->uninherited('default_locale')); | ||
| 296 | } | ||
| 297 | |||
| 298 | // Set default timezone consistently to avoid NZ-specific dependencies | ||
| 299 |             date_default_timezone_set('UTC'); | ||
| 300 | |||
| 301 |             if (class_exists(Member::class)) { | ||
| 302 | Member::set_password_validator(null); | ||
| 303 | } | ||
| 304 | |||
| 305 |             if (class_exists(Cookie::class)) { | ||
| 306 |                 Cookie::config()->update('report_errors', false); | ||
| 307 | } | ||
| 308 | |||
| 309 |             if (class_exists(RootURLController::class)) { | ||
| 310 | RootURLController::reset(); | ||
| 311 | } | ||
| 312 | |||
| 313 |             if (class_exists(Security::class)) { | ||
| 314 | Security::clear_database_is_ready(); | ||
| 315 | } | ||
| 316 | |||
| 317 | // Set up test routes | ||
| 318 | $this->setUpRoutes(); | ||
| 319 | |||
| 320 | $fixtureFiles = $this->getFixturePaths(); | ||
| 321 | |||
| 322 |             if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { | ||
| 323 | // Assign fixture factory to deprecated prop in case old tests use it over the getter | ||
| 324 | /** @var FixtureTestState $fixtureState */ | ||
| 325 |                 $fixtureState = static::$state->getStateByName('fixtures'); | ||
| 326 | $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class); | ||
| 327 | |||
| 328 |                 $this->logInWithPermission('ADMIN'); | ||
| 329 | } | ||
| 330 | |||
| 331 | // turn off template debugging | ||
| 332 |             if (class_exists(SSViewer::class)) { | ||
| 333 |                 SSViewer::config()->update('source_file_comments', false); | ||
| 334 | } | ||
| 335 | |||
| 336 | // Set up the test mailer | ||
| 337 |             if (class_exists(TestMailer::class)) { | ||
| 338 | Injector::inst()->registerService(new TestMailer(), Mailer::class); | ||
| 339 | } | ||
| 340 | |||
| 341 |             if (class_exists(Email::class)) { | ||
| 342 |                 Email::config()->remove('send_all_emails_to'); | ||
| 343 |                 Email::config()->remove('send_all_emails_from'); | ||
| 344 |                 Email::config()->remove('cc_all_emails_to'); | ||
| 345 |                 Email::config()->remove('bcc_all_emails_to'); | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | |||
| 350 | /** | ||
| 351 | * Helper method to determine if the current test should enable a test database | ||
| 352 | * | ||
| 353 | * @param $fixtureFiles | ||
| 354 | * @return bool | ||
| 355 | */ | ||
| 356 | protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) | ||
| 357 |         { | ||
| 358 | $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; | ||
| 359 | |||
| 360 | return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) | ||
| 361 | || $this->currentTestEnablesDatabase(); | ||
| 362 | } | ||
| 363 | |||
| 364 | /** | ||
| 365 | * Helper method to check, if the current test uses the database. | ||
| 366 | * This can be switched on with the annotation "@useDatabase" | ||
| 367 | * | ||
| 368 | * @return bool | ||
| 369 | */ | ||
| 370 | protected function currentTestEnablesDatabase() | ||
| 371 |         { | ||
| 372 | $annotations = $this->getAnnotations(); | ||
| 373 | |||
| 374 |             return array_key_exists('useDatabase', $annotations['method']) | ||
| 375 | && $annotations['method']['useDatabase'][0] !== 'false'; | ||
| 376 | } | ||
| 377 | |||
| 378 | /** | ||
| 379 | * Helper method to check, if the current test uses the database. | ||
| 380 | * This can be switched on with the annotation "@useDatabase false" | ||
| 381 | * | ||
| 382 | * @return bool | ||
| 383 | */ | ||
| 384 | protected function currentTestDisablesDatabase() | ||
| 385 |         { | ||
| 386 | $annotations = $this->getAnnotations(); | ||
| 387 | |||
| 388 |             return array_key_exists('useDatabase', $annotations['method']) | ||
| 389 | && $annotations['method']['useDatabase'][0] === 'false'; | ||
| 390 | } | ||
| 391 | |||
| 392 | /** | ||
| 393 |          * Called once per test case ({@link SapphireTest} subclass). | ||
| 394 |          * This is different to {@link setUp()}, which gets called once | ||
| 395 | * per method. Useful to initialize expensive operations which | ||
| 396 | * don't change state for any called method inside the test, | ||
| 397 |          * e.g. dynamically adding an extension. See {@link teardownAfterClass()} | ||
| 398 | * for tearing down the state again. | ||
| 399 | * | ||
| 400 | * Always sets up in order: | ||
| 401 | * - Reset php state | ||
| 402 | * - Nest | ||
| 403 | * - Custom state helpers | ||
| 404 | * | ||
| 405 | * User code should call parent::setUpBeforeClass() before custom setup code | ||
| 406 | * | ||
| 407 | * @throws Exception | ||
| 408 | */ | ||
| 409 | public static function setUpBeforeClass(): void | ||
| 410 |         { | ||
| 411 | // Start tests | ||
| 412 | static::start(); | ||
| 413 | |||
| 414 |             if (!static::$state) { | ||
| 415 |                 throw new Exception('SapphireTest failed to bootstrap!'); | ||
| 416 | } | ||
| 417 | |||
| 418 | // Call state helpers | ||
| 419 | static::$state->setUpOnce(static::class); | ||
| 420 | |||
| 421 | // Build DB if we have objects | ||
| 422 |             if (class_exists(DataObject::class) && static::getExtraDataObjects()) { | ||
| 423 | DataObject::reset(); | ||
| 424 | static::resetDBSchema(true, true); | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | /** | ||
| 429 | * tearDown method that's called once per test class rather once per test method. | ||
| 430 | * | ||
| 431 | * Always sets up in order: | ||
| 432 | * - Custom state helpers | ||
| 433 | * - Unnest | ||
| 434 | * - Reset php state | ||
| 435 | * | ||
| 436 | * User code should call parent::tearDownAfterClass() after custom tear down code | ||
| 437 | */ | ||
| 438 | public static function tearDownAfterClass(): void | ||
| 439 |         { | ||
| 440 | // Call state helpers | ||
| 441 | static::$state->tearDownOnce(static::class); | ||
| 442 | |||
| 443 | // Reset DB schema | ||
| 444 | static::resetDBSchema(); | ||
| 445 | } | ||
| 446 | |||
| 447 | /** | ||
| 448 | * @return FixtureFactory|false | ||
| 449 | * @deprecated 4.0.0:5.0.0 | ||
| 450 | */ | ||
| 451 | public function getFixtureFactory() | ||
| 452 |         { | ||
| 453 |             Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); | ||
| 454 | /** @var FixtureTestState $state */ | ||
| 455 |             $state = static::$state->getStateByName('fixtures'); | ||
| 456 | return $state->getFixtureFactory(static::class); | ||
| 457 | } | ||
| 458 | |||
| 459 | /** | ||
| 460 | * Sets a new fixture factory | ||
| 461 | * @param FixtureFactory $factory | ||
| 462 | * @return $this | ||
| 463 | * @deprecated 4.0.0:5.0.0 | ||
| 464 | */ | ||
| 465 | public function setFixtureFactory(FixtureFactory $factory) | ||
| 466 |         { | ||
| 467 |             Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); | ||
| 468 | /** @var FixtureTestState $state */ | ||
| 469 |             $state = static::$state->getStateByName('fixtures'); | ||
| 470 | $state->setFixtureFactory($factory, static::class); | ||
| 471 | $this->fixtureFactory = $factory; | ||
| 472 | return $this; | ||
| 473 | } | ||
| 474 | |||
| 475 | /** | ||
| 476 | * Get the ID of an object from the fixture. | ||
| 477 | * | ||
| 478 | * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work | ||
| 479 | * @param string $identifier The identifier string, as provided in your fixture file | ||
| 480 | * @return int | ||
| 481 | */ | ||
| 482 | protected function idFromFixture($className, $identifier) | ||
| 483 |         { | ||
| 484 | /** @var FixtureTestState $state */ | ||
| 485 |             $state = static::$state->getStateByName('fixtures'); | ||
| 486 | $id = $state->getFixtureFactory(static::class)->getId($className, $identifier); | ||
| 487 | |||
| 488 |             if (!$id) { | ||
| 489 | throw new InvalidArgumentException(sprintf( | ||
| 490 | "Couldn't find object '%s' (class: %s)", | ||
| 491 | $identifier, | ||
| 492 | $className | ||
| 493 | )); | ||
| 494 | } | ||
| 495 | |||
| 496 | return $id; | ||
| 497 | } | ||
| 498 | |||
| 499 | /** | ||
| 500 | * Return all of the IDs in the fixture of a particular class name. | ||
| 501 | * Will collate all IDs form all fixtures if multiple fixtures are provided. | ||
| 502 | * | ||
| 503 | * @param string $className The data class or table name, as specified in your fixture file | ||
| 504 | * @return array A map of fixture-identifier => object-id | ||
| 505 | */ | ||
| 506 | protected function allFixtureIDs($className) | ||
| 507 |         { | ||
| 508 | /** @var FixtureTestState $state */ | ||
| 509 |             $state = static::$state->getStateByName('fixtures'); | ||
| 510 | return $state->getFixtureFactory(static::class)->getIds($className); | ||
| 511 | } | ||
| 512 | |||
| 513 | /** | ||
| 514 | * Get an object from the fixture. | ||
| 515 | * | ||
| 516 | * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work | ||
| 517 | * @param string $identifier The identifier string, as provided in your fixture file | ||
| 518 | * | ||
| 519 | * @return DataObject | ||
| 520 | */ | ||
| 521 | protected function objFromFixture($className, $identifier) | ||
| 522 |         { | ||
| 523 | /** @var FixtureTestState $state */ | ||
| 524 |             $state = static::$state->getStateByName('fixtures'); | ||
| 525 | $obj = $state->getFixtureFactory(static::class)->get($className, $identifier); | ||
| 526 | |||
| 527 |             if (!$obj) { | ||
| 528 | throw new InvalidArgumentException(sprintf( | ||
| 529 | "Couldn't find object '%s' (class: %s)", | ||
| 530 | $identifier, | ||
| 531 | $className | ||
| 532 | )); | ||
| 533 | } | ||
| 534 | |||
| 535 | return $obj; | ||
| 536 | } | ||
| 537 | |||
| 538 | /** | ||
| 539 | * Load a YAML fixture file into the database. | ||
| 540 | * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. | ||
| 541 | * Doesn't clear existing fixtures. | ||
| 542 | * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir | ||
| 543 | * @deprecated 4.0.0:5.0.0 | ||
| 544 | * | ||
| 545 | */ | ||
| 546 | public function loadFixture($fixtureFile) | ||
| 547 |         { | ||
| 548 |             Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead'); | ||
| 549 | $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); | ||
| 550 | $fixture->writeInto($this->getFixtureFactory()); | ||
| 551 | } | ||
| 552 | |||
| 553 | /** | ||
| 554 | * Clear all fixtures which were previously loaded through | ||
| 555 |          * {@link loadFixture()} | ||
| 556 | */ | ||
| 557 | public function clearFixtures() | ||
| 558 |         { | ||
| 559 | /** @var FixtureTestState $state */ | ||
| 560 |             $state = static::$state->getStateByName('fixtures'); | ||
| 561 | $state->getFixtureFactory(static::class)->clear(); | ||
| 562 | } | ||
| 563 | |||
| 564 | /** | ||
| 565 | * Useful for writing unit tests without hardcoding folder structures. | ||
| 566 | * | ||
| 567 | * @return string Absolute path to current class. | ||
| 568 | */ | ||
| 569 | protected function getCurrentAbsolutePath() | ||
| 570 |         { | ||
| 571 | $filename = ClassLoader::inst()->getItemPath(static::class); | ||
| 572 |             if (!$filename) { | ||
| 573 |                 throw new LogicException('getItemPath returned null for ' . static::class | ||
| 574 | . '. Try adding flush=1 to the test run.'); | ||
| 575 | } | ||
| 576 | return dirname($filename); | ||
| 577 | } | ||
| 578 | |||
| 579 | /** | ||
| 580 | * @return string File path relative to webroot | ||
| 581 | */ | ||
| 582 | protected function getCurrentRelativePath() | ||
| 590 | } | ||
| 591 | |||
| 592 | /** | ||
| 593 | * Setup the test. | ||
| 594 | * Always sets up in order: | ||
| 595 | * - Custom state helpers | ||
| 596 | * - Unnest | ||
| 597 | * - Reset php state | ||
| 598 | * | ||
| 599 | * User code should call parent::tearDown() after custom tear down code | ||
| 600 | */ | ||
| 601 | protected function tearDown(): void | ||
| 621 | } | ||
| 622 | |||
| 623 | public static function assertContains( | ||
| 624 | $needle, | ||
| 625 | $haystack, | ||
| 626 | $message = '', | ||
| 627 | $ignoreCase = false, | ||
| 628 | $checkForObjectIdentity = true, | ||
| 629 | $checkForNonObjectIdentity = false | ||
| 630 |         ): void { | ||
| 631 |             if ($haystack instanceof DBField) { | ||
| 632 | $haystack = (string)$haystack; | ||
| 633 | } | ||
| 634 |             if (is_iterable($haystack)) { | ||
| 635 | $strict = is_object($needle) ? $checkForObjectIdentity : $checkForNonObjectIdentity; | ||
| 636 |                 if ($strict) { | ||
| 637 | parent::assertContains($needle, $haystack, $message); | ||
| 638 |                 } else { | ||
| 639 | parent::assertContainsEquals($needle, $haystack, $message); | ||
| 640 | } | ||
| 641 |             } else { | ||
| 642 | static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase); | ||
| 643 | } | ||
| 644 | } | ||
| 645 | |||
| 646 | public static function assertContainsNonIterable( | ||
| 647 | $needle, | ||
| 648 | $haystack, | ||
| 649 | $message = '', | ||
| 650 | $ignoreCase = false | ||
| 651 |         ): void { | ||
| 652 |             if ($haystack instanceof DBField) { | ||
| 653 | $haystack = (string)$haystack; | ||
| 654 | } | ||
| 655 |             if ($ignoreCase) { | ||
| 656 | parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message); | ||
| 657 |             } else { | ||
| 658 | parent::assertStringContainsString($needle, $haystack, $message); | ||
| 659 | } | ||
| 660 | } | ||
| 661 | |||
| 662 | public static function assertNotContains( | ||
| 682 | } | ||
| 683 | } | ||
| 684 | |||
| 685 | protected static function assertNotContainsNonIterable( | ||
| 698 | } | ||
| 699 | } | ||
| 700 | |||
| 701 | /** | ||
| 702 | * Backwards compatibility for core tests | ||
| 703 | */ | ||
| 704 | public static function assertInternalType($expected, $actual, $message = '') | ||
| 705 |         { | ||
| 706 |             switch ($expected) { | ||
| 707 | case 'numeric': | ||
| 708 | static::assertIsNumeric($actual, $message); | ||
| 709 | return; | ||
| 710 | case 'integer': | ||
| 711 | case 'int': | ||
| 712 | static::assertIsInt($actual, $message); | ||
| 713 | return; | ||
| 714 | case 'double': | ||
| 715 | case 'float': | ||
| 716 | case 'real': | ||
| 717 | static::assertIsFloat($actual, $message); | ||
| 718 | return; | ||
| 719 | case 'string': | ||
| 720 | static::assertIsString($actual, $message); | ||
| 721 | return; | ||
| 722 | case 'boolean': | ||
| 723 | case 'bool': | ||
| 724 | static::assertIsBool($actual, $message); | ||
| 725 | return; | ||
| 726 | case 'null': | ||
| 727 | static::assertTrue(is_null($actual), $message); | ||
| 728 | return; | ||
| 729 | case 'array': | ||
| 730 | static::assertIsArray($actual, $message); | ||
| 731 | return; | ||
| 732 | case 'object': | ||
| 733 | static::assertIsObject($actual, $message); | ||
| 734 | return; | ||
| 735 | case 'resource': | ||
| 736 | static::assertIsResource($actual, $message); | ||
| 737 | return; | ||
| 738 | case 'resource (closed)': | ||
| 739 | static::assertIsClosedResource($actual, $message); | ||
| 740 | return; | ||
| 741 | case 'scalar': | ||
| 742 | static::assertIsScalar($actual, $message); | ||
| 743 | return; | ||
| 744 | case 'callable': | ||
| 745 | static::assertIsCallable($actual, $message); | ||
| 746 | return; | ||
| 747 | case 'iterable': | ||
| 748 | static::assertIsIterable($actual, $message); | ||
| 749 | return; | ||
| 750 | default: | ||
| 751 | return false; | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | /** | ||
| 756 | * Clear the log of emails sent | ||
| 757 | * | ||
| 758 | * @return bool True if emails cleared | ||
| 759 | */ | ||
| 760 | public function clearEmails() | ||
| 761 |         { | ||
| 762 | /** @var Mailer $mailer */ | ||
| 763 | $mailer = Injector::inst()->get(Mailer::class); | ||
| 764 |             if ($mailer instanceof TestMailer) { | ||
| 765 | $mailer->clearEmails(); | ||
| 766 | return true; | ||
| 767 | } | ||
| 768 | return false; | ||
| 769 | } | ||
| 770 | |||
| 771 | /** | ||
| 772 | * Search for an email that was sent. | ||
| 773 | * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. | ||
| 774 | * @param string $to | ||
| 775 | * @param string $from | ||
| 776 | * @param string $subject | ||
| 777 | * @param string $content | ||
| 778 | * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', | ||
| 779 | * 'HtmlContent' | ||
| 780 | */ | ||
| 781 | public static function findEmail($to, $from = null, $subject = null, $content = null) | ||
| 782 |         { | ||
| 783 | /** @var Mailer $mailer */ | ||
| 784 | $mailer = Injector::inst()->get(Mailer::class); | ||
| 785 |             if ($mailer instanceof TestMailer) { | ||
| 786 | return $mailer->findEmail($to, $from, $subject, $content); | ||
| 787 | } | ||
| 788 | return null; | ||
| 789 | } | ||
| 790 | |||
| 791 | /** | ||
| 792 | * Assert that the matching email was sent since the last call to clearEmails() | ||
| 793 | * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. | ||
| 794 | * | ||
| 795 | * @param string $to | ||
| 796 | * @param string $from | ||
| 797 | * @param string $subject | ||
| 798 | * @param string $content | ||
| 799 | */ | ||
| 800 | public static function assertEmailSent($to, $from = null, $subject = null, $content = null) | ||
| 801 |         { | ||
| 802 | $found = (bool)static::findEmail($to, $from, $subject, $content); | ||
| 803 | |||
| 804 | $infoParts = ''; | ||
| 805 | $withParts = []; | ||
| 806 |             if ($to) { | ||
| 807 | $infoParts .= " to '$to'"; | ||
| 808 | } | ||
| 809 |             if ($from) { | ||
| 810 | $infoParts .= " from '$from'"; | ||
| 811 | } | ||
| 812 |             if ($subject) { | ||
| 813 | $withParts[] = "subject '$subject'"; | ||
| 814 | } | ||
| 815 |             if ($content) { | ||
| 816 | $withParts[] = "content '$content'"; | ||
| 817 | } | ||
| 818 |             if ($withParts) { | ||
| 819 |                 $infoParts .= ' with ' . implode(' and ', $withParts); | ||
| 820 | } | ||
| 821 | |||
| 822 | static::assertTrue( | ||
| 823 | $found, | ||
| 824 | "Failed asserting that an email was sent$infoParts." | ||
| 825 | ); | ||
| 826 | } | ||
| 827 | |||
| 828 | |||
| 829 | /** | ||
| 830 |          * Assert that the given {@link SS_List} includes DataObjects matching the given key-value | ||
| 831 | * pairs. Each match must correspond to 1 distinct record. | ||
| 832 | * | ||
| 833 | * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can | ||
| 834 | * either pass a single pattern or an array of patterns. | ||
| 835 |          * @param SS_List $list The {@link SS_List} to test. | ||
| 836 | * @param string $message | ||
| 837 | * | ||
| 838 | * Examples | ||
| 839 | * -------- | ||
| 840 | * Check that $members includes an entry with Email = [email protected]: | ||
| 841 | * $this->assertListContains(['Email' => '[email protected]'], $members); | ||
| 842 | * | ||
| 843 | * Check that $members includes entries with Email = [email protected] and with | ||
| 844 | * Email = [email protected]: | ||
| 845 | * $this->assertListContains([ | ||
| 846 | * ['Email' => '[email protected]'], | ||
| 847 | * ['Email' => '[email protected]'], | ||
| 848 | * ], $members); | ||
| 849 | */ | ||
| 850 | public static function assertListContains($matches, SS_List $list, $message = '') | ||
| 851 |         { | ||
| 852 |             if (!is_array($matches)) { | ||
| 853 | throw self::createPHPUnitFrameworkException( | ||
| 854 | 1, | ||
| 855 | 'array' | ||
| 856 | ); | ||
| 857 | } | ||
| 858 | |||
| 859 | static::assertThat( | ||
| 860 | $list, | ||
| 861 | new SSListContains( | ||
| 862 | $matches | ||
| 863 | ), | ||
| 864 | $message | ||
| 865 | ); | ||
| 866 | } | ||
| 867 | |||
| 868 | /** | ||
| 869 | * @param $matches | ||
| 870 | * @param $dataObjectSet | ||
| 871 | * @deprecated 4.0.0:5.0.0 Use assertListContains() instead | ||
| 872 | * | ||
| 873 | */ | ||
| 874 | public function assertDOSContains($matches, $dataObjectSet) | ||
| 875 |         { | ||
| 876 |             Deprecation::notice('5.0', 'Use assertListContains() instead'); | ||
| 877 | static::assertListContains($matches, $dataObjectSet); | ||
| 878 | } | ||
| 879 | |||
| 880 | /** | ||
| 881 | * Asserts that no items in a given list appear in the given dataobject list | ||
| 882 | * | ||
| 883 | * @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can | ||
| 884 | * either pass a single pattern or an array of patterns. | ||
| 885 |          * @param SS_List $list The {@link SS_List} to test. | ||
| 886 | * @param string $message | ||
| 887 | * | ||
| 888 | * Examples | ||
| 889 | * -------- | ||
| 890 | * Check that $members doesn't have an entry with Email = [email protected]: | ||
| 891 | * $this->assertListNotContains(['Email' => '[email protected]'], $members); | ||
| 892 | * | ||
| 893 | * Check that $members doesn't have entries with Email = [email protected] and with | ||
| 894 | * Email = [email protected]: | ||
| 895 | * $this->assertListNotContains([ | ||
| 896 | * ['Email' => '[email protected]'], | ||
| 897 | * ['Email' => '[email protected]'], | ||
| 898 | * ], $members); | ||
| 899 | */ | ||
| 900 | public static function assertListNotContains($matches, SS_List $list, $message = '') | ||
| 901 |         { | ||
| 902 |             if (!is_array($matches)) { | ||
| 903 | throw self::createPHPUnitFrameworkException( | ||
| 904 | 1, | ||
| 905 | 'array' | ||
| 906 | ); | ||
| 907 | } | ||
| 908 | |||
| 909 | $constraint = new LogicalNot( | ||
| 910 | new SSListContains( | ||
| 911 | $matches | ||
| 912 | ) | ||
| 913 | ); | ||
| 914 | |||
| 915 | static::assertThat( | ||
| 916 | $list, | ||
| 917 | $constraint, | ||
| 918 | $message | ||
| 919 | ); | ||
| 920 | } | ||
| 921 | |||
| 922 | /** | ||
| 923 | * @param $matches | ||
| 924 | * @param $dataObjectSet | ||
| 925 | * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead | ||
| 926 | * | ||
| 927 | */ | ||
| 928 | public static function assertNotDOSContains($matches, $dataObjectSet) | ||
| 929 |         { | ||
| 930 |             Deprecation::notice('5.0', 'Use assertListNotContains() instead'); | ||
| 931 | static::assertListNotContains($matches, $dataObjectSet); | ||
| 932 | } | ||
| 933 | |||
| 934 | /** | ||
| 935 |          * Assert that the given {@link SS_List} includes only DataObjects matching the given | ||
| 936 | * key-value pairs. Each match must correspond to 1 distinct record. | ||
| 937 | * | ||
| 938 | * Example | ||
| 939 | * -------- | ||
| 940 | * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't | ||
| 941 | * matter: | ||
| 942 | * $this->assertListEquals([ | ||
| 943 | * ['FirstName' =>'Sam', 'Surname' => 'Minnee'], | ||
| 944 | * ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], | ||
| 945 | * ], $members); | ||
| 946 | * | ||
| 947 | * @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can | ||
| 948 | * either pass a single pattern or an array of patterns. | ||
| 949 |          * @param mixed $list The {@link SS_List} to test. | ||
| 950 | * @param string $message | ||
| 951 | */ | ||
| 952 | public static function assertListEquals($matches, SS_List $list, $message = '') | ||
| 953 |         { | ||
| 954 |             if (!is_array($matches)) { | ||
| 955 | throw self::createPHPUnitFrameworkException( | ||
| 956 | 1, | ||
| 957 | 'array' | ||
| 958 | ); | ||
| 959 | } | ||
| 960 | |||
| 961 | static::assertThat( | ||
| 962 | $list, | ||
| 963 | new SSListContainsOnly( | ||
| 964 | $matches | ||
| 965 | ), | ||
| 966 | $message | ||
| 967 | ); | ||
| 968 | } | ||
| 969 | |||
| 970 | /** | ||
| 971 | * @param $matches | ||
| 972 | * @param SS_List $dataObjectSet | ||
| 973 | * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead | ||
| 974 | * | ||
| 975 | */ | ||
| 976 | public function assertDOSEquals($matches, $dataObjectSet) | ||
| 977 |         { | ||
| 978 |             Deprecation::notice('5.0', 'Use assertListEquals() instead'); | ||
| 979 | static::assertListEquals($matches, $dataObjectSet); | ||
| 980 | } | ||
| 981 | |||
| 982 | |||
| 983 | /** | ||
| 984 |          * Assert that the every record in the given {@link SS_List} matches the given key-value | ||
| 985 | * pairs. | ||
| 986 | * | ||
| 987 | * Example | ||
| 988 | * -------- | ||
| 989 | * Check that every entry in $members has a Status of 'Active': | ||
| 990 | * $this->assertListAllMatch(['Status' => 'Active'], $members); | ||
| 991 | * | ||
| 992 | * @param mixed $match The pattern to match. The pattern is a map of key-value pairs. | ||
| 993 |          * @param mixed $list The {@link SS_List} to test. | ||
| 994 | * @param string $message | ||
| 995 | */ | ||
| 996 | public static function assertListAllMatch($match, SS_List $list, $message = '') | ||
| 997 |         { | ||
| 998 |             if (!is_array($match)) { | ||
| 999 | throw self::createPHPUnitFrameworkException( | ||
| 1000 | 1, | ||
| 1001 | 'array' | ||
| 1002 | ); | ||
| 1003 | } | ||
| 1004 | |||
| 1005 | static::assertThat( | ||
| 1006 | $list, | ||
| 1007 | new SSListContainsOnlyMatchingItems( | ||
| 1008 | $match | ||
| 1009 | ), | ||
| 1010 | $message | ||
| 1011 | ); | ||
| 1012 | } | ||
| 1013 | |||
| 1014 | /** | ||
| 1015 | * @param $match | ||
| 1016 | * @param SS_List $dataObjectSet | ||
| 1017 | * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead | ||
| 1018 | * | ||
| 1019 | */ | ||
| 1020 | public function assertDOSAllMatch($match, SS_List $dataObjectSet) | ||
| 1021 |         { | ||
| 1022 |             Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); | ||
| 1023 | static::assertListAllMatch($match, $dataObjectSet); | ||
| 1024 | } | ||
| 1025 | |||
| 1026 | /** | ||
| 1027 | * Removes sequences of repeated whitespace characters from SQL queries | ||
| 1028 | * making them suitable for string comparison | ||
| 1029 | * | ||
| 1030 | * @param string $sql | ||
| 1031 | * @return string The cleaned and normalised SQL string | ||
| 1032 | */ | ||
| 1033 | protected static function normaliseSQL($sql) | ||
| 1034 |         { | ||
| 1035 |             return trim(preg_replace('/\s+/m', ' ', $sql)); | ||
| 1036 | } | ||
| 1037 | |||
| 1038 | /** | ||
| 1039 | * Asserts that two SQL queries are equivalent | ||
| 1040 | * | ||
| 1041 | * @param string $expectedSQL | ||
| 1042 | * @param string $actualSQL | ||
| 1043 | * @param string $message | ||
| 1044 | * @param float|int $delta | ||
| 1045 | * @param integer $maxDepth | ||
| 1046 | * @param boolean $canonicalize | ||
| 1047 | * @param boolean $ignoreCase | ||
| 1048 | */ | ||
| 1049 | public static function assertSQLEquals( | ||
| 1050 | $expectedSQL, | ||
| 1051 | $actualSQL, | ||
| 1052 | $message = '' | ||
| 1053 |         ) { | ||
| 1054 | // Normalise SQL queries to remove patterns of repeating whitespace | ||
| 1055 | $expectedSQL = static::normaliseSQL($expectedSQL); | ||
| 1056 | $actualSQL = static::normaliseSQL($actualSQL); | ||
| 1057 | |||
| 1058 | static::assertEquals($expectedSQL, $actualSQL, $message); | ||
| 1059 | } | ||
| 1060 | |||
| 1061 | /** | ||
| 1062 | * Asserts that a SQL query contains a SQL fragment | ||
| 1063 | * | ||
| 1064 | * @param string $needleSQL | ||
| 1065 | * @param string $haystackSQL | ||
| 1066 | * @param string $message | ||
| 1067 | * @param boolean $ignoreCase | ||
| 1068 | * @param boolean $checkForObjectIdentity | ||
| 1069 | */ | ||
| 1070 | public static function assertSQLContains( | ||
| 1071 | $needleSQL, | ||
| 1072 | $haystackSQL, | ||
| 1073 | $message = '', | ||
| 1074 | $ignoreCase = false, | ||
| 1075 | $checkForObjectIdentity = true | ||
| 1076 |         ) { | ||
| 1077 | $needleSQL = static::normaliseSQL($needleSQL); | ||
| 1078 | $haystackSQL = static::normaliseSQL($haystackSQL); | ||
| 1079 |             if (is_iterable($haystackSQL)) { | ||
| 1080 | /** @var iterable $iterableHaystackSQL */ | ||
| 1081 | $iterableHaystackSQL = $haystackSQL; | ||
| 1082 | static::assertContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity); | ||
| 1083 |             } else { | ||
| 1084 | static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); | ||
| 1085 | } | ||
| 1086 | } | ||
| 1087 | |||
| 1088 | /** | ||
| 1089 | * Asserts that a SQL query contains a SQL fragment | ||
| 1090 | * | ||
| 1091 | * @param string $needleSQL | ||
| 1092 | * @param string $haystackSQL | ||
| 1093 | * @param string $message | ||
| 1094 | * @param boolean $ignoreCase | ||
| 1095 | * @param boolean $checkForObjectIdentity | ||
| 1096 | */ | ||
| 1097 | public static function assertSQLNotContains( | ||
| 1098 | $needleSQL, | ||
| 1099 | $haystackSQL, | ||
| 1100 | $message = '', | ||
| 1101 | $ignoreCase = false, | ||
| 1102 | $checkForObjectIdentity = true | ||
| 1103 |         ) { | ||
| 1104 | $needleSQL = static::normaliseSQL($needleSQL); | ||
| 1105 | $haystackSQL = static::normaliseSQL($haystackSQL); | ||
| 1106 |             if (is_iterable($haystackSQL)) { | ||
| 1107 | /** @var iterable $iterableHaystackSQL */ | ||
| 1108 | $iterableHaystackSQL = $haystackSQL; | ||
| 1109 | static::assertNotContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity); | ||
| 1110 |             } else { | ||
| 1111 | static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase); | ||
| 1112 | } | ||
| 1113 | } | ||
| 1114 | |||
| 1115 | /** | ||
| 1116 | * Start test environment | ||
| 1117 | */ | ||
| 1118 | public static function start() | ||
| 1119 |         { | ||
| 1120 |             if (static::is_running_test()) { | ||
| 1121 | return; | ||
| 1122 | } | ||
| 1123 | |||
| 1124 | // Health check | ||
| 1125 |             if (InjectorLoader::inst()->countManifests()) { | ||
| 1126 |                 throw new LogicException('SapphireTest::start() cannot be called within another application'); | ||
| 1127 | } | ||
| 1128 | static::set_is_running_test(true); | ||
| 1129 | |||
| 1130 | // Test application | ||
| 1131 | $kernel = new TestKernel(BASE_PATH); | ||
| 1132 | |||
| 1133 |             if (class_exists(HTTPApplication::class)) { | ||
| 1134 | // Mock request | ||
| 1135 | $_SERVER['argv'] = ['vendor/bin/phpunit', '/']; | ||
| 1136 | $request = CLIRequestBuilder::createFromEnvironment(); | ||
| 1137 | |||
| 1138 | $app = new HTTPApplication($kernel); | ||
| 1139 |                 $flush = array_key_exists('flush', $request->getVars()); | ||
| 1140 | |||
| 1141 | // Custom application | ||
| 1142 |                 $res = $app->execute($request, function (HTTPRequest $request) { | ||
| 1143 | // Start session and execute | ||
| 1144 | $request->getSession()->init($request); | ||
| 1145 | |||
| 1146 | // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class | ||
| 1147 | // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) | ||
| 1148 | DataObject::reset(); | ||
| 1149 | |||
| 1150 | // Set dummy controller; | ||
| 1151 | $controller = Controller::create(); | ||
| 1152 | $controller->setRequest($request); | ||
| 1153 | $controller->pushCurrent(); | ||
| 1154 | $controller->doInit(); | ||
| 1155 | }, $flush); | ||
| 1156 | |||
| 1157 |                 if ($res && $res->isError()) { | ||
| 1158 | throw new LogicException($res->getBody()); | ||
| 1159 | } | ||
| 1160 |             } else { | ||
| 1161 | // Allow flush from the command line in the absence of HTTPApplication's special sauce | ||
| 1162 | $flush = false; | ||
| 1163 |                 foreach ($_SERVER['argv'] as $arg) { | ||
| 1164 |                     if (preg_match('/^(--)?flush(=1)?$/', $arg)) { | ||
| 1165 | $flush = true; | ||
| 1166 | } | ||
| 1167 | } | ||
| 1168 | $kernel->boot($flush); | ||
| 1169 | } | ||
| 1170 | |||
| 1171 | // Register state | ||
| 1172 | static::$state = SapphireTestState::singleton(); | ||
| 1173 | // Register temp DB holder | ||
| 1174 | static::tempDB(); | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | /** | ||
| 1178 | * Reset the testing database's schema, but only if it is active | ||
| 1179 | * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included | ||
| 1180 | * @param bool $forceCreate Force DB to be created if it doesn't exist | ||
| 1181 | */ | ||
| 1182 | public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false) | ||
| 1183 |         { | ||
| 1184 |             if (!static::$tempDB) { | ||
| 1185 | return; | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | // Check if DB is active before reset | ||
| 1189 |             if (!static::$tempDB->isUsed()) { | ||
| 1190 |                 if (!$forceCreate) { | ||
| 1191 | return; | ||
| 1192 | } | ||
| 1193 | static::$tempDB->build(); | ||
| 1194 | } | ||
| 1195 | $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; | ||
| 1196 | static::$tempDB->resetDBSchema((array)$extraDataObjects); | ||
| 1197 | } | ||
| 1198 | |||
| 1199 | /** | ||
| 1200 | * A wrapper for automatically performing callbacks as a user with a specific permission | ||
| 1201 | * | ||
| 1202 | * @param string|array $permCode | ||
| 1203 | * @param callable $callback | ||
| 1204 | * @return mixed | ||
| 1205 | */ | ||
| 1206 | public function actWithPermission($permCode, $callback) | ||
| 1207 |         { | ||
| 1208 | return Member::actAs($this->createMemberWithPermission($permCode), $callback); | ||
| 1209 | } | ||
| 1210 | |||
| 1211 | /** | ||
| 1212 | * Create Member and Group objects on demand with specific permission code | ||
| 1213 | * | ||
| 1214 | * @param string|array $permCode | ||
| 1215 | * @return Member | ||
| 1216 | */ | ||
| 1217 | protected function createMemberWithPermission($permCode) | ||
| 1218 |         { | ||
| 1219 |             if (is_array($permCode)) { | ||
| 1220 | $permArray = $permCode; | ||
| 1221 |                 $permCode = implode('.', $permCode); | ||
| 1222 |             } else { | ||
| 1223 | $permArray = [$permCode]; | ||
| 1224 | } | ||
| 1225 | |||
| 1226 | // Check cached member | ||
| 1227 |             if (isset($this->cache_generatedMembers[$permCode])) { | ||
| 1228 | $member = $this->cache_generatedMembers[$permCode]; | ||
| 1229 |             } else { | ||
| 1230 | // Generate group with these permissions | ||
| 1231 | $group = Group::create(); | ||
| 1232 | $group->Title = "$permCode group"; | ||
| 1233 | $group->write(); | ||
| 1234 | |||
| 1235 | // Create each individual permission | ||
| 1236 |                 foreach ($permArray as $permArrayItem) { | ||
| 1237 | $permission = Permission::create(); | ||
| 1238 | $permission->Code = $permArrayItem; | ||
| 1239 | $permission->write(); | ||
| 1240 | $group->Permissions()->add($permission); | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | $member = Member::get()->filter([ | ||
| 1244 | 'Email' => "[email protected]", | ||
| 1245 | ])->first(); | ||
| 1246 |                 if (!$member) { | ||
| 1247 | $member = Member::create(); | ||
| 1248 | } | ||
| 1249 | |||
| 1250 | $member->FirstName = $permCode; | ||
| 1251 | $member->Surname = 'User'; | ||
| 1252 | $member->Email = "[email protected]"; | ||
| 1253 | $member->write(); | ||
| 1254 | $group->Members()->add($member); | ||
| 1255 | |||
| 1256 | $this->cache_generatedMembers[$permCode] = $member; | ||
| 1257 | } | ||
| 1258 | return $member; | ||
| 1259 | } | ||
| 1260 | |||
| 1261 | /** | ||
| 1262 | * Create a member and group with the given permission code, and log in with it. | ||
| 1263 | * Returns the member ID. | ||
| 1264 | * | ||
| 1265 | * @param string|array $permCode Either a permission, or list of permissions | ||
| 1266 | * @return int Member ID | ||
| 1267 | */ | ||
| 1268 | public function logInWithPermission($permCode = 'ADMIN') | ||
| 1269 |         { | ||
| 1270 | $member = $this->createMemberWithPermission($permCode); | ||
| 1271 | $this->logInAs($member); | ||
| 1272 | return $member->ID; | ||
| 1273 | } | ||
| 1274 | |||
| 1275 | /** | ||
| 1276 | * Log in as the given member | ||
| 1277 | * | ||
| 1278 | * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in | ||
| 1279 | */ | ||
| 1280 | public function logInAs($member) | ||
| 1281 |         { | ||
| 1282 |             if (is_numeric($member)) { | ||
| 1283 | $member = DataObject::get_by_id(Member::class, $member); | ||
| 1284 |             } elseif (!is_object($member)) { | ||
| 1285 | $member = $this->objFromFixture(Member::class, $member); | ||
| 1286 | } | ||
| 1287 | Injector::inst()->get(IdentityStore::class)->logIn($member); | ||
| 1288 | } | ||
| 1289 | |||
| 1290 | /** | ||
| 1291 | * Log out the current user | ||
| 1292 | */ | ||
| 1293 | public function logOut() | ||
| 1294 |         { | ||
| 1295 | /** @var IdentityStore $store */ | ||
| 1296 | $store = Injector::inst()->get(IdentityStore::class); | ||
| 1297 | $store->logOut(); | ||
| 1298 | } | ||
| 1299 | |||
| 1300 | /** | ||
| 1301 | * Cache for logInWithPermission() | ||
| 1302 | */ | ||
| 1303 | protected $cache_generatedMembers = []; | ||
| 1304 | |||
| 1305 | /** | ||
| 1306 | * Test against a theme. | ||
| 1307 | * | ||
| 1308 | * @param string $themeBaseDir themes directory | ||
| 1309 | * @param string $theme Theme name | ||
| 1310 | * @param callable $callback | ||
| 1311 | * @throws Exception | ||
| 1312 | */ | ||
| 1313 | protected function useTestTheme($themeBaseDir, $theme, $callback) | ||
| 1314 |         { | ||
| 1315 | Config::nest(); | ||
| 1316 |             if (strpos($themeBaseDir, BASE_PATH) === 0) { | ||
| 1317 | $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); | ||
| 1318 | } | ||
| 1319 |             SSViewer::config()->update('theme_enabled', true); | ||
| 1320 | SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']); | ||
| 1321 | |||
| 1322 |             try { | ||
| 1323 | $callback(); | ||
| 1324 |             } finally { | ||
| 1325 | Config::unnest(); | ||
| 1326 | } | ||
| 1327 | } | ||
| 1328 | |||
| 1329 | /** | ||
| 1330 | * Get fixture paths for this test | ||
| 1331 | * | ||
| 1332 | * @return array List of paths | ||
| 1333 | */ | ||
| 1334 | protected function getFixturePaths() | ||
| 1335 |         { | ||
| 1336 | $fixtureFile = static::get_fixture_file(); | ||
| 1337 |             if (empty($fixtureFile)) { | ||
| 1338 | return []; | ||
| 1339 | } | ||
| 1340 | |||
| 1341 | $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; | ||
| 1342 | |||
| 1343 |             return array_map(function ($fixtureFilePath) { | ||
| 1344 | return $this->resolveFixturePath($fixtureFilePath); | ||
| 1345 | }, $fixtureFiles); | ||
| 1346 | } | ||
| 1347 | |||
| 1348 | /** | ||
| 1349 | * Return all extra objects to scaffold for this test | ||
| 1350 | * @return array | ||
| 1351 | */ | ||
| 1352 | public static function getExtraDataObjects() | ||
| 1355 | } | ||
| 1356 | |||
| 1357 | /** | ||
| 1358 | * Get additional controller classes to register routes for | ||
| 1359 | * | ||
| 1360 | * @return array | ||
| 1361 | */ | ||
| 1362 | public static function getExtraControllers() | ||
| 1363 |         { | ||
| 1364 | return static::$extra_controllers; | ||
| 1365 | } | ||
| 1366 | |||
| 1367 | /** | ||
| 1368 | * Map a fixture path to a physical file | ||
| 1369 | * | ||
| 1370 | * @param string $fixtureFilePath | ||
| 1371 | * @return string | ||
| 1372 | */ | ||
| 1373 | protected function resolveFixturePath($fixtureFilePath) | ||
| 1374 |         { | ||
| 1375 | // support loading via composer name path. | ||
| 1376 |             if (strpos($fixtureFilePath, ':') !== false) { | ||
| 1377 | return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); | ||
| 1378 | } | ||
| 1379 | |||
| 1380 | // Support fixture paths relative to the test class, rather than relative to webroot | ||
| 1381 | // String checking is faster than file_exists() calls. | ||
| 1382 | $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); | ||
| 1383 |             if ($resolvedPath) { | ||
| 1384 | return $resolvedPath; | ||
| 1385 | } | ||
| 1386 | |||
| 1387 | // Check if file exists relative to base dir | ||
| 1388 | $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); | ||
| 1389 |             if ($resolvedPath) { | ||
| 1390 | return $resolvedPath; | ||
| 1391 | } | ||
| 1392 | |||
| 1393 | return $fixtureFilePath; | ||
| 1394 | } | ||
| 1395 | |||
| 1396 | protected function setUpRoutes() | ||
| 1397 |         { | ||
| 1398 |             if (!class_exists(Director::class)) { | ||
| 1399 | return; | ||
| 1400 | } | ||
| 1401 | |||
| 1402 | // Get overridden routes | ||
| 1403 | $rules = $this->getExtraRoutes(); | ||
| 1404 | |||
| 1405 | // Add all other routes | ||
| 1406 |             foreach (Director::config()->uninherited('rules') as $route => $rule) { | ||
| 1407 |                 if (!isset($rules[$route])) { | ||
| 1408 | $rules[$route] = $rule; | ||
| 1409 | } | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | // Add default catch-all rule | ||
| 1413 | $rules['$Controller//$Action/$ID/$OtherID'] = '*'; | ||
| 1414 | |||
| 1415 | // Add controller-name auto-routing | ||
| 1416 |             Director::config()->set('rules', $rules); | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | /** | ||
| 1420 | * Get extra routes to merge into Director.rules | ||
| 1421 | * | ||
| 1422 | * @return array | ||
| 1423 | */ | ||
| 1424 | protected function getExtraRoutes() | ||
| 1434 | } | ||
| 1435 | |||
| 1436 | // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 === | ||
| 1437 | |||
| 1438 | /** | ||
| 1439 | * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory() | ||
| 1440 | * | ||
| 1441 | * @param $argument | ||
| 1442 | * @param $type | ||
| 1443 | * @param $value | ||
| 1444 | */ | ||
| 1445 | public static function createPHPUnitFrameworkException($argument, $type, $value = null) | ||
| 1446 |         { | ||
| 1447 | $stack = debug_backtrace(false); | ||
| 1448 | |||
| 1449 | return new PHPUnitFrameworkException( | ||
| 1450 | sprintf( | ||
| 1451 | 'Argument #%d%sof %s::%s() must be a %s', | ||
| 1452 | $argument, | ||
| 1453 |                     $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', | ||
| 1454 | $stack[1]['class'], | ||
| 1455 | $stack[1]['function'], | ||
| 1456 | $type | ||
| 1457 | ) | ||
| 1458 | ); | ||
| 1459 | } | ||
| 1460 | |||
| 1461 | /** | ||
| 1462 | * Returns the annotations for this test. | ||
| 1463 | * | ||
| 1464 | * @return array | ||
| 1465 | */ | ||
| 1466 | public function getAnnotations() | ||
| 1471 | ); | ||
| 1472 | } | ||
| 1473 | } | ||
| 1475 | 
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths