4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
20 namespace api\v
4\Entity
;
23 use api\v
4\UnitTestCase
;
24 use Civi\Api4\Utils\CoreUtil
;
29 class ConformanceTest
extends UnitTestCase
{
31 use \api\v
4\Traits\TableDropperTrait
;
32 use \api\v
4\Traits\OptionCleanupTrait
{
33 setUp
as setUpOptionCleanup
;
37 * @var \api\v4\Service\TestCreationParameterProvider
39 protected $creationParamProvider;
42 * Set up baseline for testing
44 public function setUp(): void
{
47 'civicrm_custom_group',
48 'civicrm_custom_field',
51 'civicrm_participant',
53 $this->dropByPrefix('civicrm_value_myfavorite');
54 $this->cleanup(['tablesToTruncate' => $tablesToTruncate]);
55 $this->setUpOptionCleanup();
56 $this->loadDataSet('CaseType');
57 $this->loadDataSet('ConformanceTest');
58 $this->creationParamProvider
= \Civi
::container()->get('test.param_provider');
63 * Get entities to test.
65 * This is the hi-tech list as generated via Civi's runtime services. It
66 * is canonical, but relies on services that may not be available during
67 * early parts of PHPUnit lifecycle.
71 * @throws \API_Exception
72 * @throws \Civi\API\Exception\UnauthorizedException
74 public function getEntitiesHitech() {
75 // Ensure all components are enabled so their entities show up
76 foreach (array_keys(\CRM_Core_Component
::getComponents()) as $component) {
77 \CRM_Core_BAO_ConfigSetting
::enableComponent($component);
79 return $this->toDataProviderArray(Entity
::get(FALSE)->execute()->column('name'));
83 * Get entities to test.
85 * This is the low-tech list as generated by manual-overrides and direct inspection.
86 * It may be summoned at any time during PHPUnit lifecycle, but it may require
87 * occasional twiddling to give correct results.
91 public function getEntitiesLotech() {
93 $manual['remove'] = ['CustomValue'];
94 $manual['transform'] = ['CiviCase' => 'Case'];
97 $srcDir = dirname(__DIR__
, 5);
98 foreach ((array) glob("$srcDir/Civi/Api4/*.php") as $name) {
99 $fileName = basename($name, '.php');
100 $scanned[] = $manual['transform'][$fileName] ??
$fileName;
104 array_unique(array_merge($scanned, $manual['add'])),
108 return $this->toDataProviderArray($names);
112 * Ensure that "getEntitiesLotech()" (which is the 'dataProvider') is up to date
113 * with "getEntitiesHitech()" (which is a live feed available entities).
115 public function testEntitiesProvider() {
116 $this->assertEquals($this->getEntitiesHitech(), $this->getEntitiesLotech(), "The lo-tech list of entities does not match the hi-tech list. You probably need to update getEntitiesLotech().");
120 * @param string $entity
123 * @dataProvider getEntitiesLotech
125 * @throws \API_Exception
127 public function testConformance($entity): void
{
128 $entityClass = CoreUtil
::getApiClass($entity);
130 $this->checkEntityInfo($entityClass);
131 $actions = $this->checkActions($entityClass);
133 // Go no further if it's not a CRUD entity
134 if (array_diff(['get', 'create', 'update', 'delete'], array_keys($actions))) {
135 $this->markTestSkipped("The API \"$entity\" does not implement CRUD actions");
138 $this->checkFields($entityClass, $entity);
139 $id = $this->checkCreation($entity, $entityClass);
140 $this->checkGet($entityClass, $id, $entity);
141 $this->checkGetCount($entityClass, $id, $entity);
142 $this->checkUpdateFailsFromCreate($entityClass, $id);
143 $this->checkWrongParamType($entityClass);
144 $this->checkDeleteWithNoId($entityClass);
145 $this->checkDeletion($entityClass, $id);
146 $this->checkPostDelete($entityClass, $id, $entity);
150 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
152 protected function checkEntityInfo($entityClass): void
{
153 $info = $entityClass::getInfo();
154 $this->assertNotEmpty($info['name']);
155 $this->assertNotEmpty($info['title']);
156 $this->assertNotEmpty($info['title_plural']);
157 $this->assertNotEmpty($info['type']);
158 $this->assertNotEmpty($info['description']);
159 // Bridge must be between exactly 2 entities
160 if (in_array('EntityBridge', $info['type'], TRUE)) {
161 $this->assertCount(2, $info['bridge']);
166 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
167 * @param string $entity
169 * @throws \API_Exception
171 protected function checkFields($entityClass, $entity) {
172 $fields = $entityClass::getFields(FALSE)
173 ->setIncludeCustom(FALSE)
177 $errMsg = sprintf('%s is missing required ID field', $entity);
178 $subset = ['data_type' => 'Integer'];
180 $this->assertArrayHasKey('data_type', $fields['id'], $errMsg);
181 $this->assertEquals('Integer', $fields['id']['data_type']);
185 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
189 * @throws \API_Exception
191 protected function checkActions($entityClass): array {
192 $actions = $entityClass::getActions(FALSE)
196 $this->assertNotEmpty($actions);
197 return (array) $actions;
201 * @param string $entity
202 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
206 protected function checkCreation($entity, $entityClass) {
207 $requiredParams = $this->creationParamProvider
->getRequired($entity);
208 $createResult = $entityClass::create()
209 ->setValues($requiredParams)
210 ->setCheckPermissions(FALSE)
214 $this->assertArrayHasKey('id', $createResult, "create missing ID");
215 $id = $createResult['id'];
217 $this->assertGreaterThanOrEqual(1, $id, "$entity ID not positive");
223 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
226 protected function checkUpdateFailsFromCreate($entityClass, $id): void
{
227 $exceptionThrown = '';
229 $entityClass::create(FALSE)
230 ->addValue('id', $id)
233 catch (\API_Exception
$e) {
234 $exceptionThrown = $e->getMessage();
236 $this->assertStringContainsString('id', $exceptionThrown);
240 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
242 * @param string $entity
244 protected function checkGet($entityClass, $id, $entity) {
245 $getResult = $entityClass::get(FALSE)
246 ->addWhere('id', '=', $id)
249 $errMsg = sprintf('Failed to fetch a %s after creation', $entity);
250 $this->assertEquals($id, $getResult->first()['id'], $errMsg);
251 $this->assertEquals(1, $getResult->count(), $errMsg);
255 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
257 * @param string $entity
259 protected function checkGetCount($entityClass, $id, $entity): void
{
260 $getResult = $entityClass::get(FALSE)
261 ->addWhere('id', '=', $id)
264 $errMsg = sprintf('%s getCount failed', $entity);
265 $this->assertEquals(1, $getResult->count(), $errMsg);
267 $getResult = $entityClass::get(FALSE)
270 $errMsg = sprintf('%s getCount failed', $entity);
271 $this->assertGreaterThanOrEqual(1, $getResult->count(), $errMsg);
275 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
277 protected function checkDeleteWithNoId($entityClass) {
278 $exceptionThrown = '';
280 $entityClass::delete()
283 catch (\API_Exception
$e) {
284 $exceptionThrown = $e->getMessage();
286 $this->assertStringContainsString('required', $exceptionThrown);
290 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
292 protected function checkWrongParamType($entityClass) {
293 $exceptionThrown = '';
296 ->setDebug('not a bool')
299 catch (\API_Exception
$e) {
300 $exceptionThrown = $e->getMessage();
302 $this->assertStringContainsString('debug', $exceptionThrown);
303 $this->assertStringContainsString('type', $exceptionThrown);
307 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
310 protected function checkDeletion($entityClass, $id) {
311 $deleteResult = $entityClass::delete(FALSE)
312 ->addWhere('id', '=', $id)
315 // should get back an array of deleted id
316 $this->assertEquals([['id' => $id]], (array) $deleteResult);
320 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
322 * @param string $entity
324 protected function checkPostDelete($entityClass, $id, $entity) {
325 $getDeletedResult = $entityClass::get(FALSE)
326 ->addWhere('id', '=', $id)
329 $errMsg = sprintf('Entity "%s" was not deleted', $entity);
330 $this->assertEquals(0, count($getDeletedResult), $errMsg);
334 * @param array $names
335 * List of entity names.
338 * List of data-provider arguments, one for each entity-name.
339 * Ex: ['Foo' => ['Foo'], 'Bar' => ['Bar']]
341 protected function toDataProviderArray($names) {
345 foreach ($names as $name) {
346 $result[$name] = [$name];