Merge pull request #17602 from mattwire/supportsrecurring
[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 use PHPUnit\Framework\Error\Deprecated as DeprecatedError;
7
8 /**
9 * @group headless
10 */
11 class PropertyBagTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
12
13 /**
14 * @return \Civi\Test\CiviEnvBuilder
15 */
16 public function setUpHeadless() {
17 static $reset = FALSE;
18 $return = \Civi\Test::headless()->apply($reset);
19 $reset = FALSE;
20 return $return;
21 }
22
23 /**
24 * Test we can set a contact ID.
25 */
26 public function testSetContactID() {
27 // Do things proper.
28 $propertyBag = new PropertyBag();
29 $propertyBag->setContactID(123);
30 $this->assertEquals(123, $propertyBag->getContactID());
31
32 // Same but this time set contact ID with string.
33 // (php should throw its own warnings about this because of the signature)
34 $propertyBag = new PropertyBag();
35 $propertyBag->setContactID('123');
36 $this->assertInternalType('int', $propertyBag->getContactID());
37 $this->assertEquals(123, $propertyBag->getContactID());
38
39 // Test we can have different labels
40 $propertyBag = new PropertyBag();
41 $propertyBag->setContactID(123);
42 $propertyBag->setContactID(456, 'new');
43 $this->assertEquals(123, $propertyBag->getContactID());
44 $this->assertEquals(456, $propertyBag->getContactID('new'));
45 }
46
47 /**
48 * Test we cannot set an invalid contact ID.
49 *
50 * @expectedException \InvalidArgumentException
51 */
52 public function testSetContactIDFailsIfInvalid() {
53 $propertyBag = new PropertyBag();
54 $propertyBag->setContactID(0);
55 }
56
57 /**
58 * Test we can set a contact ID the wrong way
59 */
60 public function testSetContactIDLegacyWay() {
61 $propertyBag = new PropertyBag();
62
63 // To prevent E_USER_DEPRECATED errors during phpunit tests we take a copy
64 // of the existing error_reporting.
65 $oldLevel = error_reporting();
66 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
67
68 foreach (['contactID', 'contact_id'] as $prop) {
69 // Set by array access should cause deprecated error.
70 try {
71 $propertyBag[$prop] = 123;
72 $this->fail("Using array access to set a property '$prop' should trigger deprecated notice.");
73 }
74 catch (DeprecatedError $e) {
75 }
76
77 // But it should still work.
78 error_reporting($ignoreUserDeprecatedErrors);
79 $propertyBag[$prop] = 123;
80 error_reporting($oldLevel);
81 $this->assertEquals(123, $propertyBag->getContactID());
82
83 // Getting by array access should also cause deprecation error.
84 try {
85 $_ = $propertyBag[$prop];
86 $this->fail("Using array access to get a property '$prop' should trigger deprecated notice.");
87 }
88 catch (DeprecatedError $e) {
89 }
90
91 // But again, it should work.
92 error_reporting($ignoreUserDeprecatedErrors);
93 $this->assertEquals(123, $propertyBag[$prop], "Getting '$prop' by array access should work");
94 error_reporting($oldLevel);
95 }
96 }
97
98 /**
99 * Test that emails set by the legacy method of 'email-5' can be retrieved with getEmail.
100 */
101 public function testSetBillingEmailLegacy() {
102 $localPropertyBag = PropertyBag::cast(['email-' . \CRM_Core_BAO_LocationType::getBilling() => 'a@b.com']);
103 $this->assertEquals('a@b.com', $localPropertyBag->getEmail());
104 }
105
106 /**
107 * Test that null is valid for recurring contribution ID.
108 *
109 * See https://github.com/civicrm/civicrm-core/pull/17292
110 */
111 public function testRecurProcessorIDNull() {
112 $bag = new PropertyBag();
113 $bag->setRecurProcessorID(NULL);
114 $value = $bag->getRecurProcessorID();
115 $this->assertNull($value);
116 }
117
118 /**
119 */
120 public function testMergeInputs() {
121 $propertyBag = PropertyBag::cast([
122 'contactID' => 123,
123 'contributionRecurID' => 456,
124 ]);
125 $this->assertEquals(123, $propertyBag->getContactID());
126 $this->assertEquals(456, $propertyBag->getContributionRecurID());
127 }
128
129 /**
130 * Test we can set and access custom props.
131 */
132 public function testSetCustomProp() {
133 $oldLevel = error_reporting();
134 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
135
136 // The proper way.
137 $propertyBag = new PropertyBag();
138 $propertyBag->setCustomProperty('customThingForMyProcessor', 'fidget');
139 $this->assertEquals('fidget', $propertyBag->getCustomProperty('customThingForMyProcessor'));
140 $this->assertEquals('', $propertyBag->lastWarning);
141
142 // Test we can do this with array, although we should get a warning.
143 $propertyBag = new PropertyBag();
144
145 // Set by array access should cause deprecated error.
146 try {
147 $propertyBag['customThingForMyProcessor'] = 'fidget';
148 $this->fail("Using array access to set an implicitly custom property should trigger deprecated notice.");
149 }
150 catch (DeprecatedError $e) {
151 }
152
153 // But it should still work.
154 error_reporting($ignoreUserDeprecatedErrors);
155 $propertyBag['customThingForMyProcessor'] = 'fidget';
156 error_reporting($oldLevel);
157 $this->assertEquals('fidget', $propertyBag->getCustomProperty('customThingForMyProcessor'));
158
159 // Getting by array access should also cause deprecation error.
160 try {
161 $_ = $propertyBag['customThingForMyProcessor'];
162 $this->fail("Using array access to get an implicitly custom property should trigger deprecated notice.");
163 }
164 catch (DeprecatedError $e) {
165 }
166
167 // But again, it should work.
168 error_reporting($ignoreUserDeprecatedErrors);
169 $this->assertEquals('fidget', $propertyBag['customThingForMyProcessor']);
170 error_reporting($oldLevel);
171
172 }
173
174 /**
175 * Test we can't set a custom prop that we know about.
176 *
177 * @expectedException \InvalidArgumentException
178 * @expectedExceptionMessage Attempted to set 'contactID' via setCustomProperty - must use using its setter.
179 */
180 public function testSetCustomPropFails() {
181 $propertyBag = new PropertyBag();
182 $propertyBag->setCustomProperty('contactID', 123);
183 }
184
185 /**
186 * Test we get NULL for custom prop that was not set.
187 *
188 * This is only for backward compatibility/ease of transition. One day it would be nice to throw an exception instead.
189 *
190 * @expectedException \BadMethodCallException
191 * @expectedExceptionMessage Property 'aCustomProp' has not been set.
192 */
193 public function testGetCustomPropFails() {
194 $propertyBag = new PropertyBag();
195 // Tricky test. We need to ignore deprecation errors, we're testing deprecated behaviour,
196 // but we need to listen out for a different exception.
197 $oldLevel = error_reporting();
198 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
199 error_reporting($ignoreUserDeprecatedErrors);
200
201 // Do the do.
202 try {
203 $v = $propertyBag['aCustomProp'];
204 error_reporting($oldLevel);
205 $this->fail("Expected BadMethodCallException from accessing an unset custom prop.");
206 }
207 catch (\BadMethodCallException $e) {
208 // reset error level.
209 error_reporting($oldLevel);
210 // rethrow for phpunit to catch.
211 throw $e;
212 }
213
214 }
215
216 /**
217 *
218 * @dataProvider otherParamsDataProvider
219 */
220 public function testOtherParams($prop, $legacy_names, $valid_values, $invalid_values) {
221 $setter = 'set' . ucfirst($prop);
222 $getter = 'get' . ucfirst($prop);
223
224 // Using the setter and getter, check we can pass stuff in and get expected out.
225 foreach ($valid_values as $_) {
226 list($given, $expect) = $_;
227 $propertyBag = new PropertyBag();
228 try {
229 $propertyBag->$setter($given);
230 }
231 catch (\Exception $e) {
232 $this->fail("Expected to be able to set '$prop' to '$given' but got " . get_class($e) . ": " . $e->getMessage());
233 }
234 try {
235 $this->assertEquals($expect, $propertyBag->$getter());
236 }
237 catch (\Exception $e) {
238 $this->fail("Expected to be able to call $getter, having called $setter with '$given' but got " . get_class($e) . ": " . $e->getMessage());
239 }
240 }
241 // Using the setter and getter, check we get an error for invalid data.
242 foreach ($invalid_values as $given) {
243 try {
244 $propertyBag = new PropertyBag();
245 $propertyBag->$setter($given);
246 }
247 catch (\InvalidArgumentException $e) {
248 // counts this assertion.
249 $this->assertTrue(TRUE);
250 continue;
251 }
252 $this->fail("Expected an error trying to set $prop to " . json_encode($given) . " but did not get one.");
253 }
254
255 $oldLevel = error_reporting();
256 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
257
258 // Check array access for the proper property name and any aliases.
259 // This is going to throw a bunch of deprecated errors, but we know this
260 // (and have tested it elsewhere) so we turn those off.
261 error_reporting($ignoreUserDeprecatedErrors);
262 foreach (array_merge([$prop], $legacy_names) as $name) {
263 foreach ($valid_values as $_) {
264 list($given, $expect) = $_;
265 $propertyBag = new PropertyBag();
266 $propertyBag[$name] = $given;
267 $this->assertEquals($expect, $propertyBag->$getter(), "Failed to set $prop via array access on $name");
268 // Nb. I don't feel the need to repeat all the checks above for every alias.
269 // We only really need to test that the array access works for each alias.
270 break;
271 }
272 }
273 error_reporting($oldLevel);
274 }
275
276 /**
277 * Test the require method works.
278 */
279 public function testRequire() {
280 $propertyBag = new PropertyBag();
281 $propertyBag->setContactID(123);
282 $propertyBag->setDescription('foo');
283 // This one should not error.
284 $propertyBag->require(['contactID', 'description']);
285 try {
286 $propertyBag->require(['contactID', 'description', 'contributionID', 'somethingthatdoesntexist']);
287 }
288 catch (\InvalidArgumentException $e) {
289 $this->assertEquals('Required properties missing: contributionID, somethingthatdoesntexist', $e->getMessage());
290 }
291 }
292
293 /**
294 * Test retrieves using CRM_Utils_Array::value still work.
295 */
296 public function testUtilsArray() {
297 $propertyBag = new PropertyBag();
298 $propertyBag->setContactID(123);
299 // This will throw deprecation notices but we don't care.
300 $oldLevel = error_reporting();
301 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
302 error_reporting($ignoreUserDeprecatedErrors);
303 $this->assertEquals(123, \CRM_Utils_Array::value('contact_id', $propertyBag));
304
305 // Test that using utils array value to get a nonexistent property returns the default.
306 $this->assertEquals(456, \CRM_Utils_Array::value('ISawAManWhoWasntThere', $propertyBag, 456));
307 error_reporting($oldLevel);
308 }
309
310 /**
311 */
312 public function testEmpty() {
313 $propertyBag = new PropertyBag();
314 $propertyBag->setContactID(123);
315 $propertyBag->setRecurProcessorID('');
316 $propertyBag->setBillingPostalCode(NULL);
317 $propertyBag->setFeeAmount(0);
318 $propertyBag->setCustomProperty('custom_issue', 'black lives matter');
319 $propertyBag->setCustomProperty('custom_null', NULL);
320 $propertyBag->setCustomProperty('custom_false', FALSE);
321 $propertyBag->setCustomProperty('custom_zls', '');
322 $propertyBag->setCustomProperty('custom_0', 0);
323
324 // To prevent E_USER_DEPRECATED errors during phpunit tests we take a copy
325 // of the existing error_reporting.
326 $oldLevel = error_reporting();
327 $ignoreUserDeprecatedErrors = $oldLevel & ~E_USER_DEPRECATED;
328 error_reporting($ignoreUserDeprecatedErrors);
329
330 // Tests on known properties.
331 $v = empty($propertyBag->getContactID());
332 $this->assertFalse($v, "empty on a set, known property should return False");
333 $v = empty($propertyBag['contactID']);
334 $this->assertFalse($v, "empty on a set, known property accessed by ArrayAccess with correct name should return False");
335 $v = empty($propertyBag['contact_id']);
336 $this->assertFalse($v, "empty on a set, known property accessed by ArrayAccess with legacy name should return False");
337 $v = empty($propertyBag['recurProcessorID']);
338 $this->assertTrue($v, "empty on an unset, known property accessed by ArrayAccess should return True");
339 $v = empty($propertyBag->getRecurProcessorID());
340 $this->assertTrue($v, "empty on a set, but '' value should return True");
341 $v = empty($propertyBag->getFeeAmount());
342 $this->assertTrue($v, "empty on a set, but 0 value should return True");
343 $v = empty($propertyBag->getBillingPostalCode());
344 $this->assertTrue($v, "empty on a set, but NULL value should return True");
345
346 // Test custom properties.
347 $v = empty($propertyBag->getCustomProperty('custom_issue'));
348 $this->assertFalse($v, "empty on a set custom property with non-empty value should return False");
349 foreach (['null', 'false', 'zls', '0'] as $_) {
350 $v = empty($propertyBag["custom_$_"]);
351 $this->assertTrue($v, "empty on a set custom property with $_ value should return TRUE");
352 }
353 $v = empty($propertyBag['nonexistent_custom_field']);
354 $this->assertTrue($v, "empty on a non-existent custom property should return True");
355
356 $v = empty($propertyBag['custom_issue']);
357 $this->assertFalse($v, "empty on a set custom property accessed by ArrayAccess should return False");
358
359 error_reporting($oldLevel);
360 }
361
362 /**
363 *
364 * Data provider for testOtherParams
365 *
366 */
367 public function otherParamsDataProvider() {
368 $valid_bools = [['0' , FALSE], ['', FALSE], [0, FALSE], [FALSE, FALSE], [TRUE, TRUE], [1, TRUE], ['1', TRUE]];
369 $valid_strings = [['foo' , 'foo'], ['', '']];
370 $valid_strings_inc_null = [['foo' , 'foo'], ['', ''], [NULL, '']];
371 $valid_ints = [[123, 123], ['123', 123]];
372 $invalid_ints = [-1, 0, NULL, ''];
373 return [
374 ['billingStreetAddress', [], $valid_strings_inc_null, []],
375 ['billingSupplementalAddress1', [], $valid_strings_inc_null, []],
376 ['billingSupplementalAddress2', [], $valid_strings_inc_null, []],
377 ['billingSupplementalAddress3', [], $valid_strings_inc_null, []],
378 ['billingCity', [], $valid_strings_inc_null, []],
379 ['billingPostalCode', [], $valid_strings_inc_null, []],
380 ['billingCounty', [], $valid_strings_inc_null, []],
381 ['billingCountry', [], [['GB', 'GB'], ['NZ', 'NZ']], ['XX', '', NULL, 0]],
382 ['contributionID', ['contribution_id'], $valid_ints, $invalid_ints],
383 ['contributionRecurID', ['contribution_recur_id'], $valid_ints, $invalid_ints],
384 ['description', [], [['foo' , 'foo'], ['', '']], []],
385 ['feeAmount', ['fee_amount'], [[1.23, 1.23], ['4.56', 4.56]], [NULL]],
386 ['firstName', [], $valid_strings_inc_null, []],
387 ['invoiceID', ['invoice_id'], $valid_strings, []],
388 ['isBackOffice', ['is_back_office'], $valid_bools, [NULL]],
389 ['isRecur', ['is_recur'], $valid_bools, [NULL]],
390 ['lastName', [], $valid_strings_inc_null, []],
391 ['paymentToken', [], $valid_strings, []],
392 ['recurFrequencyInterval', ['frequency_interval'], $valid_ints, $invalid_ints],
393 ['recurFrequencyUnit', [], [['month', 'month'], ['day', 'day'], ['year', 'year']], ['', NULL, 0]],
394 ['recurProcessorID', [], [['foo', 'foo']], [str_repeat('x', 256)]],
395 ['transactionID', ['transaction_id'], $valid_strings, []],
396 ['trxnResultCode', [], $valid_strings, []],
397 ];
398 }
399
400 /**
401 * Test generic getter, setter methods.
402 *
403 */
404 public function testGetterAndSetter() {
405 $propertyBag = new PropertyBag();
406
407 $propertyBag->setter('contactID', 123);
408 $this->assertEquals(123, $propertyBag->getContactID(), "Failed testing that a valid property was set correctly");
409
410 $result = $propertyBag->getter('contactID');
411 $this->assertEquals(123, $result, "Failed testing the getter on a set property");
412
413 $result = $propertyBag->getter('contactID', TRUE, 456);
414 $this->assertEquals(123, $result, "Failed testing the getter on a set property when providing a default");
415
416 $result = $propertyBag->getter('contributionRecurID', TRUE, 456);
417 $this->assertEquals(456, $result, "Failed testing the getter on an unset property when providing a default");
418
419 try {
420 $result = $propertyBag->getter('contributionRecurID', FALSE);
421 $this->fail("getter called with unset property should throw exception but none was thrown");
422 }
423 catch (\BadMethodCallException $e) {
424 }
425
426 $result = $propertyBag->getter('contribution_recur_id', TRUE, NULL);
427 $this->assertNull($result, "Failed testing the getter on an invalid property when providing a default");
428
429 try {
430 $result = $propertyBag->getter('contribution_recur_id');
431 }
432 catch (\InvalidArgumentException $e) {
433 $this->assertEquals("Attempted to get 'contribution_recur_id' via getCustomProperty - must use using its getter.", $e->getMessage());
434 }
435
436 // Nb. up to 5.26, the custom property getter did not throw an exception if the property is unset, it just returned NULL.
437 // Now, we return NULL for array access (legacy) but for modern access
438 // (getter, getPropX(), getCustomProperty()) then we throw an exception if
439 // it is not set.
440 try {
441 $result = $propertyBag->getter('something_custom');
442 $this->fail("Expected a BadMethodCallException when getting 'something_custom' which has not been set.");
443 }
444 catch (\BadMethodCallException $e) {
445 }
446
447 try {
448 $propertyBag->setter('some_custom_thing', 'foo');
449 $this->fail("Expected to get an exception when trying to use setter for a non-standard property.");
450 }
451 catch (\BadMethodCallException $e) {
452 $this->assertEquals("Cannot use generic setter with non-standard properties; you must use setCustomProperty for custom properties.", $e->getMessage());
453 }
454
455 // Test labels.
456 $propertyBag->setter('contactID', '100', 'original');
457 $this->assertEquals(123, $propertyBag->getContactID(), "Looks like the setter did not respect the label.");
458 $this->assertEquals(100, $propertyBag->getContactID('original'), "Failed to retrieve the labelled property");
459 $this->assertEquals(100, $propertyBag->getter('contactID', FALSE, NULL, 'original'), "Failed using the getter to retrieve the labelled property");
460
461 }
462
463 }