Add ValidateValuesTest
authorTim Otten <totten@civicrm.org>
Thu, 20 May 2021 06:17:25 +0000 (23:17 -0700)
committerTim Otten <totten@civicrm.org>
Mon, 7 Jun 2021 03:18:52 +0000 (20:18 -0700)
tests/phpunit/api/v4/Entity/ValidateValuesTest.php [new file with mode: 0644]

diff --git a/tests/phpunit/api/v4/Entity/ValidateValuesTest.php b/tests/phpunit/api/v4/Entity/ValidateValuesTest.php
new file mode 100644 (file)
index 0000000..3cc1a83
--- /dev/null
@@ -0,0 +1,214 @@
+<?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       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+namespace api\v4\Entity;
+
+use Civi\Api4\Contact;
+use api\v4\UnitTestCase;
+use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Assert that 'validateValues' runs during
+ *
+ * @group headless
+ */
+class ValidateValuesTest extends UnitTestCase implements TransactionalInterface {
+
+  private $lastValidator;
+
+  protected function setUp(): void {
+    $this->lastValidator = NULL;
+    parent::setUp();
+  }
+
+  protected function tearDown(): void {
+    $this->setValidator(NULL);
+    parent::tearDown();
+  }
+
+  /**
+   * Fire ValidateValuesEvent several times - and ensure it conveys the
+   * expected data.
+   *
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public function testHookData() {
+    $hookCount = 0;
+
+    // Step 1: `create()` a record for Ms. Alice Alison.
+    $this->setValidator(function (ValidateValuesEvent $e) use (&$hookCount) {
+      $this->assertWellFormedEvent($e);
+      if ($e->getEntityName() !== 'Contact') {
+        return;
+      }
+
+      $hookCount++;
+      $this->assertEquals('create', $e->getActionName());
+      $this->assertEquals('Alice', $e->records[0]['first_name']);
+      $this->assertEquals('Alison', $e->records[0]['last_name']);
+
+      $this->assertFalse($e->diffs->isLoaded());
+      $this->assertEquals(1, count($e->diffs));
+      $this->assertEquals(NULL, $e->diffs[0]['old']);
+      $this->assertEquals('Alice', $e->diffs[0]['new']['first_name']);
+      $this->assertEquals('Alison', $e->diffs[0]['new']['last_name']);
+
+    });
+    $created = Contact::create()->setValues([
+      'contact_type' => 'Individual',
+      'first_name' => 'Alice',
+      'last_name' => 'Alison',
+    ])->execute()->single();
+    $this->assertTrue(is_numeric($created['id']));
+    $this->assertEquals(1, $hookCount);
+
+    // Step 2: `save()` a couple records for Ms. Alice Alison and Mr. Bob Bobmom.
+    $this->setValidator(function (ValidateValuesEvent $e) use (&$hookCount) {
+      $this->assertWellFormedEvent($e);
+      if ($e->getEntityName() !== 'Contact') {
+        return;
+      }
+
+      $hookCount++;
+      $this->assertEquals('save', $e->getActionName());
+      $this->assertEquals('Alicia', $e->records[0]['first_name']);
+      $this->assertEquals('Bob', $e->records[1]['first_name']);
+
+      $this->assertFalse($e->diffs->isLoaded());
+      $this->assertEquals(2, count($e->diffs));
+      $this->assertEquals('Alice', $e->diffs[0]['old']['first_name']);
+      $this->assertEquals('Alicia', $e->diffs[0]['new']['first_name']);
+      $this->assertEquals(NULL, $e->diffs[1]['old']);
+      $this->assertEquals('Bob', $e->diffs[1]['new']['first_name']);
+
+    });
+    $saved = Contact::save()->setRecords([
+      ['id' => $created['id'], 'first_name' => 'Alicia'],
+      ['contact_type' => 'Individual', 'first_name' => 'Bob', 'last_name' => 'Bobmom'],
+    ])->execute();
+    $this->assertEquals(2, $saved->count());
+    $this->assertEquals(2, $hookCount);
+
+    // Step 3: `update()` a record for Mr. Bob Bobmom
+    $this->setValidator(function (ValidateValuesEvent $e) use (&$hookCount) {
+      $this->assertWellFormedEvent($e);
+      if ($e->getEntityName() !== 'Contact') {
+        return;
+      }
+
+      $hookCount++;
+      $this->assertEquals('update', $e->getActionName());
+      $this->assertEquals('Bobby', $e->records[0]['first_name']);
+
+      $this->assertFalse($e->diffs->isLoaded());
+      $this->assertEquals(1, count($e->diffs));
+      $this->assertEquals('Bob', $e->diffs[0]['old']['first_name']);
+      $this->assertEquals('Bobmom', $e->diffs[0]['old']['last_name']);
+      $this->assertEquals('Bobby', $e->diffs[0]['new']['first_name']);
+    });
+    $updated = Contact::update()
+      ->setValues(['first_name' => 'Bobby'])
+      ->addWhere('last_name', '=', 'Bobmom')
+      ->execute();
+    $this->assertEquals(1, $updated->count());
+    $this->assertEquals(3, $hookCount);
+  }
+
+  public function testRaiseError() {
+    $this->setValidator(function (ValidateValuesEvent $e) use (&$hookCount) {
+      $this->assertWellFormedEvent($e);
+      if ($e->getEntityName() !== 'Contact') {
+        return;
+      }
+
+      foreach ($e->records as $k => $record) {
+        $e->addError($k, 'first_name', 'not-namey-enough', ts('The first name is not sufficiently namey.'));
+        $e->addError($k, ['first_name', 'last_name'], 'tongue-twister', ts('When the names are put together, they become a tongue twister.'));
+        $e->errors[] = [
+          'record' => $k,
+          'fields' => ['last_name'],
+          'name' => 'misspelled',
+          'message' => ts('I disagree with the spelling of your name.'),
+        ];
+      }
+
+      $hookCount++;
+    });
+
+    try {
+      Contact::create()->setValues([
+        'contact_type' => 'Individual',
+        'first_name' => 'Alice',
+        'last_name' => 'Alison',
+      ])->execute();
+      $this->fail('Expected an exception due to validation error');
+    }
+    catch (\API_Exception $e) {
+      $this->assertEquals(1, $hookCount);
+      $this->assertRegExp(';not sufficiently namey;', $e->getMessage());
+      $this->assertRegExp(';tongue twister;', $e->getMessage());
+      $this->assertRegExp(';disagree with the spelling;', $e->getMessage());
+    }
+  }
+
+  /**
+   * Add/replace the validator.
+   *
+   * @param callable $func
+   */
+  protected function setValidator($func) {
+    $dispatcher = \Civi::dispatcher();
+    if ($this->lastValidator) {
+      $dispatcher->removeListener('civi.api4.validate', $this->lastValidator);
+    }
+    if ($func) {
+      $dispatcher->addListener('civi.api4.validate', $func);
+    }
+    $this->lastValidator = $func;
+  }
+
+  protected function assertWellFormedEvent(ValidateValuesEvent $e) {
+    $this->assertRegExp('/Contact/', $e->getEntityName());
+    $this->assertRegExp('/create|save|update/', $e->getActionName());
+    $this->assertTrue(count($e->records) > 0);
+    foreach ($e->records as $record) {
+      $this->assertWellFormedFields($record);
+    }
+
+    // We want to let the main test do some assertions on the lazy-array, so we'll peek a clone.
+    $peekAtDiffs = clone $e->diffs;
+    $this->assertTrue(count($peekAtDiffs) > 0);
+    foreach ($peekAtDiffs as $diff) {
+      $this->assertTrue(is_array($diff['new']));
+      $this->assertWellFormedFields($diff['new']);
+      if ($diff['old'] !== NULL) {
+        $this->assertTrue(is_array($diff['old']));
+        $this->assertWellFormedFields($diff['old']);
+      }
+    }
+  }
+
+  protected function assertWellFormedFields($record) {
+    foreach ($record as $field => $value) {
+      $this->assertRegExp('/^[a-zA-Z0-9_]+$/', $field);
+    }
+  }
+
+}