1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* These tests assert various things about processing a renewal payment for a WooCommerce Subscription. |
4
|
|
|
* |
5
|
|
|
* The responses from HTTP requests are mocked using the WP filter `pre_http_request`. |
6
|
|
|
* |
7
|
|
|
* There are a few methods that need to be mocked in the class WC_Stripe_Subs_Compat, which is |
8
|
|
|
* why that class is mocked even though the method under test is part of that class. |
9
|
|
|
* |
10
|
|
|
* @package WooCommerce_Stripe/Classes/WC_Stripe_Subscription_Renewal_Test |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* WC_Stripe_Subscription_Renewal_Test |
15
|
|
|
*/ |
16
|
|
|
class WC_Stripe_Subscription_Renewal_Test extends WP_UnitTestCase { |
17
|
|
|
/** |
18
|
|
|
* System under test, and a mock object with some methods mocked for testing |
19
|
|
|
* |
20
|
|
|
* @var PHPUnit_Framework_MockObject_MockObject |
21
|
|
|
*/ |
22
|
|
|
private $wc_stripe_subs_compat; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* The statement descriptor we'll use in a test. |
26
|
|
|
* |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
private $statement_descriptor; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Sets up things all tests need. |
33
|
|
|
*/ |
34
|
|
|
public function setUp() { |
35
|
|
|
parent::setUp(); |
36
|
|
|
|
37
|
|
|
$this->wc_stripe_subs_compat = $this->getMockBuilder( 'WC_Stripe_Subs_Compat' ) |
38
|
|
|
->disableOriginalConstructor() |
39
|
|
|
->setMethods( array( 'prepare_order_source', 'has_subscription', 'ensure_subscription_has_customer_id' ) ) |
40
|
|
|
->getMock(); |
41
|
|
|
|
42
|
|
|
// Mocked in order to get metadata[payment_type] = recurring in the HTTP request. |
43
|
|
|
$this->wc_stripe_subs_compat |
44
|
|
|
->expects( $this->any() ) |
45
|
|
|
->method( 'has_subscription' ) |
46
|
|
|
->will( |
47
|
|
|
$this->returnValue( true ) |
48
|
|
|
); |
49
|
|
|
|
50
|
|
|
$this->statement_descriptor = 'This is a statement descriptor.'; |
51
|
|
|
add_option( |
52
|
|
|
'woocommerce_stripe_settings', |
53
|
|
|
array( |
54
|
|
|
'statement_descriptor' => $this->statement_descriptor, |
55
|
|
|
) |
56
|
|
|
); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Tears down the stuff we set up. |
61
|
|
|
*/ |
62
|
|
|
public function tearDown() { |
63
|
|
|
parent::tearDown(); |
64
|
|
|
|
65
|
|
|
delete_option( 'woocommerce_stripe_settings' ); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Overall this test works like this: |
70
|
|
|
* |
71
|
|
|
* 1. Several things are set up or mocked. |
72
|
|
|
* 2. A function that will mock an HTTP response for the payment_intents is created. |
73
|
|
|
* 3. That same function has some assertions about the things we send to the |
74
|
|
|
* payments_intents endpoint. |
75
|
|
|
* 4. The function under test - `process_subscription_payment` - is called. |
76
|
|
|
* 5. More assertions are made. |
77
|
|
|
*/ |
78
|
|
|
public function test_renewal_successful() { |
79
|
|
|
// Arrange: Some variables we'll use later. |
80
|
|
|
$renewal_order = WC_Helper_Order::create_order(); |
81
|
|
|
$amount = 20; // WC Subs sends an amount to be used, instead of using the order amount. |
82
|
|
|
$stripe_amount = WC_Stripe_Helper::get_stripe_amount( $amount ); |
83
|
|
|
$currency = strtolower( $renewal_order->get_currency() ); |
84
|
|
|
$customer = 'cus_123abc'; |
85
|
|
|
$source = 'src_123abc'; |
86
|
|
|
$statement_descriptor = WC_Stripe_Helper::clean_statement_descriptor( $this->statement_descriptor ); |
87
|
|
|
$should_retry = false; |
88
|
|
|
$previous_error = false; |
89
|
|
|
$payments_intents_api_endpoint = 'https://api.stripe.com/v1/payment_intents'; |
90
|
|
|
$urls_used = array(); |
91
|
|
|
|
92
|
|
|
$renewal_order->set_payment_method( 'stripe' ); |
93
|
|
|
|
94
|
|
|
// Arrange: Mock prepare_order_source() so that we have a customer and source. |
95
|
|
|
$this->wc_stripe_subs_compat |
96
|
|
|
->expects( $this->any() ) |
97
|
|
|
->method( 'prepare_order_source' ) |
98
|
|
|
->will( |
99
|
|
|
$this->returnValue( |
100
|
|
|
(object) array( |
101
|
|
|
'token_id' => false, |
102
|
|
|
'customer' => $customer, |
103
|
|
|
'source' => $source, |
104
|
|
|
'source_object' => (object) array(), |
105
|
|
|
) |
106
|
|
|
) |
107
|
|
|
); |
108
|
|
|
|
109
|
|
|
// Arrange: Add filter that will return a mocked HTTP response for the payment_intent call. |
110
|
|
|
// Note: There are assertions in the callback function. |
111
|
|
|
$pre_http_request_response_callback = function( $preempt, $request_args, $url ) use ( |
112
|
|
|
$renewal_order, |
113
|
|
|
$stripe_amount, |
114
|
|
|
$currency, |
115
|
|
|
$customer, |
116
|
|
|
$source, |
117
|
|
|
$statement_descriptor, |
118
|
|
|
$payments_intents_api_endpoint, |
119
|
|
|
&$urls_used |
120
|
|
|
) { |
121
|
|
|
// Add all urls to array so we can later make assertions about which endpoints were used. |
122
|
|
|
array_push( $urls_used, $url ); |
123
|
|
|
|
124
|
|
|
// Continue without mocking the request if it's not the endpoint we care about. |
125
|
|
|
if ( $payments_intents_api_endpoint !== $url ) { |
126
|
|
|
return false; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
// Assert: the request method is POST. |
130
|
|
|
$this->assertArrayHasKey( 'method', $request_args ); |
131
|
|
|
$this->assertSame( 'POST', $request_args['method'] ); |
132
|
|
|
|
133
|
|
|
// Assert: the request has a body. |
134
|
|
|
$this->assertArrayHasKey( 'body', $request_args ); |
135
|
|
|
|
136
|
|
|
// Assert: the request body contains these values. |
137
|
|
|
$expected_request_body_values = array( |
138
|
|
|
'source' => $source, |
139
|
|
|
'amount' => $stripe_amount, |
140
|
|
|
'currency' => $currency, |
141
|
|
|
'payment_method_types' => array( 'card' ), |
142
|
|
|
'customer' => $customer, |
143
|
|
|
'off_session' => 'true', |
144
|
|
|
'confirm' => 'true', |
145
|
|
|
'confirmation_method' => 'automatic', |
146
|
|
|
'statement_descriptor' => $statement_descriptor, |
147
|
|
|
); |
148
|
|
View Code Duplication |
foreach ( $expected_request_body_values as $key => $value ) { |
|
|
|
|
149
|
|
|
$this->assertArrayHasKey( $key, $request_args['body'] ); |
150
|
|
|
$this->assertSame( $value, $request_args['body'][ $key ] ); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
// Assert: the request body contains these keys, without checking for their value. |
154
|
|
|
$expected_request_body_keys = array( |
155
|
|
|
'description', |
156
|
|
|
'metadata', |
157
|
|
|
); |
158
|
|
|
foreach ( $expected_request_body_keys as $key ) { |
159
|
|
|
$this->assertArrayHasKey( $key, $request_args['body'] ); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
// Assert: the body metadata has these values. |
163
|
|
|
$order_id = (string) $renewal_order->get_id(); |
164
|
|
|
$expected_metadata_values = array( |
165
|
|
|
'order_id' => $order_id, |
166
|
|
|
'payment_type' => 'recurring', |
167
|
|
|
); |
168
|
|
|
foreach ( $expected_metadata_values as $key => $value ) { |
169
|
|
|
$this->assertArrayHasKey( $key, $request_args['body']['metadata'] ); |
170
|
|
|
$this->assertSame( $value, $request_args['body']['metadata'][ $key ] ); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
// Assert: the body metadata has these keys, without checking for their value. |
174
|
|
|
$expected_metadata_keys = array( |
175
|
|
|
'customer_name', |
176
|
|
|
'customer_email', |
177
|
|
|
'site_url', |
178
|
|
|
); |
179
|
|
|
foreach ( $expected_metadata_keys as $key ) { |
180
|
|
|
$this->assertArrayHasKey( $key, $request_args['body']['metadata'] ); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
// Assert: the request body does not contains these keys. |
184
|
|
|
$expected_missing_request_body_keys = array( |
185
|
|
|
'capture', // No need to capture with a payment intent. |
186
|
|
|
'capture_method', // The default ('automatic') is what we want in this case, so we leave it off. |
187
|
|
|
'expand[]', |
188
|
|
|
); |
189
|
|
|
foreach ( $expected_missing_request_body_keys as $key ) { |
190
|
|
|
$this->assertArrayNotHasKey( $key, $request_args['body'] ); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// Arrange: return dummy content as the response. |
194
|
|
|
return array( |
195
|
|
|
'headers' => array(), |
196
|
|
|
// Too bad we aren't dynamically setting things 'cus_123abc' when using this file. |
197
|
|
|
'body' => file_get_contents( 'tests/phpunit/dummy-data/subscription_renewal_response_success.json' ), |
198
|
|
|
'response' => array( |
199
|
|
|
'code' => 200, |
200
|
|
|
'message' => 'OK', |
201
|
|
|
), |
202
|
|
|
'cookies' => array(), |
203
|
|
|
'filename' => null, |
204
|
|
|
); |
205
|
|
|
}; |
206
|
|
|
|
207
|
|
|
add_filter( 'pre_http_request', $pre_http_request_response_callback, 10, 3 ); |
208
|
|
|
|
209
|
|
|
// Arrange: Make sure to check that an action we care about was called |
210
|
|
|
// by hooking into it. |
211
|
|
|
$mock_action_process_payment = new MockAction(); |
212
|
|
|
add_action( |
213
|
|
|
'wc_gateway_stripe_process_payment', |
214
|
|
|
[ &$mock_action_process_payment, 'action' ] |
215
|
|
|
); |
216
|
|
|
|
217
|
|
|
// Act: call process_subscription_payment(). |
218
|
|
|
// We need to use `wc_stripe_subs_compat` here because we mocked this class earlier. |
219
|
|
|
$result = $this->wc_stripe_subs_compat->process_subscription_payment( 20, $renewal_order, $should_retry, $previous_error ); |
220
|
|
|
|
221
|
|
|
// Assert: nothing was returned. |
222
|
|
|
$this->assertEquals( $result, null ); |
223
|
|
|
|
224
|
|
|
// Assert that we saved the payment intent to the order. |
225
|
|
|
$order_id = $renewal_order->get_id(); |
226
|
|
|
$order = wc_get_order( $order_id ); |
227
|
|
|
$order_data = $order->get_meta( '_stripe_intent_id' ); |
228
|
|
|
|
229
|
|
|
$this->assertEquals( $order_data, 'pi_123abc' ); |
230
|
|
|
|
231
|
|
|
// Transaction ID was saved to order. |
232
|
|
|
$order_transaction_id = $order->get_transaction_id(); |
233
|
|
|
$this->assertEquals( $order_transaction_id, 'ch_123abc' ); |
234
|
|
|
|
235
|
|
|
// Assert: the order was marked as processing (this is done in process_response()). |
236
|
|
|
$this->assertEquals( $order->get_status(), 'processing' ); |
237
|
|
|
|
238
|
|
|
// Assert: called payment intents. |
239
|
|
|
$this->assertTrue( in_array( $payments_intents_api_endpoint, $urls_used ) ); |
240
|
|
|
|
241
|
|
|
// Assert: Our hook was called once. |
242
|
|
|
$this->assertEquals( 1, $mock_action_process_payment->get_call_count() ); |
243
|
|
|
|
244
|
|
|
// Assert: Only our hook was called. |
245
|
|
|
$this->assertEquals( array( 'wc_gateway_stripe_process_payment' ), $mock_action_process_payment->get_tags() ); |
246
|
|
|
|
247
|
|
|
// Clean up. |
248
|
|
|
remove_filter( 'pre_http_request', array( $this, 'pre_http_request_response_success' ) ); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Overall this test works like this: |
253
|
|
|
* |
254
|
|
|
* 1. Several things are set up or mocked. |
255
|
|
|
* 2. A function that will mock an HTTP response for the payment_intents is created. |
256
|
|
|
* 3. That same function has some assertions about the things we send to the |
257
|
|
|
* payments_intents endpoint. |
258
|
|
|
* 4. The function under test - `process_subscription_payment` - is called. |
259
|
|
|
* 5. More assertions are made. |
260
|
|
|
*/ |
261
|
|
|
public function test_renewal_authorization_required() { |
262
|
|
|
// Arrange: Some variables we'll use later. |
263
|
|
|
$renewal_order = WC_Helper_Order::create_order(); |
264
|
|
|
$amount = 20; |
265
|
|
|
$stripe_amount = WC_Stripe_Helper::get_stripe_amount( $amount ); |
266
|
|
|
$currency = strtolower( $renewal_order->get_currency() ); |
267
|
|
|
$customer = 'cus_123abc'; |
268
|
|
|
$source = 'src_123abc'; |
269
|
|
|
$should_retry = false; |
270
|
|
|
$previous_error = false; |
271
|
|
|
$payments_intents_api_endpoint = 'https://api.stripe.com/v1/payment_intents'; |
272
|
|
|
$urls_used = array(); |
273
|
|
|
|
274
|
|
|
// Arrange: Mock prepare_order_source() so that we have a customer and source. |
275
|
|
|
$this->wc_stripe_subs_compat |
276
|
|
|
->expects( $this->any() ) |
277
|
|
|
->method( 'prepare_order_source' ) |
278
|
|
|
->will( |
279
|
|
|
$this->returnValue( |
280
|
|
|
(object) array( |
281
|
|
|
'token_id' => false, |
282
|
|
|
'customer' => $customer, |
283
|
|
|
'source' => $source, |
284
|
|
|
'source_object' => (object) array(), |
285
|
|
|
) |
286
|
|
|
) |
287
|
|
|
); |
288
|
|
|
|
289
|
|
|
// Arrange: Add filter that will return a mocked HTTP response for the payment_intent call. |
290
|
|
|
$pre_http_request_response_callback = function( $preempt, $request_args, $url ) use ( |
291
|
|
|
$renewal_order, |
292
|
|
|
$stripe_amount, |
293
|
|
|
$currency, |
294
|
|
|
$customer, |
295
|
|
|
$source, |
296
|
|
|
$payments_intents_api_endpoint, |
297
|
|
|
&$urls_used |
298
|
|
|
) { |
299
|
|
|
// Add all urls to array so we can later make assertions about which endpoints were used. |
300
|
|
|
array_push( $urls_used, $url ); |
301
|
|
|
|
302
|
|
|
// Continue without mocking the request if it's not the endpoint we care about. |
303
|
|
|
if ( $payments_intents_api_endpoint !== $url ) { |
304
|
|
|
return false; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
// Arrange: return dummy content as the response. |
308
|
|
|
return array( |
309
|
|
|
'headers' => array(), |
310
|
|
|
// Too bad we aren't dynamically setting things 'cus_123abc' when using this file. |
311
|
|
|
'body' => file_get_contents( 'tests/phpunit/dummy-data/subscription_renewal_response_authentication_required.json' ), |
312
|
|
|
'response' => array( |
313
|
|
|
'code' => 402, |
314
|
|
|
'message' => 'Payment Required', |
315
|
|
|
), |
316
|
|
|
'cookies' => array(), |
317
|
|
|
'filename' => null, |
318
|
|
|
); |
319
|
|
|
}; |
320
|
|
|
add_filter( 'pre_http_request', $pre_http_request_response_callback, 10, 3 ); |
321
|
|
|
|
322
|
|
|
// Arrange: Make sure to check that an action we care about was called |
323
|
|
|
// by hooking into it. |
324
|
|
|
$mock_action_process_payment = new MockAction(); |
325
|
|
|
add_action( |
326
|
|
|
'wc_gateway_stripe_process_payment_authentication_required', |
327
|
|
|
[ &$mock_action_process_payment, 'action' ] |
328
|
|
|
); |
329
|
|
|
|
330
|
|
|
// Act: call process_subscription_payment(). |
331
|
|
|
// We need to use `wc_stripe_subs_compat` here because we mocked this class earlier. |
332
|
|
|
$result = $this->wc_stripe_subs_compat->process_subscription_payment( 20, $renewal_order, $should_retry, $previous_error ); |
333
|
|
|
|
334
|
|
|
// Assert: nothing was returned. |
335
|
|
|
$this->assertEquals( $result, null ); |
336
|
|
|
|
337
|
|
|
// Assert that we saved the payment intent to the order. |
338
|
|
|
$order_id = $renewal_order->get_id(); |
339
|
|
|
$order = wc_get_order( $order_id ); |
340
|
|
|
$order_data = $order->get_meta( '_stripe_intent_id' ); |
341
|
|
|
$order_transaction_id = $order->get_transaction_id(); |
342
|
|
|
|
343
|
|
|
// Intent was saved to order even though there was an error in the response body. |
344
|
|
|
$this->assertEquals( $order_data, 'pi_123abc' ); |
345
|
|
|
|
346
|
|
|
// Transaction ID was saved to order. |
347
|
|
|
$this->assertEquals( $order_transaction_id, 'ch_123abc' ); |
348
|
|
|
|
349
|
|
|
// Assert: the order was marked as failed. |
350
|
|
|
$this->assertEquals( $order->get_status(), 'failed' ); |
351
|
|
|
|
352
|
|
|
// Assert: called payment intents. |
353
|
|
|
$this->assertTrue( in_array( $payments_intents_api_endpoint, $urls_used ) ); |
354
|
|
|
|
355
|
|
|
// Assert: Our hook was called once. |
356
|
|
|
$this->assertEquals( 1, $mock_action_process_payment->get_call_count() ); |
357
|
|
|
|
358
|
|
|
// Assert: Only our hook was called. |
359
|
|
|
$this->assertEquals( array( 'wc_gateway_stripe_process_payment_authentication_required' ), $mock_action_process_payment->get_tags() ); |
360
|
|
|
|
361
|
|
|
// Clean up. |
362
|
|
|
remove_filter( 'pre_http_request', array( $this, 'pre_http_request_response_success' ) ); |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.