APIv4 - Fix nonstandard camelCase of title_plural field
[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\Traits\TableDropperTrait;
24 use api\v4\UnitTestCase;
25
26 /**
27 * @group headless
28 */
29 class ConformanceTest extends UnitTestCase {
30
31 use 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() {
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 return $this->toDataProviderArray(Entity::get(FALSE)->execute()->column('name'));
74 }
75
76 /**
77 * Get entities to test.
78 *
79 * This is the low-tech list as generated by manual-overrides and direct inspection.
80 * It may be summoned at any time during PHPUnit lifecycle, but it may require
81 * occasional twiddling to give correct results.
82 *
83 * @return array
84 */
85 public function getEntitiesLotech() {
86 $manual['add'] = [];
87 $manual['remove'] = ['CustomValue'];
88
89 $scanned = [];
90 $srcDir = dirname(dirname(dirname(dirname(dirname(__DIR__)))));
91 foreach ((array) glob("$srcDir/Civi/Api4/*.php") as $name) {
92 $scanned[] = preg_replace('/\.php/', '', basename($name));
93 }
94
95 $names = array_diff(
96 array_unique(array_merge($scanned, $manual['add'])),
97 $manual['remove']
98 );
99
100 return $this->toDataProviderArray($names);
101 }
102
103 /**
104 * Ensure that "getEntitiesLotech()" (which is the 'dataProvider') is up to date
105 * with "getEntitiesHitech()" (which is a live feed available entities).
106 */
107 public function testEntitiesProvider() {
108 $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().");
109 }
110
111 /**
112 * @param string $entity
113 * Ex: 'Contact'
114 * @dataProvider getEntitiesLotech
115 */
116 public function testConformance($entity) {
117 $entityClass = 'Civi\Api4\\' . $entity;
118
119 $this->checkEntityInfo($entityClass);
120 $actions = $this->checkActions($entityClass);
121
122 // Go no further if it's not a CRUD entity
123 if (array_diff(['get', 'create', 'update', 'delete'], array_keys($actions))) {
124 $this->markTestSkipped("The entity \"$entity\" does not ");
125 return;
126 }
127
128 $this->checkFields($entityClass, $entity);
129 $id = $this->checkCreation($entity, $entityClass);
130 $this->checkGet($entityClass, $id, $entity);
131 $this->checkGetCount($entityClass, $id, $entity);
132 $this->checkUpdateFailsFromCreate($entityClass, $id);
133 $this->checkWrongParamType($entityClass);
134 $this->checkDeleteWithNoId($entityClass);
135 $this->checkDeletion($entityClass, $id);
136 $this->checkPostDelete($entityClass, $id, $entity);
137 }
138
139 /**
140 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
141 */
142 protected function checkEntityInfo($entityClass) {
143 $info = $entityClass::getInfo();
144 $this->assertNotEmpty($info['name']);
145 $this->assertNotEmpty($info['title']);
146 $this->assertNotEmpty($info['title_plural']);
147 $this->assertNotEmpty($info['type']);
148 $this->assertNotEmpty($info['description']);
149 }
150
151 /**
152 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
153 * @param string $entity
154 */
155 protected function checkFields($entityClass, $entity) {
156 $fields = $entityClass::getFields(FALSE)
157 ->setIncludeCustom(FALSE)
158 ->execute()
159 ->indexBy('name');
160
161 $errMsg = sprintf('%s is missing required ID field', $entity);
162 $subset = ['data_type' => 'Integer'];
163
164 $this->assertArraySubset($subset, $fields['id'], $errMsg);
165 }
166
167 /**
168 * @param string $entityClass
169 *
170 * @return array
171 */
172 protected function checkActions($entityClass) {
173 $actions = $entityClass::getActions(FALSE)
174 ->execute()
175 ->indexBy('name');
176
177 $this->assertNotEmpty($actions);
178 return (array) $actions;
179 }
180
181 /**
182 * @param string $entity
183 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
184 *
185 * @return mixed
186 */
187 protected function checkCreation($entity, $entityClass) {
188 $requiredParams = $this->creationParamProvider->getRequired($entity);
189 $createResult = $entityClass::create()
190 ->setValues($requiredParams)
191 ->setCheckPermissions(FALSE)
192 ->execute()
193 ->first();
194
195 $this->assertArrayHasKey('id', $createResult, "create missing ID");
196 $id = $createResult['id'];
197
198 $this->assertGreaterThanOrEqual(1, $id, "$entity ID not positive");
199
200 return $id;
201 }
202
203 /**
204 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
205 * @param int $id
206 */
207 protected function checkUpdateFailsFromCreate($entityClass, $id) {
208 $exceptionThrown = '';
209 try {
210 $entityClass::create(FALSE)
211 ->addValue('id', $id)
212 ->execute();
213 }
214 catch (\API_Exception $e) {
215 $exceptionThrown = $e->getMessage();
216 }
217 $this->assertContains('id', $exceptionThrown);
218 }
219
220 /**
221 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
222 * @param int $id
223 * @param string $entity
224 */
225 protected function checkGet($entityClass, $id, $entity) {
226 $getResult = $entityClass::get(FALSE)
227 ->addWhere('id', '=', $id)
228 ->execute();
229
230 $errMsg = sprintf('Failed to fetch a %s after creation', $entity);
231 $this->assertEquals($id, $getResult->first()['id'], $errMsg);
232 $this->assertEquals(1, $getResult->count(), $errMsg);
233 }
234
235 /**
236 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
237 * @param int $id
238 * @param string $entity
239 */
240 protected function checkGetCount($entityClass, $id, $entity) {
241 $getResult = $entityClass::get(FALSE)
242 ->addWhere('id', '=', $id)
243 ->selectRowCount()
244 ->execute();
245 $errMsg = sprintf('%s getCount failed', $entity);
246 $this->assertEquals(1, $getResult->count(), $errMsg);
247
248 $getResult = $entityClass::get(FALSE)
249 ->selectRowCount()
250 ->execute();
251 $errMsg = sprintf('%s getCount failed', $entity);
252 $this->assertGreaterThanOrEqual(1, $getResult->count(), $errMsg);
253 }
254
255 /**
256 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
257 */
258 protected function checkDeleteWithNoId($entityClass) {
259 $exceptionThrown = '';
260 try {
261 $entityClass::delete()
262 ->execute();
263 }
264 catch (\API_Exception $e) {
265 $exceptionThrown = $e->getMessage();
266 }
267 $this->assertContains('required', $exceptionThrown);
268 }
269
270 /**
271 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
272 */
273 protected function checkWrongParamType($entityClass) {
274 $exceptionThrown = '';
275 try {
276 $entityClass::get()
277 ->setDebug('not a bool')
278 ->execute();
279 }
280 catch (\API_Exception $e) {
281 $exceptionThrown = $e->getMessage();
282 }
283 $this->assertContains('debug', $exceptionThrown);
284 $this->assertContains('type', $exceptionThrown);
285 }
286
287 /**
288 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
289 * @param int $id
290 */
291 protected function checkDeletion($entityClass, $id) {
292 $deleteResult = $entityClass::delete(FALSE)
293 ->addWhere('id', '=', $id)
294 ->execute();
295
296 // should get back an array of deleted id
297 $this->assertEquals([['id' => $id]], (array) $deleteResult);
298 }
299
300 /**
301 * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
302 * @param int $id
303 * @param string $entity
304 */
305 protected function checkPostDelete($entityClass, $id, $entity) {
306 $getDeletedResult = $entityClass::get(FALSE)
307 ->addWhere('id', '=', $id)
308 ->execute();
309
310 $errMsg = sprintf('Entity "%s" was not deleted', $entity);
311 $this->assertEquals(0, count($getDeletedResult), $errMsg);
312 }
313
314 /**
315 * @param array $names
316 * List of entity names.
317 * Ex: ['Foo', 'Bar']
318 * @return array
319 * List of data-provider arguments, one for each entity-name.
320 * Ex: ['Foo' => ['Foo'], 'Bar' => ['Bar']]
321 */
322 protected function toDataProviderArray($names) {
323 sort($names);
324
325 $result = [];
326 foreach ($names as $name) {
327 $result[$name] = [$name];
328 }
329 return $result;
330 }
331
332 }