Skip to content

Commit 8cdc64f

Browse files
committed
test: ✅ Add tests for missing order id validation and retry
Cover the three scenarios introduced by execute_order_request(): - Both attempts return 201 with missing id → throws after retry - Both attempts return 201 with empty body → throws after retry - First attempt missing id, retry succeeds → order created
1 parent 9b0dad7 commit 8cdc64f

1 file changed

Lines changed: 261 additions & 1 deletion

File tree

tests/PHPUnit/WcGateway/Endpoint/CaptureCardPaymentTest.php

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
1616
use WooCommerce\PayPalCommerce\Settings\Data\SettingsProvider;
1717
use WooCommerce\PayPalCommerce\TestCase;
18+
use RuntimeException;
1819
use function Brain\Monkey\Functions\expect;
1920
use function Brain\Monkey\Functions\when;
2021

@@ -89,7 +90,7 @@ private function make_testee( string $three_d_secure_enum, array &$captured_body
8990
function ( string $url, array $args ) use ( &$captured_body, $headers_stub ): array {
9091
$captured_body = (array) json_decode( $args['body'], true );
9192
return array(
92-
'body' => '{}',
93+
'body' => '{"id":"MOCK-ORDER-ID"}',
9394
'response' => array( 'code' => 200 ),
9495
'headers' => $headers_stub,
9596
);
@@ -225,4 +226,263 @@ public function three_d_secure_enum_with_verification_provider(): array {
225226
'SCA_ALWAYS maps to verification method SCA_ALWAYS' => array( 'SCA_ALWAYS', 'SCA_ALWAYS' ),
226227
);
227228
}
229+
230+
/**
231+
* Build a CaptureCardPayment with a custom HTTP response for error-path tests.
232+
*
233+
* @param string $response_body Raw JSON body returned by wp_remote_get.
234+
* @param int $response_code HTTP status code.
235+
* @param OrderFactory|null $order_factory Optional custom OrderFactory mock.
236+
* @param LoggerInterface|null $logger Optional custom logger mock.
237+
*/
238+
private function make_testee_with_response(
239+
string $response_body,
240+
int $response_code,
241+
OrderFactory $order_factory = null,
242+
LoggerInterface $logger = null
243+
): CaptureCardPayment {
244+
$token = Mockery::mock( \WooCommerce\PayPalCommerce\ApiClient\Entity\Token::class );
245+
$token->shouldReceive( 'token' )->andReturn( 'test-bearer-token' );
246+
247+
$bearer = Mockery::mock( Bearer::class );
248+
$bearer->shouldReceive( 'bearer' )->andReturn( $token );
249+
250+
$purchase_unit = Mockery::mock( PurchaseUnit::class );
251+
$purchase_unit->shouldReceive( 'to_array' )->andReturn( array() );
252+
253+
$purchase_unit_factory = Mockery::mock( PurchaseUnitFactory::class );
254+
$purchase_unit_factory->shouldReceive( 'from_wc_order' )->andReturn( $purchase_unit );
255+
256+
if ( ! $order_factory ) {
257+
$order_factory = Mockery::mock( OrderFactory::class );
258+
$order_factory->allows( 'from_paypal_response' );
259+
}
260+
261+
$settings_provider = Mockery::mock( SettingsProvider::class );
262+
$settings_provider->shouldReceive( 'payment_intent' )->andReturn( 'capture' );
263+
$settings_provider->shouldReceive( 'three_d_secure_enum' )->andReturn( '' );
264+
265+
$experience_context = Mockery::mock( ExperienceContext::class );
266+
$experience_context->shouldReceive( 'to_array' )->andReturn(
267+
array(
268+
'return_url' => 'https://example.com/return',
269+
'cancel_url' => 'https://example.com/cancel',
270+
)
271+
);
272+
273+
$experience_context_builder = Mockery::mock( ExperienceContextBuilder::class );
274+
$experience_context_builder->allows( 'with_custom_return_url' )->andReturnSelf();
275+
$experience_context_builder->allows( 'with_custom_cancel_url' )->andReturnSelf();
276+
$experience_context_builder->allows( 'with_current_locale' )->andReturnSelf();
277+
$experience_context_builder->allows( 'with_current_brand_name' )->andReturnSelf();
278+
$experience_context_builder->allows( 'build' )->andReturn( $experience_context );
279+
280+
if ( ! $logger ) {
281+
$logger = Mockery::mock( LoggerInterface::class );
282+
}
283+
$logger->allows( 'debug' );
284+
285+
when( 'home_url' )->alias( function ( string $path = '' ): string {
286+
return 'https://example.com' . $path;
287+
} );
288+
when( 'add_query_arg' )->alias( function ( $args, string $url ): string {
289+
return $url;
290+
} );
291+
when( 'wc_get_checkout_url' )->justReturn( 'https://example.com/checkout/' );
292+
when( 'trailingslashit' )->alias( function ( string $value ): string {
293+
return rtrim( $value, '/' ) . '/';
294+
} );
295+
expect( 'wp_json_encode' )->andReturnUsing( 'json_encode' );
296+
when( 'apply_filters' )->alias( function ( string $hook, ...$args ) {
297+
return $args[0] ?? null;
298+
} );
299+
300+
$headers_stub = Mockery::mock( \Requests_Utility_CaseInsensitiveDictionary::class );
301+
$headers_stub->shouldReceive( 'getAll' )->andReturn( array() );
302+
303+
expect( 'wp_remote_get' )->andReturnUsing(
304+
function () use ( $response_body, $response_code, $headers_stub ): array {
305+
return array(
306+
'body' => $response_body,
307+
'response' => array( 'code' => $response_code ),
308+
'headers' => $headers_stub,
309+
);
310+
}
311+
);
312+
when( 'wp_remote_retrieve_response_code' )->alias(
313+
function ( $response ): int {
314+
return (int) ( $response['response']['code'] ?? 0 );
315+
}
316+
);
317+
318+
return new CaptureCardPayment(
319+
'https://api.paypal.com',
320+
$bearer,
321+
$order_factory,
322+
$purchase_unit_factory,
323+
$settings_provider,
324+
$experience_context_builder,
325+
$logger
326+
);
327+
}
328+
329+
/**
330+
* GIVEN PayPal returns HTTP 201 with a JSON body missing the 'id' field on both attempts
331+
* WHEN create_order processes the response
332+
* THEN a RuntimeException is thrown after retry
333+
* AND the logger receives a warning for each failed attempt and an info for the retry
334+
*/
335+
public function test_create_order_throws_when_response_missing_id(): void {
336+
$response_body = '{"status":"CREATED","links":[]}';
337+
338+
$logger = Mockery::mock( LoggerInterface::class );
339+
$logger->shouldReceive( 'warning' )
340+
->twice()
341+
->with(
342+
'PayPal order response missing id.',
343+
Mockery::on( function ( array $context ) use ( $response_body ): bool {
344+
return isset( $context['response_body'] )
345+
&& $context['response_body'] === $response_body;
346+
} )
347+
);
348+
$logger->shouldReceive( 'info' )->once();
349+
350+
$testee = $this->make_testee_with_response( $response_body, 201, null, $logger );
351+
$wc_order = Mockery::mock( WC_Order::class );
352+
$wc_order->shouldReceive( 'get_id' )->andReturn( 1 );
353+
354+
$this->expectException( RuntimeException::class );
355+
$this->expectExceptionMessage( 'missing id after retry' );
356+
$testee->create_order( 'vault-abc-123', $wc_order );
357+
}
358+
359+
/**
360+
* GIVEN PayPal returns HTTP 201 with an empty body on both attempts
361+
* WHEN create_order processes the response
362+
* THEN a RuntimeException is thrown after retry
363+
*/
364+
public function test_create_order_throws_on_empty_response_body(): void {
365+
$logger = Mockery::mock( LoggerInterface::class );
366+
$logger->shouldReceive( 'warning' )->twice();
367+
$logger->shouldReceive( 'info' )->once();
368+
369+
$testee = $this->make_testee_with_response( '', 201, null, $logger );
370+
$wc_order = Mockery::mock( WC_Order::class );
371+
$wc_order->shouldReceive( 'get_id' )->andReturn( 1 );
372+
373+
$this->expectException( RuntimeException::class );
374+
$this->expectExceptionMessage( 'missing id after retry' );
375+
$testee->create_order( 'vault-abc-123', $wc_order );
376+
}
377+
378+
/**
379+
* GIVEN PayPal returns an incomplete response on the first attempt
380+
* AND a valid response with an id on the retry
381+
* WHEN create_order processes the responses
382+
* THEN the order is created successfully from the retried response
383+
*/
384+
public function test_create_order_retries_and_succeeds_on_second_attempt(): void {
385+
$call_count = 0;
386+
387+
$token = Mockery::mock( \WooCommerce\PayPalCommerce\ApiClient\Entity\Token::class );
388+
$token->shouldReceive( 'token' )->andReturn( 'test-bearer-token' );
389+
390+
$bearer = Mockery::mock( Bearer::class );
391+
$bearer->shouldReceive( 'bearer' )->andReturn( $token );
392+
393+
$purchase_unit = Mockery::mock( PurchaseUnit::class );
394+
$purchase_unit->shouldReceive( 'to_array' )->andReturn( array() );
395+
396+
$purchase_unit_factory = Mockery::mock( PurchaseUnitFactory::class );
397+
$purchase_unit_factory->shouldReceive( 'from_wc_order' )->andReturn( $purchase_unit );
398+
399+
$returned_order = Mockery::mock( Order::class );
400+
401+
$order_factory = Mockery::mock( OrderFactory::class );
402+
$order_factory->shouldReceive( 'from_paypal_response' )
403+
->once()
404+
->andReturn( $returned_order );
405+
406+
$settings_provider = Mockery::mock( SettingsProvider::class );
407+
$settings_provider->shouldReceive( 'payment_intent' )->andReturn( 'capture' );
408+
$settings_provider->shouldReceive( 'three_d_secure_enum' )->andReturn( '' );
409+
410+
$experience_context = Mockery::mock( ExperienceContext::class );
411+
$experience_context->shouldReceive( 'to_array' )->andReturn(
412+
array(
413+
'return_url' => 'https://example.com/return',
414+
'cancel_url' => 'https://example.com/cancel',
415+
)
416+
);
417+
418+
$experience_context_builder = Mockery::mock( ExperienceContextBuilder::class );
419+
$experience_context_builder->allows( 'with_custom_return_url' )->andReturnSelf();
420+
$experience_context_builder->allows( 'with_custom_cancel_url' )->andReturnSelf();
421+
$experience_context_builder->allows( 'with_current_locale' )->andReturnSelf();
422+
$experience_context_builder->allows( 'with_current_brand_name' )->andReturnSelf();
423+
$experience_context_builder->allows( 'build' )->andReturn( $experience_context );
424+
425+
$logger = Mockery::mock( LoggerInterface::class );
426+
$logger->allows( 'debug' );
427+
$logger->shouldReceive( 'warning' )->once()->with( 'PayPal order response missing id.', Mockery::any() );
428+
$logger->shouldReceive( 'info' )->once()->with( 'Retrying PayPal order creation after incomplete response.' );
429+
430+
when( 'home_url' )->alias( function ( string $path = '' ): string {
431+
return 'https://example.com' . $path;
432+
} );
433+
when( 'add_query_arg' )->alias( function ( $args, string $url ): string {
434+
return $url;
435+
} );
436+
when( 'wc_get_checkout_url' )->justReturn( 'https://example.com/checkout/' );
437+
when( 'trailingslashit' )->alias( function ( string $value ): string {
438+
return rtrim( $value, '/' ) . '/';
439+
} );
440+
expect( 'wp_json_encode' )->andReturnUsing( 'json_encode' );
441+
when( 'apply_filters' )->alias( function ( string $hook, ...$args ) {
442+
return $args[0] ?? null;
443+
} );
444+
445+
$headers_stub = Mockery::mock( \Requests_Utility_CaseInsensitiveDictionary::class );
446+
$headers_stub->shouldReceive( 'getAll' )->andReturn( array() );
447+
448+
expect( 'wp_remote_get' )->andReturnUsing(
449+
function () use ( &$call_count, $headers_stub ): array {
450+
$call_count++;
451+
if ( 1 === $call_count ) {
452+
return array(
453+
'body' => '{"status":"CREATED","links":[]}',
454+
'response' => array( 'code' => 201 ),
455+
'headers' => $headers_stub,
456+
);
457+
}
458+
return array(
459+
'body' => '{"id":"ORDER-123","status":"CREATED"}',
460+
'response' => array( 'code' => 201 ),
461+
'headers' => $headers_stub,
462+
);
463+
}
464+
);
465+
when( 'wp_remote_retrieve_response_code' )->alias(
466+
function ( $response ): int {
467+
return (int) ( $response['response']['code'] ?? 0 );
468+
}
469+
);
470+
471+
$testee = new CaptureCardPayment(
472+
'https://api.paypal.com',
473+
$bearer,
474+
$order_factory,
475+
$purchase_unit_factory,
476+
$settings_provider,
477+
$experience_context_builder,
478+
$logger
479+
);
480+
$wc_order = Mockery::mock( WC_Order::class );
481+
$wc_order->shouldReceive( 'get_id' )->andReturn( 1 );
482+
483+
$result = $testee->create_order( 'vault-abc-123', $wc_order );
484+
485+
$this->assertSame( $returned_order, $result );
486+
}
487+
228488
}

0 commit comments

Comments
 (0)