Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Comment thread
n7studios marked this conversation as resolved.
$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);
}

Expand Down Expand Up @@ -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.
*
Expand Down
94 changes: 82 additions & 12 deletions tests/Integration/TestsTrait.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* Holds tests for the Kit API.
* Test methods in ConvertKit_API_Traits that interact with the API.
*/
trait TestsTrait
{
Expand Down Expand Up @@ -4925,8 +4925,8 @@ public function testUnsubscribeByEmail()
email_address: $emailAddress
);

// Wait a moment to ensure subscriber is created.
sleep(3);
// Wait until the subscriber can be found by their email address.
$this->waitForSubscriber($emailAddress);

// Unsubscribe.
$this->assertNull($this->api->unsubscribe_by_email($emailAddress));
Expand Down Expand Up @@ -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
);
}

/**
Expand Down Expand Up @@ -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
);
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down