From a160828dc55df84af573522414eb0b38a6333e7e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 8 Sep 2026 17:35:46 +0800 Subject: [PATCH 1/2] Add retryUntil and waitForSubscriber --- tests/Integration/APITest.php | 14 ++++- tests/Integration/TestsTrait.php | 94 ++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 026d51c..82e5df5 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -184,16 +184,26 @@ public function tearDown(): void * (never as a thrown exception). We accept both return and throw so * that any input-validation code that throws still counts. * + * Where the SDK validates arguments before performing an API request, specify + * $expected, to assert that validation produced the error and not the API. + * * @since 2.0.5 + * @since 2.7.0 Added the $expected parameter. * - * @param callable $fn Callable that should fail. + * @param callable $fn Callable that should fail. + * @param string|null $expected Expected exception class name. * @return void */ - protected function assertApiError(callable $fn): void + protected function assertApiError(callable $fn, string|null $expected = null): void { try { $result = $fn(); } catch (\Throwable $e) { + if ( ! is_null($expected)) { + $this->assertInstanceOf($expected, $e); + return; + } + $this->assertTrue(true, 'Callable threw an exception as expected.'); return; } diff --git a/tests/Integration/TestsTrait.php b/tests/Integration/TestsTrait.php index 4becfdd..e0083a2 100644 --- a/tests/Integration/TestsTrait.php +++ b/tests/Integration/TestsTrait.php @@ -1,7 +1,7 @@ waitForSubscriber($emailAddress); // Unsubscribe. $this->assertNull($this->api->unsubscribe_by_email($emailAddress)); @@ -6161,12 +6161,15 @@ public function testCreateWebhookWithEventParameter() */ public function testCreateWebhookWithInvalidEvent() { - $this->assertApiError(function () { - return $this->api->create_webhook( - url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), - event: 'invalid.event' - ); - }); + $this->assertApiError( + function () { + return $this->api->create_webhook( + url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), + event: 'invalid.event' + ); + }, + \InvalidArgumentException::class + ); } /** @@ -6900,9 +6903,12 @@ public function testGetResourceLegacyLandingPage() */ public function testGetResourceInvalidURL() { - $this->assertApiError(function () { - return $this->api->get_resource('not-a-url'); - }); + $this->assertApiError( + function () { + return $this->api->get_resource('not-a-url'); + }, + \InvalidArgumentException::class + ); } /** @@ -6938,6 +6944,70 @@ public function generateEmailAddress($domain = 'kit.com') return 'php-sdk-' . date('Y-m-d-H-i-s') . '-php-' . PHP_VERSION_ID . '@' . $domain; } + /** + * Repeatedly invokes the given callback until it returns a truthy value, or the + * maximum number of attempts is reached. + * + * Use this to wrap API checks that can be flaky due to eventual consistency at Kit's + * end. List endpoints typically reflect a write within ~30 seconds, and can take up + * to 5 minutes, so reading back immediately after a write is not reliable. + * + * @since 2.7.0 + * + * @see https://developers.kit.com/api-reference/eventual-consistency + * + * @param callable $callback Callback to invoke. Should return the value to use, or + * false / null when the check has not yet succeeded. + * @param integer $attempts Maximum number of attempts. + * @param integer $delay Seconds to wait between attempts. + * @return mixed Value returned by the callback, or false if all attempts are exhausted. + */ + public function retryUntil(callable $callback, $attempts = 20, $delay = 5) + { + for ($i = 0; $i < $attempts; $i++) { + $result = $callback(); + + if ($result) { + return $result; + } + + // Don't sleep after the final attempt. + if ($i < ($attempts - 1)) { + sleep($delay); + } + } + + return false; + } + + /** + * Waits for the given email address to be queryable by get_subscriber_id(), returning + * the Subscriber ID, and failing the test if it never becomes queryable. + * + * @since 2.7.0 + * + * @param string $emailAddress Email Address. + * @return integer Subscriber ID. + */ + public function waitForSubscriber($emailAddress) + { + $subscriberID = $this->retryUntil( + function () use ($emailAddress) { + $subscriberID = $this->api->get_subscriber_id($emailAddress); + + // WP Libraries returns a WP_Error on failure; keep retrying unless we have an ID. + return is_numeric($subscriberID) ? (int) $subscriberID : false; + } + ); + + $this->assertNotFalse( + $subscriberID, + sprintf('Subscriber %s was not returned by the API in time.', $emailAddress) + ); + + return $subscriberID; + } + /** * Checks if string is html. * From 59457c48688078f3edf98773010421b027f03230 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 9 Sep 2026 10:45:05 +0800 Subject: [PATCH 2/2] Fix WP_Error being ignored --- tests/Integration/APITest.php | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 82e5df5..61a8850 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -184,8 +184,10 @@ public function tearDown(): void * (never as a thrown exception). We accept both return and throw so * that any input-validation code that throws still counts. * - * Where the SDK validates arguments before performing an API request, specify - * $expected, to assert that validation produced the error and not the API. + * Where the code validates arguments before performing an API request, specify + * $expected, to assert that validation produced the error and not the API. A + * returned WP_Error does not satisfy $expected, so a test that expects an + * exception fails if the validation is removed. * * @since 2.0.5 * @since 2.7.0 Added the $expected parameter. @@ -207,6 +209,14 @@ protected function assertApiError(callable $fn, string|null $expected = null): v $this->assertTrue(true, 'Callable threw an exception as expected.'); return; } + + // An exception was expected, so a returned WP_Error isn't the error we asked to assert. + if ( ! is_null($expected)) { + $this->fail( + sprintf('Expected %s to be thrown, but the callable returned instead.', $expected) + ); + } + $this->assertInstanceOf(\WP_Error::class, $result); } @@ -1691,6 +1701,26 @@ public function testGetAllPostsWithInvalidPostsPerRequestParameter() $this->assertEquals('get_all_posts(): the posts_per_request parameter must be equal to or less than 50.', $result->get_error_message()); } + /** + * Test that get_resource() returns a WP_Error when an invalid URL is specified. + * + * Overrides the version in TestsTrait: the PHP SDK validates the URL and throws an + * InvalidArgumentException, whereas WordPress Libraries passes the URL to + * wp_remote_get(), which returns a WP_Error. + * + * @since 2.7.0 + * + * @return void + */ + public function testGetResourceInvalidURL() + { + $this->assertApiError( + function () { + return $this->api->get_resource('not-a-url'); + } + ); + } + /** * Test that the `get_post()` function returns expected data. *