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