Skip to content

Commit 58d60f2

Browse files
committed
test: ✅ Update and add tests for the changes
1 parent 407c010 commit 58d60f2

3 files changed

Lines changed: 275 additions & 8 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PHPUnit\PpcpSettings\Data;
5+
6+
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
7+
use WooCommerce\PayPalCommerce\TestCase;
8+
use function Brain\Monkey\Actions\expectDone;
9+
use function Brain\Monkey\Functions\when;
10+
11+
/**
12+
* @covers \WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel
13+
*/
14+
class AbstractDataModelTest extends TestCase
15+
{
16+
public function setUp(): void
17+
{
18+
parent::setUp();
19+
when('get_option')->justReturn([]);
20+
}
21+
22+
private function create_testee(): TestableDataModel
23+
{
24+
return new TestableDataModel();
25+
}
26+
27+
/**
28+
* GIVEN a listener on the "settings saved" action that calls save() again
29+
* WHEN the initial save() dispatches the action
30+
* THEN the nested save() still persists the data (a second update_option call)
31+
* AND the action is dispatched only once overall, preventing a notification loop
32+
*/
33+
public function testReentrantSaveStillPersistsButFiresActionOnlyOnce(): void
34+
{
35+
$update_calls = [];
36+
when('update_option')->alias(function (string $key, $data) use (&$update_calls) {
37+
$update_calls[] = $data;
38+
return true;
39+
});
40+
41+
$listener_calls = 0;
42+
expectDone('woocommerce_paypal_payments_settings_saved')
43+
->once()
44+
->whenHappen(function ($model) use (&$listener_calls) {
45+
$listener_calls++;
46+
$model->save();
47+
});
48+
49+
$model = $this->create_testee();
50+
$model->save();
51+
52+
$this->assertSame(2, count($update_calls), 'Both the initial and the re-entrant save should persist the data');
53+
$this->assertSame(1, $listener_calls, 'The saved action must not be dispatched again by the re-entrant save');
54+
}
55+
}
56+
57+
/**
58+
* Minimal concrete subclass used only to exercise AbstractDataModel's save()/
59+
* fire_saved_action() behavior.
60+
*/
61+
class TestableDataModel extends AbstractDataModel
62+
{
63+
protected const OPTION_KEY = 'test_option_key';
64+
65+
protected function get_defaults(): array
66+
{
67+
return ['foo' => ''];
68+
}
69+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
declare( strict_types = 1 );
3+
4+
namespace WooCommerce\PayPalCommerce\StoreSync\Registration;
5+
6+
use Mockery;
7+
use Psr\Log\LoggerInterface;
8+
use WooCommerce\PayPalCommerce\StoreSync\Setting\AgenticSettingsDataModel;
9+
use WooCommerce\PayPalCommerce\TestCase;
10+
use WP_Error;
11+
12+
/**
13+
* @covers \WooCommerce\PayPalCommerce\StoreSync\Registration\ReconciliationService
14+
*/
15+
class ReconciliationServiceTest extends TestCase {
16+
17+
/**
18+
* @var AgenticSettingsDataModel|Mockery\MockInterface
19+
*/
20+
private $settings;
21+
22+
/**
23+
* @var RegistrationService|Mockery\MockInterface
24+
*/
25+
private $registration;
26+
27+
/**
28+
* @var LoggerInterface|Mockery\MockInterface
29+
*/
30+
private $logger;
31+
32+
public function setUp(): void {
33+
parent::setUp();
34+
35+
$this->settings = Mockery::mock( AgenticSettingsDataModel::class );
36+
$this->registration = Mockery::mock( RegistrationService::class );
37+
38+
$this->logger = Mockery::mock( LoggerInterface::class )->shouldIgnoreMissing();
39+
}
40+
41+
private function create_testee(): ReconciliationService {
42+
return new ReconciliationService( $this->settings, $this->registration, $this->logger );
43+
}
44+
45+
/**
46+
* GIVEN a store that should initialize agentic features but is not yet registered
47+
* WHEN reconciling the registration state
48+
* AND the registration call succeeds
49+
* THEN the store is registered
50+
* AND the toggle is left active, since registration succeeded
51+
*/
52+
public function test_reconcile_registers_store_when_desired_but_not_registered(): void {
53+
$this->settings->shouldReceive( 'should_initialize_features' )->andReturn( true );
54+
$this->registration->shouldReceive( 'is_registered' )->andReturn( false );
55+
56+
$this->registration->shouldReceive( 'register' )
57+
->once()
58+
->andReturn( new RegistrationResult( true, 'Registered', null ) );
59+
60+
$this->settings->shouldNotReceive( 'set_active' );
61+
$this->settings->shouldNotReceive( 'set_last_registration_error' );
62+
$this->settings->shouldNotReceive( 'save' );
63+
64+
$this->create_testee()->reconcile();
65+
66+
$this->addToAssertionCount( 4 );
67+
}
68+
69+
/**
70+
* GIVEN a store that should initialize agentic features but is not yet registered
71+
* WHEN reconciling the registration state
72+
* AND the registration call fails
73+
* THEN the toggle is switched off, so a failure is never retried automatically
74+
* AND the failure reason is logged for troubleshooting
75+
* AND the settings are saved
76+
*/
77+
public function test_reconcile_disables_toggle_when_registration_fails(): void {
78+
$this->settings->shouldReceive( 'should_initialize_features' )->andReturn( true );
79+
$this->registration->shouldReceive( 'is_registered' )->andReturn( false );
80+
81+
$this->registration->shouldReceive( 'register' )
82+
->once()
83+
->andReturn( new WP_Error( 'registration_failed', 'store not found' ) );
84+
85+
$this->settings->shouldReceive( 'set_active' )->once()->with( false );
86+
$this->settings->shouldReceive( 'save' )->once();
87+
88+
$this->create_testee()->reconcile();
89+
90+
$this->addToAssertionCount( 3 );
91+
}
92+
93+
/**
94+
* GIVEN a store that is registered but should no longer have agentic features active
95+
* WHEN reconciling the registration state
96+
* THEN the store is deregistered
97+
*/
98+
public function test_reconcile_deregisters_store_when_no_longer_desired(): void {
99+
$this->settings->shouldReceive( 'should_initialize_features' )->andReturn( false );
100+
$this->registration->shouldReceive( 'is_registered' )->andReturn( true );
101+
102+
$this->registration->shouldReceive( 'deregister' )->once();
103+
$this->registration->shouldNotReceive( 'register' );
104+
105+
$this->create_testee()->reconcile();
106+
107+
$this->addToAssertionCount( 2 );
108+
}
109+
110+
/**
111+
* GIVEN the desired and actual registration state already match
112+
* WHEN reconciling the registration state
113+
* THEN neither registration nor deregistration is attempted
114+
*
115+
* @dataProvider matching_state_provider
116+
*/
117+
public function test_reconcile_does_nothing_when_state_already_matches( bool $desired, bool $actual ): void {
118+
$this->settings->shouldReceive( 'should_initialize_features' )->andReturn( $desired );
119+
$this->registration->shouldReceive( 'is_registered' )->andReturn( $actual );
120+
121+
$this->registration->shouldNotReceive( 'register' );
122+
$this->registration->shouldNotReceive( 'deregister' );
123+
124+
$this->create_testee()->reconcile();
125+
126+
$this->addToAssertionCount( 2 );
127+
}
128+
129+
public function matching_state_provider(): array {
130+
return array(
131+
'already registered and still desired' => array( true, true ),
132+
'already deregistered and no longer desired' => array( false, false ),
133+
);
134+
}
135+
136+
/**
137+
* GIVEN auto-registration has been disabled via the PPCP_AGENTIC_AUTO_REGISTER constant
138+
* AND a store that should initialize agentic features but is not yet registered
139+
* WHEN reconciling the registration state
140+
* THEN registration is skipped
141+
*
142+
* @runInSeparateProcess
143+
* @preserveGlobalState disabled
144+
*/
145+
public function test_reconcile_skips_registration_when_auto_register_is_disabled(): void {
146+
define( 'PPCP_AGENTIC_AUTO_REGISTER', false );
147+
148+
$this->settings->shouldReceive( 'should_initialize_features' )->andReturn( true );
149+
$this->registration->shouldReceive( 'is_registered' )->andReturn( false );
150+
151+
$this->registration->shouldNotReceive( 'register' );
152+
$this->registration->shouldNotReceive( 'deregister' );
153+
154+
$this->create_testee()->reconcile();
155+
156+
$this->addToAssertionCount( 2 );
157+
}
158+
}

tests/PHPUnit/StoreSync/Registration/RegistrationServiceTest.php

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use WooCommerce\PayPalCommerce\StoreSync\Merchant\MerchantMetadataProvider;
1111
use WooCommerce\PayPalCommerce\TestCase;
1212
use WP_Error;
13+
use function Brain\Monkey\Actions\expectDone;
1314
use function Brain\Monkey\Functions\expect;
1415
use function Brain\Monkey\Functions\when;
1516

@@ -145,6 +146,7 @@ public function test_registration_succeeds_with_valid_merchant_data(): void {
145146
* AND the webhook returns a failure response
146147
* THEN registration should fail with error
147148
* AND the registration token should not be saved
149+
* AND no local token should be deleted, since none exists to remove
148150
*/
149151
public function test_registration_fails_when_webhook_returns_error(): void {
150152
$this->stub_merchant_metadata();
@@ -159,6 +161,7 @@ public function test_registration_fails_when_webhook_returns_error(): void {
159161
$this->assertSame( 'registration_failed', $result->get_error_code() );
160162
$this->assertSame( 'Invalid merchant data', $result->get_error_message() );
161163
$this->assertFalse( $testee->was_token_saved );
164+
$this->assertFalse( $testee->was_token_deleted );
162165
}
163166

164167
/**
@@ -259,25 +262,62 @@ public function test_deregistration_succeeds_for_registered_store(): void {
259262
/**
260263
* GIVEN a registered store
261264
* WHEN deregistering from PayPal Agentic Commerce
262-
* AND the webhook returns a failure response
263-
* THEN deregistration should fail with error
264-
* AND the store should still be considered registered
265+
* AND the endpoint rejects the deregistration (e.g. "store not found")
266+
* THEN the local registration token is still deleted, since the endpoint already
267+
* considers the store gone
268+
* AND the deregistered action still fires
269+
* AND the store is no longer considered registered
265270
*/
266-
public function test_deregistration_fails_when_webhook_returns_error(): void {
271+
public function test_deregistration_deletes_token_when_endpoint_rejects_it(): void {
267272
$this->webhook_config->method( 'get_registration_uninstall_url' )
268273
->willReturn( 'https://d-staging.joinhoney.com/webhooks/ws/uninstall' );
269-
$this->stub_failed_webhook_response( 'Token not found' );
274+
$this->stub_failed_webhook_response( 'store not found' );
270275

271276
$testee = $this->create_testable_service( true );
272277

273278
$this->assertTrue( $testee->is_registered(), 'Store should be registered before deregistration attempt' );
274279

280+
expectDone( 'woocommerce_paypal_payments_store_sync_deregistered' )->once();
281+
282+
$result = $testee->deregister();
283+
284+
$this->assertInstanceOf( RegistrationResult::class, $result );
285+
$this->assertFalse( $result->success );
286+
$this->assertTrue( $testee->was_token_deleted );
287+
$this->assertFalse( $testee->is_registered(), 'Store should no longer be registered once the local token is removed' );
288+
}
289+
290+
/**
291+
* GIVEN a registered store
292+
* WHEN deregistering from PayPal Agentic Commerce
293+
* AND the webhook request fails with a transport error
294+
* THEN the local registration token is still deleted (fire-and-forget)
295+
* AND the deregistered action still fires
296+
* AND the transport error is returned to the caller
297+
*/
298+
public function test_deregistration_deletes_token_on_transport_error(): void {
299+
$this->webhook_config->method( 'get_registration_uninstall_url' )
300+
->willReturn( 'https://d-staging.joinhoney.com/webhooks/ws/uninstall' );
301+
302+
$wp_error = new WP_Error( 'http_request_failed', 'Connection timeout' );
303+
304+
when( 'wp_remote_post' )->justReturn( $wp_error );
305+
when( 'is_wp_error' )->alias(
306+
function ( $thing ) {
307+
return $thing instanceof WP_Error;
308+
}
309+
);
310+
311+
$testee = $this->create_testable_service( true );
312+
313+
expectDone( 'woocommerce_paypal_payments_store_sync_deregistered' )->once();
314+
275315
$result = $testee->deregister();
276316

277317
$this->assertInstanceOf( WP_Error::class, $result );
278-
$this->assertSame( 'deregistration_failed', $result->get_error_code() );
279-
$this->assertSame( 'Token not found', $result->get_error_message() );
280-
$this->assertTrue( $testee->is_registered(), 'Store should still be registered after failed deregistration' );
318+
$this->assertSame( 'webhook_request_failed', $result->get_error_code() );
319+
$this->assertTrue( $testee->was_token_deleted );
320+
$this->assertFalse( $testee->is_registered() );
281321
}
282322

283323
/**

0 commit comments

Comments
 (0)