diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 026d51c..61a8850 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -184,19 +184,39 @@ 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 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. * - * @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; } + + // 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); } @@ -1681,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. * 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. *