--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * The "civicrm/dev/fake-error" page is a mock to facilitate E2E testing of the error-reporting mechanism.
+ * Use this page to provoke common/representative errors.
+ *
+ * Of course, we don't want to permit arbitrary users to provoke arbitrary errors -- that could
+ * lead to noisy/confusing logs.
+ *
+ * This has two main modes:
+ *
+ * - If you give no parameters (or unsigned parameters), it simply says "Hello world".
+ * - If you give an authentic JWT with the claim `civi.fake-error`, then it will report
+ * one of the pre-canned error messages.
+ */
+class CRM_Core_Page_FakeError extends CRM_Core_Page {
+
+ public function run() {
+ try {
+ /** @var \Civi\Crypto\CryptoJwt $jwt */
+ $jwt = Civi::service('crypto.jwt');
+ $claims = $jwt->decode(CRM_Utils_Request::retrieve('token', 'String'));
+ }
+ catch (\Exception $e) {
+ $claims = [];
+ }
+
+ if (empty($claims['civi.fake-error'])) {
+ echo 'Hello world';
+ return;
+ }
+
+ switch ($claims['civi.fake-error']) {
+ case 'exception':
+ throw new \CRM_Core_Exception("This is a fake problem (exception).");
+
+ case 'fatal':
+ CRM_Core_Error::fatal('This is a fake problem (fatal).');
+ break;
+
+ case 'permission':
+ CRM_Utils_System::permissionDenied();
+ break;
+
+ default:
+ return 'Unrecognized error type.';
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+namespace E2E\Core;
+
+use Civi\Test\HttpTestTrait;
+
+/**
+ * Class ErrorTest
+ * @package E2E\Core
+ * @group e2e
+ *
+ * Check that errors are reported in a sensible way. In this context, we speak of a few common error types, eg
+ *
+ * - `fatal` -- ie `CRM_Core_Error::fatal("Some message")'
+ * - `exception` -- ie `throw new \Exception("Some message")'
+ * - `permission` -- ie `CRM_Utils_System::permissionDenied()`
+ */
+class ErrorTest extends \CiviEndToEndTestCase {
+
+ use HttpTestTrait;
+
+ /**
+ * FIXME: These represent pre-existing bugs.
+ *
+ * By default, these test scenarios do not run in CI.
+ * However, you can run them manually by setting env-var `FORCE_ALL=1`.
+ *
+ * @var string[]
+ */
+ protected $nonCompliant = [
+ // Format: "{$uf}_{$testFunc}_{$errorType}"
+ '/WordPress_testErrorStatus_(fatal|exception)/',
+ '/Drupal_testErrorChrome_(fatal|exception)/',
+ ];
+
+ public function getErrorTypes() {
+ return [
+ 'frontend_fatal' => ['frontend://civicrm/dev/fake-error', 'fatal'],
+ 'frontend_exception' => ['frontend://civicrm/dev/fake-error', 'exception'],
+ 'frontend_permission' => ['frontend://civicrm/dev/fake-error', 'permission'],
+ 'backend_fatal' => ['backend://civicrm/dev/fake-error', 'fatal'],
+ 'backend_exception' => ['backend://civicrm/dev/fake-error', 'exception'],
+ 'backend_permission' => ['backend://civicrm/dev/fake-error', 'permission'],
+ ];
+ }
+
+ /**
+ * When showing an error screen, does the basic message come through?
+ *
+ * @param string $url
+ * Ex: 'frontend://civicrm/dev/fake-error'
+ * @param string $errorType
+ * Ex: 'fatal' or 'exception'
+ * @dataProvider getErrorTypes
+ */
+ public function testErrorMessage(string $url, string $errorType) {
+ $this->skipIfNonCompliant(__FUNCTION__, $errorType);
+ $messages = [
+ 'fatal' => '/This is a fake problem \(fatal\)/',
+ 'exception' => '/This is a fake problem \(exception\)/',
+ 'permission' => '/(You do not have permission|You are not authorized to access)/',
+ ];
+ $response = $this->provokeError($url, $errorType);
+ $this->assertBodyRegexp($messages[$errorType] ?? 'Test error: Invalid error type', $response);
+ }
+
+ /**
+ * When showing an error screen, does the HTTP status indicate an error?
+ *
+ * @param string $url
+ * Ex: 'frontend://civicrm/dev/fake-error'
+ * @param string $errorType
+ * Ex: 'fatal' or 'exception'
+ * @dataProvider getErrorTypes
+ */
+ public function testErrorStatus(string $url, string $errorType) {
+ $this->skipIfNonCompliant(__FUNCTION__, $errorType);
+ $httpCodes = [
+ 'fatal' => 500,
+ 'exception' => 500,
+ 'permission' => 403,
+ ];
+ $response = $this->provokeError($url, $errorType);
+ $this->assertStatusCode($httpCodes[$errorType] ?? 'Test error: Invalid error type', $response);
+ }
+
+ /**
+ * @param string $url
+ * Ex: 'frontend://civicrm/dev/fake-error'
+ * @param string $errorType
+ * Ex: 'fatal' or 'exception'
+ * @dataProvider getErrorTypes
+ */
+ public function testErrorChrome(string $url, string $errorType) {
+ $this->skipIfNonCompliant(__FUNCTION__, $errorType);
+ $patterns = [
+ 'Backdrop' => '/body class=\".*not-logged-in/',
+ 'Drupal' => '/body class=\".*not-logged-in/',
+ 'Drupal8' => '/body class=\".*not-logged-in/',
+ 'WordPress' => '/ role=.navigation./',
+ ];
+ if (!isset($patterns[CIVICRM_UF])) {
+ $this->markTestIncomplete('testErrorChrome() cannot check for chrome on ' . CIVICRM_UF);
+ }
+
+ $response = $this->provokeError($url, $errorType);
+ $this->assertContentType('text/html', $response);
+ $this->assertBodyRegexp($patterns[CIVICRM_UF], $response, 'Body should have some chrome/decoration');
+ }
+
+ /**
+ * @param string $url
+ * @param string $errorType
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ protected function provokeError(string $url, string $errorType) {
+ $http = $this->createGuzzle(['http_errors' => FALSE]);
+ $jwt = \Civi::service('crypto.jwt')->encode([
+ 'exp' => \CRM_Utils_Time::time() + 3600,
+ 'civi.fake-error' => $errorType,
+ ]);
+ return $http->get("$url?token=$jwt");
+ }
+
+ protected function skipIfNonCompliant($func, $errorType) {
+ if (getenv('FORCE_ALL')) {
+ return;
+ }
+ $sig = implode('_', [CIVICRM_UF, $func, $errorType]);
+ foreach ($this->nonCompliant as $nonCompliant) {
+ if (preg_match($nonCompliant, $sig)) {
+ $this->markTestIncomplete("Skipping non-compliant scenario ($sig matches $nonCompliant)");
+ }
+ }
+ }
+
+}