Improve PropertyBag handling of offsetGet and custom properties; add more tests
[civicrm-core.git] / tests / phpunit / Civi / Payment / PropertyBagTest.php
1 <?php
2 namespace Civi\Payment;
3
4 use Civi\Test\HeadlessInterface;
5 use Civi\Test\TransactionalInterface;
6
7 /**
8 * @group headless
9 */
10 class PropertyBagTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
11
12 /**
13 * @return \Civi\Test\CiviEnvBuilder
14 */
15 public function setUpHeadless() {
16 static $reset = FALSE;
17 $return = \Civi\Test::headless()->apply($reset);
18 $reset = FALSE;
19 return $return;
20 }
21
22 /**
23 * Test we can set a contact ID.
24 */
25 public function testSetContactID() {
26 // Do things proper.
27 $propertyBag = new PropertyBag();
28 $propertyBag->setContactID(123);
29 $this->assertEquals(123, $propertyBag->getContactID());
30
31 // Same but this time set contact ID with string.
32 // (php should throw its own warnings about this because of the signature)
33 $propertyBag = new PropertyBag();
34 $propertyBag->setContactID('123');
35 $this->assertInternalType('int', $propertyBag->getContactID());
36 $this->assertEquals(123, $propertyBag->getContactID());
37
38 // Test we can have different labels
39 $propertyBag = new PropertyBag();
40 $propertyBag->setContactID(123);
41 $propertyBag->setContactID(456, 'new');
42 $this->assertEquals(123, $propertyBag->getContactID());
43 $this->assertEquals(456, $propertyBag->getContactID('new'));
44 }
45
46 /**
47 * Test we cannot set an invalid contact ID.
48 *
49 * @expectedException \InvalidArgumentException
50 */
51 public function testSetContactIDFailsIfInvalid() {
52 $propertyBag = new PropertyBag();
53 $propertyBag->setContactID(0);
54 }
55
56 /**
57 * Test we can set a contact ID the wrong way
58 */
59 public function testSetContactIDLegacyWay() {
60 $propertyBag = new PropertyBag();
61 $propertyBag['contactID'] = 123;
62 $this->assertEquals(123, $propertyBag->getContactID());
63 $this->assertEquals(123, $propertyBag['contactID']);
64 // There should not be any warnings yet.
65 $this->assertEquals("", $propertyBag->lastWarning);
66
67 // Now access via legacy name - should work but generate warning.
68 $this->assertEquals(123, $propertyBag['contact_id']);
69 $this->assertEquals("We have translated 'contact_id' to 'contactID' for you, but please update your code to use the propper setters and getters.", $propertyBag->lastWarning);
70
71 // Repeat but this time set the property using a legacy name, fetch by new name.
72 $propertyBag = new PropertyBag();
73 $propertyBag['contact_id'] = 123;
74 $this->assertEquals("We have translated 'contact_id' to 'contactID' for you, but please update your code to use the propper setters and getters.", $propertyBag->lastWarning);
75 $this->assertEquals(123, $propertyBag->getContactID());
76 $this->assertEquals(123, $propertyBag['contactID']);
77 $this->assertEquals(123, $propertyBag['contact_id']);
78 }
79
80 /**
81 * Test that emails set by the legacy method of 'email-5' can be retrieved with getEmail.
82 */
83 public function testSetBillingEmailLegacy() {
84 $localPropertyBag = new PropertyBag();
85 $localPropertyBag->mergeLegacyInputParams(['email-' . \CRM_Core_BAO_LocationType::getBilling() => 'a@b.com']);
86 $this->assertEquals('a@b.com', $localPropertyBag->getEmail());
87 }
88
89 /**
90 * Test that null is valid for recurring contribution ID.
91 *
92 * See https://github.com/civicrm/civicrm-core/pull/17292
93 */
94 public function testRecurProcessorIDNull() {
95 $bag = new PropertyBag();
96 $bag->setRecurProcessorID(NULL);
97 $value = $bag->getRecurProcessorID();
98 $this->assertNull($value);
99 }
100
101 /**
102 */
103 public function testMergeInputs() {
104 $propertyBag = new PropertyBag();
105 $propertyBag->mergeLegacyInputParams([
106 'contactID' => 123,
107 'contributionRecurID' => 456,
108 ]);
109 $this->assertEquals(123, $propertyBag->getContactID());
110 $this->assertEquals(456, $propertyBag->getContributionRecurID());
111 }
112
113 /**
114 * Test we can set and access custom props.
115 */
116 public function testSetCustomProp() {
117 $propertyBag = new PropertyBag();
118 $propertyBag->setCustomProperty('customThingForMyProcessor', 'fidget');
119 $this->assertEquals('fidget', $propertyBag->getCustomProperty('customThingForMyProcessor'));
120 $this->assertEquals('', $propertyBag->lastWarning);
121
122 // Test we can do this with array, although we should get a warning.
123 $propertyBag = new PropertyBag();
124 $propertyBag['customThingForMyProcessor'] = 'fidget';
125 $this->assertEquals('fidget', $propertyBag->getCustomProperty('customThingForMyProcessor'));
126 // Test array access, too.
127 $this->assertEquals('fidget', $propertyBag['customThingForMyProcessor']);
128 $this->assertEquals("Unknown property 'customThingForMyProcessor'. We have merged this in for now as a custom property. Please rewrite your code to use PropertyBag->setCustomProperty if it is a genuinely custom property, or a standardised setter like PropertyBag->setContactID for standard properties", $propertyBag->lastWarning);
129 }
130
131 /**
132 * Test we can't set a custom prop that we know about.
133 *
134 * @expectedException \InvalidArgumentException
135 * @expectedExceptionMessage Attempted to set 'contactID' via setCustomProperty - must use using its setter.
136 */
137 public function testSetCustomPropFails() {
138 $propertyBag = new PropertyBag();
139 $propertyBag->setCustomProperty('contactID', 123);
140 }
141
142 /**
143 * Test we can't get a custom prop that was not set.
144 *
145 * @expectedException \BadMethodCallException
146 * @expectedExceptionMessage Property 'aCustomProp' has not been set.
147 */
148 public function testGetCustomPropFails() {
149 $propertyBag = new PropertyBag();
150 $v = $propertyBag['aCustomProp'];
151 }
152
153 /**
154 *
155 * @dataProvider otherParamsDataProvider
156 */
157 public function testOtherParams($prop, $legacy_names, $valid_values, $invalid_values) {
158 $setter = 'set' . ucfirst($prop);
159 $getter = 'get' . ucfirst($prop);
160
161 // Using the setter and getter, check we can pass stuff in and get expected out.
162 foreach ($valid_values as $_) {
163 list($given, $expect) = $_;
164 $propertyBag = new PropertyBag();
165 try {
166 $propertyBag->$setter($given);
167 }
168 catch (\Exception $e) {
169 $this->fail("Expected to be able to set '$prop' to '$given' but got " . get_class($e) . ": " . $e->getMessage());
170 }
171 try {
172 $this->assertEquals($expect, $propertyBag->$getter());
173 }
174 catch (\Exception $e) {
175 $this->fail("Expected to be able to call $getter, having called $setter with '$given' but got " . get_class($e) . ": " . $e->getMessage());
176 }
177 }
178 // Using the setter and getter, check we get an error for invalid data.
179 foreach ($invalid_values as $given) {
180 try {
181 $propertyBag = new PropertyBag();
182 $propertyBag->$setter($given);
183 }
184 catch (\InvalidArgumentException $e) {
185 // counts this assertion.
186 $this->assertTrue(TRUE);
187 continue;
188 }
189 $this->fail("Expected an error trying to set $prop to " . json_encode($given) . " but did not get one.");
190 }
191
192 // Check array access for the proper property name and any aliases.
193 foreach (array_merge([$prop], $legacy_names) as $name) {
194 // Check array access
195 foreach ($valid_values as $_) {
196 list($given, $expect) = $_;
197 $propertyBag = new PropertyBag();
198 $propertyBag[$name] = $given;
199 $this->assertEquals($expect, $propertyBag->$getter(), "Failed to set $prop via array access on $name");
200 // Nb. I don't feel the need to repeat all the checks above for every alias.
201 // We only really need to test that the array access works for each alias.
202 break;
203 }
204 }
205 }
206
207 /**
208 * Test the require method works.
209 */
210 public function testRequire() {
211 $propertyBag = new PropertyBag();
212 $propertyBag->setContactID(123);
213 $propertyBag->setDescription('foo');
214 // This one should not error.
215 $propertyBag->require(['contactID', 'description']);
216 try {
217 $propertyBag->require(['contactID', 'description', 'contributionID', 'somethingthatdoesntexist']);
218 }
219 catch (\InvalidArgumentException $e) {
220 $this->assertEquals('Required properties missing: contributionID, somethingthatdoesntexist', $e->getMessage());
221 }
222 }
223
224 /**
225 * Test retrieves using CRM_Utils_Array::value still work.
226 */
227 public function testUtilsArray() {
228 $propertyBag = new PropertyBag();
229 $propertyBag->setContactID(123);
230 $this->assertEquals(123, \CRM_Utils_Array::value('contact_id', $propertyBag));
231
232 // Test that using utils array value to get a nonexistent property returns the default.
233 $this->assertEquals(456, \CRM_Utils_Array::value('ISawAManWhoWasntThere', $propertyBag, 456));
234 }
235
236 /**
237 */
238 public function testEmpty() {
239 $propertyBag = new PropertyBag();
240 $propertyBag->setContactID(123);
241 $propertyBag->setRecurProcessorID('');
242 $propertyBag->setBillingPostalCode(NULL);
243 $propertyBag->setFeeAmount(0);
244 $propertyBag->setCustomProperty('custom_issue', 'black lives matter');
245 $propertyBag->setCustomProperty('custom_null', NULL);
246 $propertyBag->setCustomProperty('custom_false', FALSE);
247 $propertyBag->setCustomProperty('custom_zls', '');
248 $propertyBag->setCustomProperty('custom_0', 0);
249
250 // Tests on known properties.
251 $v = empty($propertyBag->getContactID());
252 $this->assertFalse($v, "empty on a set, known property should return False");
253 $v = empty($propertyBag['contactID']);
254 $this->assertFalse($v, "empty on a set, known property accessed by ArrayAccess with correct name should return False");
255 $v = empty($propertyBag['contact_id']);
256 $this->assertFalse($v, "empty on a set, known property accessed by ArrayAccess with legacy name should return False");
257 $v = empty($propertyBag['recurProcessorID']);
258 $this->assertTrue($v, "empty on an unset, known property accessed by ArrayAccess should return True");
259 $v = empty($propertyBag->getRecurProcessorID());
260 $this->assertTrue($v, "empty on a set, but '' value should return True");
261 $v = empty($propertyBag->getFeeAmount());
262 $this->assertTrue($v, "empty on a set, but 0 value should return True");
263 $v = empty($propertyBag->getBillingPostalCode());
264 $this->assertTrue($v, "empty on a set, but NULL value should return True");
265
266 // Test custom properties.
267 $v = empty($propertyBag->getCustomProperty('custom_issue'));
268 $this->assertFalse($v, "empty on a set custom property with non-empty value should return False");
269 foreach (['null', 'false', 'zls', '0'] as $_) {
270 $v = empty($propertyBag["custom_$_"]);
271 $this->assertTrue($v, "empty on a set custom property with $_ value should return TRUE");
272 }
273 $v = empty($propertyBag['nonexistent_custom_field']);
274 $this->assertTrue($v, "empty on a non-existent custom property should return True");
275
276 $v = empty($propertyBag['custom_issue']);
277 $this->assertFalse($v, "empty on a set custom property accessed by ArrayAccess should return False");
278
279 }
280
281 /**
282 *
283 * Data provider for testOtherParams
284 *
285 */
286 public function otherParamsDataProvider() {
287 $valid_bools = [['0' , FALSE], ['', FALSE], [0, FALSE], [FALSE, FALSE], [TRUE, TRUE], [1, TRUE], ['1', TRUE]];
288 $valid_strings = [['foo' , 'foo'], ['', '']];
289 $valid_strings_inc_null = [['foo' , 'foo'], ['', ''], [NULL, '']];
290 $valid_ints = [[123, 123], ['123', 123]];
291 $invalid_ints = [-1, 0, NULL, ''];
292 return [
293 ['billingStreetAddress', [], $valid_strings_inc_null, []],
294 ['billingSupplementalAddress1', [], $valid_strings_inc_null, []],
295 ['billingSupplementalAddress2', [], $valid_strings_inc_null, []],
296 ['billingSupplementalAddress3', [], $valid_strings_inc_null, []],
297 ['billingCity', [], $valid_strings_inc_null, []],
298 ['billingPostalCode', [], $valid_strings_inc_null, []],
299 ['billingCounty', [], $valid_strings_inc_null, []],
300 ['billingCountry', [], [['GB', 'GB'], ['NZ', 'NZ']], ['XX', '', NULL, 0]],
301 ['contributionID', ['contribution_id'], $valid_ints, $invalid_ints],
302 ['contributionRecurID', ['contribution_recur_id'], $valid_ints, $invalid_ints],
303 ['description', [], [['foo' , 'foo'], ['', '']], []],
304 ['feeAmount', ['fee_amount'], [[1.23, 1.23], ['4.56', 4.56]], [NULL]],
305 ['firstName', [], $valid_strings_inc_null, []],
306 ['invoiceID', ['invoice_id'], $valid_strings, []],
307 ['isBackOffice', ['is_back_office'], $valid_bools, [NULL]],
308 ['isRecur', ['is_recur'], $valid_bools, [NULL]],
309 ['lastName', [], $valid_strings_inc_null, []],
310 ['paymentToken', [], $valid_strings, []],
311 ['recurFrequencyInterval', ['frequency_interval'], $valid_ints, $invalid_ints],
312 ['recurFrequencyUnit', [], [['month', 'month'], ['day', 'day'], ['year', 'year']], ['', NULL, 0]],
313 ['recurProcessorID', [], [['foo', 'foo']], [str_repeat('x', 256)]],
314 ['transactionID', ['transaction_id'], $valid_strings, []],
315 ['trxnResultCode', [], $valid_strings, []],
316 ];
317 }
318
319 /**
320 * Test generic getter, setter methods.
321 *
322 */
323 public function testGetterAndSetter() {
324 $propertyBag = new PropertyBag();
325
326 $propertyBag->setter('contactID', 123);
327 $this->assertEquals(123, $propertyBag->getContactID(), "Failed testing that a valid property was set correctly");
328
329 $result = $propertyBag->getter('contactID');
330 $this->assertEquals(123, $result, "Failed testing the getter on a set property");
331
332 $result = $propertyBag->getter('contactID', TRUE, 456);
333 $this->assertEquals(123, $result, "Failed testing the getter on a set property when providing a default");
334
335 $result = $propertyBag->getter('contributionRecurID', TRUE, 456);
336 $this->assertEquals(456, $result, "Failed testing the getter on an unset property when providing a default");
337
338 try {
339 $result = $propertyBag->getter('contributionRecurID', FALSE);
340 $this->fail("getter called with unset property should throw exception but none was thrown");
341 }
342 catch (\BadMethodCallException $e) {
343 }
344
345 $result = $propertyBag->getter('contribution_recur_id', TRUE, NULL);
346 $this->assertNull($result, "Failed testing the getter on an invalid property when providing a default");
347
348 try {
349 $result = $propertyBag->getter('contribution_recur_id');
350 }
351 catch (\InvalidArgumentException $e) {
352 $this->assertEquals("Attempted to get 'contribution_recur_id' via getCustomProperty - must use using its getter.", $e->getMessage());
353 }
354
355 // Nb. hmmm. the custom property getter does not throw an exception if the property is unset, it just returns NULL.
356 $result = $propertyBag->getter('something_custom');
357 $this->assertNull($result, "Failed testing the getter on an unset custom property when not providing a default");
358
359 try {
360 $propertyBag->setter('some_custom_thing', 'foo');
361 $this->fail("Expected to get an exception when trying to use setter for a non-standard property.");
362 }
363 catch (\BadMethodCallException $e) {
364 $this->assertEquals("Cannot use generic setter with non-standard properties; you must use setCustomProperty for custom properties.", $e->getMessage());
365 }
366
367 // Test labels.
368 $propertyBag->setter('contactID', '100', 'original');
369 $this->assertEquals(123, $propertyBag->getContactID(), "Looks like the setter did not respect the label.");
370 $this->assertEquals(100, $propertyBag->getContactID('original'), "Failed to retrieve the labelled property");
371 $this->assertEquals(100, $propertyBag->getter('contactID', FALSE, NULL, 'original'), "Failed using the getter to retrieve the labelled property");
372
373 }
374
375 }