SearchKit - Change @searchable annotation from boolean to option list
[civicrm-core.git] / tests / phpunit / api / v4 / Entity / ConformanceTest.php
CommitLineData
19b53e5b
C
1<?php
2
380f3545
TO
3/*
4 +--------------------------------------------------------------------+
7d61e75f 5 | Copyright CiviCRM LLC. All rights reserved. |
380f3545 6 | |
7d61e75f
TO
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 |
380f3545
TO
10 +--------------------------------------------------------------------+
11 */
12
13/**
14 *
15 * @package CRM
ca5cec67 16 * @copyright CiviCRM LLC https://civicrm.org/licensing
380f3545
TO
17 */
18
19
19b53e5b
C
20namespace api\v4\Entity;
21
22use Civi\Api4\Entity;
19b53e5b 23use api\v4\UnitTestCase;
eb378b8a 24use Civi\Api4\Utils\CoreUtil;
19b53e5b
C
25
26/**
27 * @group headless
28 */
29class ConformanceTest extends UnitTestCase {
30
30755fcb 31 use \api\v4\Traits\TableDropperTrait;
19b53e5b
C
32 use \api\v4\Traits\OptionCleanupTrait {
33 setUp as setUpOptionCleanup;
34 }
35
36 /**
37 * @var \api\v4\Service\TestCreationParameterProvider
38 */
39 protected $creationParamProvider;
40
41 /**
42 * Set up baseline for testing
43 */
0b49aa04 44 public function setUp(): void {
19b53e5b 45 $tablesToTruncate = [
96f09dda 46 'civicrm_case_type',
19b53e5b
C
47 'civicrm_custom_group',
48 'civicrm_custom_field',
49 'civicrm_group',
50 'civicrm_event',
51 'civicrm_participant',
52 ];
53 $this->dropByPrefix('civicrm_value_myfavorite');
54 $this->cleanup(['tablesToTruncate' => $tablesToTruncate]);
55 $this->setUpOptionCleanup();
baf63a69 56 $this->loadDataSet('CaseType');
19b53e5b
C
57 $this->loadDataSet('ConformanceTest');
58 $this->creationParamProvider = \Civi::container()->get('test.param_provider');
59 parent::setUp();
19b53e5b
C
60 }
61
5acc6183 62 /**
63 * Get entities to test.
64 *
8868b7fc
TO
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.
68 *
5acc6183 69 * @return array
70 *
71 * @throws \API_Exception
72 * @throws \Civi\API\Exception\UnauthorizedException
73 */
8868b7fc 74 public function getEntitiesHitech() {
d31fb4e3 75 // Ensure all components are enabled so their entities show up
a62d97f3
CW
76 foreach (array_keys(\CRM_Core_Component::getComponents()) as $component) {
77 \CRM_Core_BAO_ConfigSetting::enableComponent($component);
78 }
fe806431 79 return $this->toDataProviderArray(Entity::get(FALSE)->execute()->column('name'));
19b53e5b
C
80 }
81
82 /**
8868b7fc
TO
83 * Get entities to test.
84 *
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.
88 *
89 * @return array
19b53e5b 90 */
8868b7fc
TO
91 public function getEntitiesLotech() {
92 $manual['add'] = [];
93 $manual['remove'] = ['CustomValue'];
a62d97f3 94 $manual['transform'] = ['CiviCase' => 'Case'];
8868b7fc
TO
95
96 $scanned = [];
155ea29a 97 $srcDir = dirname(__DIR__, 5);
8868b7fc 98 foreach ((array) glob("$srcDir/Civi/Api4/*.php") as $name) {
a62d97f3
CW
99 $fileName = basename($name, '.php');
100 $scanned[] = $manual['transform'][$fileName] ?? $fileName;
19b53e5b 101 }
8868b7fc
TO
102
103 $names = array_diff(
104 array_unique(array_merge($scanned, $manual['add'])),
105 $manual['remove']
106 );
107
108 return $this->toDataProviderArray($names);
109 }
110
111 /**
112 * Ensure that "getEntitiesLotech()" (which is the 'dataProvider') is up to date
113 * with "getEntitiesHitech()" (which is a live feed available entities).
114 */
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().");
117 }
118
119 /**
120 * @param string $entity
121 * Ex: 'Contact'
155ea29a 122 *
8868b7fc 123 * @dataProvider getEntitiesLotech
155ea29a 124 *
125 * @throws \API_Exception
8868b7fc 126 */
155ea29a 127 public function testConformance($entity): void {
eb378b8a 128 $entityClass = CoreUtil::getApiClass($entity);
8868b7fc 129
30755fcb 130 $this->checkEntityInfo($entityClass);
8868b7fc
TO
131 $actions = $this->checkActions($entityClass);
132
133 // Go no further if it's not a CRUD entity
134 if (array_diff(['get', 'create', 'update', 'delete'], array_keys($actions))) {
30755fcb 135 $this->markTestSkipped("The API \"$entity\" does not implement CRUD actions");
8868b7fc
TO
136 }
137
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);
19b53e5b
C
147 }
148
149 /**
30755fcb
CW
150 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
151 */
155ea29a 152 protected function checkEntityInfo($entityClass): void {
30755fcb
CW
153 $info = $entityClass::getInfo();
154 $this->assertNotEmpty($info['name']);
155 $this->assertNotEmpty($info['title']);
9813ae79 156 $this->assertNotEmpty($info['title_plural']);
30755fcb
CW
157 $this->assertNotEmpty($info['type']);
158 $this->assertNotEmpty($info['description']);
aa998597 159 $this->assertContains($info['searchable'], ['primary', 'secondary', 'none']);
27d31a0f
CW
160 // Bridge must be between exactly 2 entities
161 if (in_array('EntityBridge', $info['type'], TRUE)) {
162 $this->assertCount(2, $info['bridge']);
163 }
30755fcb
CW
164 }
165
166 /**
167 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
5acc6183 168 * @param string $entity
155ea29a 169 *
170 * @throws \API_Exception
19b53e5b
C
171 */
172 protected function checkFields($entityClass, $entity) {
fe806431 173 $fields = $entityClass::getFields(FALSE)
19b53e5b
C
174 ->setIncludeCustom(FALSE)
175 ->execute()
176 ->indexBy('name');
177
178 $errMsg = sprintf('%s is missing required ID field', $entity);
179 $subset = ['data_type' => 'Integer'];
180
df347a8c
SL
181 $this->assertArrayHasKey('data_type', $fields['id'], $errMsg);
182 $this->assertEquals('Integer', $fields['id']['data_type']);
19b53e5b
C
183 }
184
185 /**
30755fcb 186 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
5acc6183 187 *
188 * @return array
155ea29a 189 *
190 * @throws \API_Exception
19b53e5b 191 */
155ea29a 192 protected function checkActions($entityClass): array {
fe806431 193 $actions = $entityClass::getActions(FALSE)
19b53e5b
C
194 ->execute()
195 ->indexBy('name');
196
197 $this->assertNotEmpty($actions);
198 return (array) $actions;
199 }
200
201 /**
202 * @param string $entity
203 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
204 *
205 * @return mixed
206 */
207 protected function checkCreation($entity, $entityClass) {
208 $requiredParams = $this->creationParamProvider->getRequired($entity);
209 $createResult = $entityClass::create()
210 ->setValues($requiredParams)
211 ->setCheckPermissions(FALSE)
212 ->execute()
213 ->first();
214
215 $this->assertArrayHasKey('id', $createResult, "create missing ID");
216 $id = $createResult['id'];
217
218 $this->assertGreaterThanOrEqual(1, $id, "$entity ID not positive");
219
220 return $id;
221 }
222
223 /**
224 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
225 * @param int $id
226 */
155ea29a 227 protected function checkUpdateFailsFromCreate($entityClass, $id): void {
19b53e5b
C
228 $exceptionThrown = '';
229 try {
fe806431 230 $entityClass::create(FALSE)
19b53e5b
C
231 ->addValue('id', $id)
232 ->execute();
233 }
234 catch (\API_Exception $e) {
235 $exceptionThrown = $e->getMessage();
236 }
df347a8c 237 $this->assertStringContainsString('id', $exceptionThrown);
19b53e5b
C
238 }
239
240 /**
241 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
242 * @param int $id
243 * @param string $entity
244 */
245 protected function checkGet($entityClass, $id, $entity) {
fe806431 246 $getResult = $entityClass::get(FALSE)
19b53e5b
C
247 ->addWhere('id', '=', $id)
248 ->execute();
249
250 $errMsg = sprintf('Failed to fetch a %s after creation', $entity);
251 $this->assertEquals($id, $getResult->first()['id'], $errMsg);
252 $this->assertEquals(1, $getResult->count(), $errMsg);
253 }
254
255 /**
256 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
257 * @param int $id
258 * @param string $entity
259 */
155ea29a 260 protected function checkGetCount($entityClass, $id, $entity): void {
fe806431 261 $getResult = $entityClass::get(FALSE)
19b53e5b
C
262 ->addWhere('id', '=', $id)
263 ->selectRowCount()
264 ->execute();
265 $errMsg = sprintf('%s getCount failed', $entity);
266 $this->assertEquals(1, $getResult->count(), $errMsg);
267
fe806431 268 $getResult = $entityClass::get(FALSE)
19b53e5b
C
269 ->selectRowCount()
270 ->execute();
271 $errMsg = sprintf('%s getCount failed', $entity);
272 $this->assertGreaterThanOrEqual(1, $getResult->count(), $errMsg);
273 }
274
275 /**
276 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
277 */
278 protected function checkDeleteWithNoId($entityClass) {
279 $exceptionThrown = '';
280 try {
281 $entityClass::delete()
282 ->execute();
283 }
284 catch (\API_Exception $e) {
285 $exceptionThrown = $e->getMessage();
286 }
df347a8c 287 $this->assertStringContainsString('required', $exceptionThrown);
19b53e5b
C
288 }
289
290 /**
291 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
292 */
293 protected function checkWrongParamType($entityClass) {
294 $exceptionThrown = '';
295 try {
296 $entityClass::get()
6764a9d3 297 ->setDebug('not a bool')
19b53e5b
C
298 ->execute();
299 }
300 catch (\API_Exception $e) {
301 $exceptionThrown = $e->getMessage();
302 }
df347a8c
SL
303 $this->assertStringContainsString('debug', $exceptionThrown);
304 $this->assertStringContainsString('type', $exceptionThrown);
19b53e5b
C
305 }
306
307 /**
308 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
309 * @param int $id
310 */
311 protected function checkDeletion($entityClass, $id) {
fe806431 312 $deleteResult = $entityClass::delete(FALSE)
19b53e5b
C
313 ->addWhere('id', '=', $id)
314 ->execute();
315
316 // should get back an array of deleted id
317 $this->assertEquals([['id' => $id]], (array) $deleteResult);
318 }
319
320 /**
321 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
322 * @param int $id
323 * @param string $entity
324 */
325 protected function checkPostDelete($entityClass, $id, $entity) {
fe806431 326 $getDeletedResult = $entityClass::get(FALSE)
19b53e5b
C
327 ->addWhere('id', '=', $id)
328 ->execute();
329
330 $errMsg = sprintf('Entity "%s" was not deleted', $entity);
331 $this->assertEquals(0, count($getDeletedResult), $errMsg);
332 }
333
8868b7fc
TO
334 /**
335 * @param array $names
336 * List of entity names.
337 * Ex: ['Foo', 'Bar']
338 * @return array
339 * List of data-provider arguments, one for each entity-name.
340 * Ex: ['Foo' => ['Foo'], 'Bar' => ['Bar']]
341 */
342 protected function toDataProviderArray($names) {
343 sort($names);
344
345 $result = [];
346 foreach ($names as $name) {
347 $result[$name] = [$name];
348 }
349 return $result;
350 }
351
19b53e5b 352}