Add is empty filter to search / api
[civicrm-core.git] / tests / phpunit / api / v4 / Action / BasicActionsTest.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\Action;
21
22use api\v4\UnitTestCase;
23use Civi\Api4\MockBasicEntity;
24
25/**
26 * @group headless
27 */
28class BasicActionsTest extends UnitTestCase {
29
c0e68893 30 private function replaceRecords(&$records) {
31 MockBasicEntity::delete()->addWhere('id', '>', 0)->execute();
32 foreach ($records as &$record) {
33 $record['id'] = MockBasicEntity::create()->setValues($record)->execute()->first()['id'];
34 }
35 }
36
19b53e5b
C
37 public function testCrud() {
38 MockBasicEntity::delete()->addWhere('id', '>', 0)->execute();
39
40 $id1 = MockBasicEntity::create()->addValue('foo', 'one')->execute()->first()['id'];
41
42 $result = MockBasicEntity::get()->execute();
43 $this->assertCount(1, $result);
44
45 $id2 = MockBasicEntity::create()->addValue('foo', 'two')->execute()->first()['id'];
46
47 $result = MockBasicEntity::get()->selectRowCount()->execute();
48 $this->assertEquals(2, $result->count());
49
50 MockBasicEntity::update()->addWhere('id', '=', $id2)->addValue('foo', 'new')->execute();
51
52 $result = MockBasicEntity::get()->addOrderBy('id', 'DESC')->setLimit(1)->execute();
651c4c95
CW
53 // The object's count() method will account for all results, ignoring limit, while the array results are limited
54 $this->assertCount(2, $result);
55 $this->assertCount(1, (array) $result);
19b53e5b
C
56 $this->assertEquals('new', $result->first()['foo']);
57
58 $result = MockBasicEntity::save()
961e974c
CW
59 ->addRecord(['id' => $id1, 'foo' => 'one updated', 'weight' => '5'])
60 ->addRecord(['id' => $id2, 'group:label' => 'Second'])
19b53e5b
C
61 ->addRecord(['foo' => 'three'])
62 ->addDefault('color', 'pink')
63 ->setReload(TRUE)
64 ->execute()
65 ->indexBy('id');
66
961e974c 67 $this->assertTrue(5 === $result[$id1]['weight']);
19b53e5b 68 $this->assertEquals('new', $result[$id2]['foo']);
961e974c 69 $this->assertEquals('two', $result[$id2]['group']);
19b53e5b
C
70 $this->assertEquals('three', $result->last()['foo']);
71 $this->assertCount(3, $result);
72 foreach ($result as $item) {
73 $this->assertEquals('pink', $item['color']);
74 }
75
961e974c
CW
76 $ent1 = MockBasicEntity::get()->addWhere('id', '=', $id1)->execute()->first();
77 $this->assertEquals('one updated', $ent1['foo']);
78 $this->assertFalse(isset($ent1['group:label']));
79
80 $ent2 = MockBasicEntity::get()->addWhere('group:label', '=', 'Second')->addSelect('group:label', 'group')->execute()->first();
81 $this->assertEquals('two', $ent2['group']);
82 $this->assertEquals('Second', $ent2['group:label']);
83 // We didn't select this
84 $this->assertFalse(isset($ent2['group:name']));
85
86 // With no SELECT, all fields should be returned but not suffixy stuff like group:name
87 $ent2 = MockBasicEntity::get()->addWhere('group:label', '=', 'Second')->execute()->first();
88 $this->assertEquals('two', $ent2['group']);
89 $this->assertFalse(isset($ent2['group:name']));
90 // This one wasn't selected but did get used by the WHERE clause; ensure it isn't returned
91 $this->assertFalse(isset($ent2['group:label']));
19b53e5b
C
92
93 MockBasicEntity::delete()->addWhere('id', '=', $id2);
94 $result = MockBasicEntity::get()->execute();
95 $this->assertEquals('one updated', $result->first()['foo']);
96 }
97
98 public function testReplace() {
19b53e5b
C
99 $objects = [
100 ['group' => 'one', 'color' => 'red'],
101 ['group' => 'one', 'color' => 'blue'],
102 ['group' => 'one', 'color' => 'green'],
103 ['group' => 'two', 'color' => 'orange'],
104 ];
105
c0e68893 106 $this->replaceRecords($objects);
19b53e5b
C
107
108 // Keep red, change blue, delete green, and add yellow
109 $replacements = [
110 ['color' => 'red', 'id' => $objects[0]['id']],
111 ['color' => 'not blue', 'id' => $objects[1]['id']],
112 ['color' => 'yellow'],
113 ];
114
115 MockBasicEntity::replace()->addWhere('group', '=', 'one')->setRecords($replacements)->execute();
116
117 $newObjects = MockBasicEntity::get()->addOrderBy('id', 'DESC')->execute()->indexBy('id');
118
119 $this->assertCount(4, $newObjects);
120
121 $this->assertEquals('yellow', $newObjects->first()['color']);
122
123 $this->assertEquals('not blue', $newObjects[$objects[1]['id']]['color']);
124
125 // Ensure group two hasn't been altered
126 $this->assertEquals('orange', $newObjects[$objects[3]['id']]['color']);
127 $this->assertEquals('two', $newObjects[$objects[3]['id']]['group']);
128 }
129
130 public function testBatchFrobnicate() {
19b53e5b
C
131 $objects = [
132 ['group' => 'one', 'color' => 'red', 'number' => 10],
133 ['group' => 'one', 'color' => 'blue', 'number' => 20],
134 ['group' => 'one', 'color' => 'green', 'number' => 30],
135 ['group' => 'two', 'color' => 'blue', 'number' => 40],
136 ];
c0e68893 137 $this->replaceRecords($objects);
19b53e5b
C
138
139 $result = MockBasicEntity::batchFrobnicate()->addWhere('color', '=', 'blue')->execute();
140 $this->assertEquals(2, count($result));
141 $this->assertEquals([400, 1600], \CRM_Utils_Array::collect('frobnication', (array) $result));
142 }
143
144 public function testGetFields() {
145 $getFields = MockBasicEntity::getFields()->execute()->indexBy('name');
146
3ffbd21c 147 $this->assertCount(7, $getFields);
19b53e5b
C
148 $this->assertEquals('Id', $getFields['id']['title']);
149 // Ensure default data type is "String" when not specified
150 $this->assertEquals('String', $getFields['color']['data_type']);
151
152 // Getfields should default to loadOptions = false and reduce them to bool
153 $this->assertTrue($getFields['group']['options']);
3ffbd21c 154 $this->assertTrue($getFields['fruit']['options']);
19b53e5b
C
155 $this->assertFalse($getFields['id']['options']);
156
3ffbd21c 157 // Load simple options
19b53e5b 158 $getFields = MockBasicEntity::getFields()
3ffbd21c 159 ->addWhere('name', 'IN', ['group', 'fruit'])
19b53e5b
C
160 ->setLoadOptions(TRUE)
161 ->execute()->indexBy('name');
162
3ffbd21c 163 $this->assertCount(2, $getFields);
19b53e5b 164 $this->assertArrayHasKey('one', $getFields['group']['options']);
3ffbd21c
CW
165 // Complex options should be reduced to simple array
166 $this->assertArrayHasKey(1, $getFields['fruit']['options']);
167 $this->assertEquals('Banana', $getFields['fruit']['options'][3]);
168
169 // Load complex options
170 $getFields = MockBasicEntity::getFields()
171 ->addWhere('name', 'IN', ['group', 'fruit'])
172 ->setLoadOptions(['id', 'name', 'label', 'color'])
173 ->execute()->indexBy('name');
174
175 // Simple options should be expanded to non-assoc array
176 $this->assertCount(2, $getFields);
177 $this->assertEquals('one', $getFields['group']['options'][0]['id']);
178 $this->assertEquals('First', $getFields['group']['options'][0]['name']);
179 $this->assertEquals('First', $getFields['group']['options'][0]['label']);
180 $this->assertFalse(isset($getFields['group']['options'][0]['color']));
181 // Complex options should give all requested properties
182 $this->assertEquals('Banana', $getFields['fruit']['options'][2]['label']);
183 $this->assertEquals('yellow', $getFields['fruit']['options'][2]['color']);
19b53e5b
C
184 }
185
186 public function testItemsToGet() {
187 $get = MockBasicEntity::get()
188 ->addWhere('color', 'NOT IN', ['yellow'])
189 ->addWhere('color', 'IN', ['red', 'blue'])
190 ->addWhere('color', '!=', 'green')
191 ->addWhere('group', '=', 'one')
192 ->addWhere('size', 'LIKE', 'big')
193 ->addWhere('shape', 'LIKE', '%a');
194
195 $itemsToGet = new \ReflectionMethod($get, '_itemsToGet');
196 $itemsToGet->setAccessible(TRUE);
197
198 $this->assertEquals(['red', 'blue'], $itemsToGet->invoke($get, 'color'));
199 $this->assertEquals(['one'], $itemsToGet->invoke($get, 'group'));
200 $this->assertEquals(['big'], $itemsToGet->invoke($get, 'size'));
201 $this->assertEmpty($itemsToGet->invoke($get, 'shape'));
202 $this->assertEmpty($itemsToGet->invoke($get, 'weight'));
203 }
204
205 public function testFieldsToGet() {
206 $get = MockBasicEntity::get()
207 ->addWhere('color', '!=', 'green');
208
209 $isFieldSelected = new \ReflectionMethod($get, '_isFieldSelected');
210 $isFieldSelected->setAccessible(TRUE);
211
212 // If no "select" is set, should always return true
213 $this->assertTrue($isFieldSelected->invoke($get, 'color'));
214 $this->assertTrue($isFieldSelected->invoke($get, 'shape'));
07d6d25b 215 $this->assertTrue($isFieldSelected->invoke($get, 'size', 'color', 'shape'));
19b53e5b
C
216
217 // With a non-empty "select" fieldsToSelect() will return fields needed to evaluate each clause.
218 $get->addSelect('id');
07d6d25b 219 $this->assertTrue($isFieldSelected->invoke($get, 'color', 'shape', 'size'));
19b53e5b 220 $this->assertTrue($isFieldSelected->invoke($get, 'id'));
07d6d25b 221 $this->assertFalse($isFieldSelected->invoke($get, 'shape', 'size', 'weight'));
19b53e5b
C
222 $this->assertFalse($isFieldSelected->invoke($get, 'group'));
223
224 $get->addClause('OR', ['shape', '=', 'round'], ['AND', [['size', '=', 'big'], ['weight', '!=', 'small']]]);
225 $this->assertTrue($isFieldSelected->invoke($get, 'color'));
226 $this->assertTrue($isFieldSelected->invoke($get, 'id'));
227 $this->assertTrue($isFieldSelected->invoke($get, 'shape'));
228 $this->assertTrue($isFieldSelected->invoke($get, 'size'));
07d6d25b 229 $this->assertTrue($isFieldSelected->invoke($get, 'group', 'weight'));
19b53e5b
C
230 $this->assertFalse($isFieldSelected->invoke($get, 'group'));
231
232 $get->addOrderBy('group');
233 $this->assertTrue($isFieldSelected->invoke($get, 'group'));
234 }
235
b0932d1e 236 public function testWildcardSelect() {
b0932d1e
CW
237 $records = [
238 ['group' => 'one', 'color' => 'red', 'shape' => 'round', 'size' => 'med', 'weight' => 10],
239 ['group' => 'two', 'color' => 'blue', 'shape' => 'round', 'size' => 'med', 'weight' => 20],
240 ];
c0e68893 241 $this->replaceRecords($records);
b0932d1e
CW
242
243 foreach (MockBasicEntity::get()->addSelect('*')->execute() as $result) {
244 ksort($result);
245 $this->assertEquals(['color', 'group', 'id', 'shape', 'size', 'weight'], array_keys($result));
246 }
247
248 $result = MockBasicEntity::get()
249 ->addSelect('*e', 'weig*ht')
250 ->execute()
251 ->first();
252 $this->assertEquals(['shape', 'size', 'weight'], array_keys($result));
253 }
254
c0e68893 255 public function testEmptyAndNullOperators() {
256 $records = [
257 [],
258 ['color' => '', 'weight' => 0],
259 ['color' => 'yellow', 'weight' => 100000000000],
260 ];
261 $this->replaceRecords($records);
262
263 $result = MockBasicEntity::get()
264 ->addWhere('color', 'IS NULL')
265 ->execute()->indexBy('id');
266 $this->assertCount(1, $result);
267 $this->assertArrayHasKey($records[0]['id'], (array) $result);
39deabd6 268
c0e68893 269 $result = MockBasicEntity::get()
270 ->addWhere('color', 'IS EMPTY')
271 ->execute()->indexBy('id');
272 $this->assertCount(2, $result);
273 $this->assertArrayNotHasKey($records[2]['id'], (array) $result);
274
275 $result = MockBasicEntity::get()
276 ->addWhere('color', 'IS NOT EMPTY')
277 ->execute()->indexBy('id');
278 $this->assertCount(1, $result);
279 $this->assertArrayHasKey($records[2]['id'], (array) $result);
280
281 $result = MockBasicEntity::get()
282 ->addWhere('weight', 'IS NULL')
283 ->execute()->indexBy('id');
284 $this->assertCount(1, $result);
285 $this->assertArrayHasKey($records[0]['id'], (array) $result);
286
287 $result = MockBasicEntity::get()
288 ->addWhere('weight', 'IS EMPTY')
289 ->execute()->indexBy('id');
290 $this->assertCount(2, $result);
291 $this->assertArrayNotHasKey($records[2]['id'], (array) $result);
292
293 $result = MockBasicEntity::get()
294 ->addWhere('weight', 'IS NOT EMPTY')
295 ->execute()->indexBy('id');
296 $this->assertCount(1, $result);
297 $this->assertArrayHasKey($records[2]['id'], (array) $result);
298 }
299
300 public function testContainsOperator() {
39deabd6
CW
301 $records = [
302 ['group' => 'one', 'fruit:name' => ['apple', 'pear'], 'weight' => 11],
303 ['group' => 'two', 'fruit:name' => ['pear', 'banana'], 'weight' => 12],
304 ];
c0e68893 305 $this->replaceRecords($records);
39deabd6
CW
306
307 $result = MockBasicEntity::get()
308 ->addWhere('fruit:name', 'CONTAINS', 'apple')
309 ->execute();
310 $this->assertCount(1, $result);
311 $this->assertEquals('one', $result->first()['group']);
312
313 $result = MockBasicEntity::get()
314 ->addWhere('fruit:name', 'CONTAINS', 'pear')
315 ->execute();
316 $this->assertCount(2, $result);
317
318 $result = MockBasicEntity::get()
319 ->addWhere('group', 'CONTAINS', 'o')
320 ->execute();
321 $this->assertCount(2, $result);
322
323 $result = MockBasicEntity::get()
324 ->addWhere('weight', 'CONTAINS', 1)
325 ->execute();
326 $this->assertCount(2, $result);
327
328 $result = MockBasicEntity::get()
329 ->addWhere('fruit:label', 'CONTAINS', 'Banana')
330 ->execute();
331 $this->assertCount(1, $result);
332 $this->assertEquals('two', $result->first()['group']);
333
334 $result = MockBasicEntity::get()
335 ->addWhere('weight', 'CONTAINS', 2)
336 ->execute();
337 $this->assertCount(1, $result);
338 $this->assertEquals('two', $result->first()['group']);
339 }
340
3ffbd21c 341 public function testPseudoconstantMatch() {
3ffbd21c
CW
342 $records = [
343 ['group:label' => 'First', 'shape' => 'round', 'fruit:name' => 'banana'],
344 ['group:name' => 'Second', 'shape' => 'square', 'fruit:label' => 'Pear'],
345 ];
c0e68893 346 $this->replaceRecords($records);
3ffbd21c
CW
347
348 $results = MockBasicEntity::get()
349 ->addSelect('*', 'group:label', 'group:name', 'fruit:name', 'fruit:color', 'fruit:label')
a4499ec5 350 ->addOrderBy('fruit:color', "DESC")
3ffbd21c
CW
351 ->execute();
352
353 $this->assertEquals('round', $results[0]['shape']);
354 $this->assertEquals('one', $results[0]['group']);
355 $this->assertEquals('First', $results[0]['group:label']);
356 $this->assertEquals('First', $results[0]['group:name']);
357 $this->assertEquals(3, $results[0]['fruit']);
358 $this->assertEquals('Banana', $results[0]['fruit:label']);
359 $this->assertEquals('banana', $results[0]['fruit:name']);
360 $this->assertEquals('yellow', $results[0]['fruit:color']);
361
a4499ec5
CW
362 // Reverse order
363 $results = MockBasicEntity::get()
364 ->addOrderBy('fruit:color')
365 ->execute();
366 $this->assertEquals('two', $results[0]['group']);
367
3ffbd21c
CW
368 // Cannot match to a non-unique option property like :color on create
369 try {
370 MockBasicEntity::create()->addValue('fruit:color', 'yellow')->execute();
371 }
372 catch (\API_Exception $createError) {
373 }
374 $this->assertContains('Illegal expression', $createError->getMessage());
375 }
376
19b53e5b 377}