Skip to content
Open
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
284 changes: 196 additions & 88 deletions tests/Support/Helper/KitAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
*/
class KitAPI extends \Codeception\Module
{
/**
* Installs the Kit API recorder mu-plugin, and clears any previously recorded
* requests, before each test runs.
*
* @since 1.9.7
*
* @param \Codeception\TestInterface $test Test.
*/
public function _before(\Codeception\TestInterface $test) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
{
$this->getModule('lucatume\WPBrowser\Module\WPFilesystem')->haveMuPlugin(
'kit-api-recorder.php',
(string) file_get_contents(__DIR__ . '/../mu-plugins/kit-api-recorder.php') // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
);

$this->getModule('lucatume\WPBrowser\Module\WPDb')->haveOptionInDatabase('kit_api_log', []);
}

/**
* Returns an encoded `state` parameter compatible with OAuth.
*
Expand Down Expand Up @@ -39,10 +57,83 @@ public function apiEncodeState($returnTo, $clientID)
return $str;
}

/**
* Returns the Kit API requests the Plugin made during this test, optionally
* filtered by method, path and email address.
*
* @since 1.9.7
*
* @param EndToEndTester $I Tester.
* @param bool|string $method HTTP method (GET,POST,PUT,DELETE).
* @param bool|string $path Request path, excluding the API version e.g. `subscribers`.
* @param bool|string $emailAddress Email address in the request body.
* @return array
*/
public function grabKitAPIRequests($I, $method = false, $path = false, $emailAddress = false)
{
$log = $I->grabOptionFromDatabase('kit_api_log');

if ( ! is_array($log)) {
return [];
}

return array_values(
array_filter(
$log,
function ($request) use ($method, $path, $emailAddress) {
if ($method && $request['method'] !== $method) {
return false;
}
if ($path && $request['path'] !== $path) {
return false;
}
if ($emailAddress && ( ! array_key_exists('email_address', $request['body']) || $request['body']['email_address'] !== $emailAddress )) {
return false;
}

return true;
}
)
);
}

/**
* Returns the first Kit API request the Plugin made during this test that matches
* the given method, path and email address, waiting for it to be made.
*
* @since 1.9.7
*
* @param EndToEndTester $I Tester.
* @param string $method HTTP method (GET,POST,PUT,DELETE).
* @param string $path Request path, excluding the API version e.g. `subscribers`.
* @param bool|string $emailAddress Email address in the request body.
* @return bool|array
*/
public function grabKitAPIRequest($I, $method, $path, $emailAddress = false)
{
// The request is made by WordPress when the form is submitted, which may not have
// completed when this is called e.g. when a form submits using AJAX.
return $this->retryUntil(
function () use ($I, $method, $path, $emailAddress) {
$requests = $this->grabKitAPIRequests($I, $method, $path, $emailAddress);

return count($requests) ? $requests[0] : false;
},
10,
1
);
}

/**
* Check the given email address exists as a subscriber, and optionally
* checks that the first name and custom fields contain the expected data.
*
* The Plugin's request to create the subscriber is used to determine the subscriber ID,
* as querying the API by email address is subject to eventual consistency. Querying by
* subscriber ID returns strongly consistent results.
*
* @see https://developers.kit.com/api-reference/eventual-consistency
*
* @since 1.4.0
*
* @param EndToEndTester $I Tester.
Expand All @@ -53,76 +144,67 @@ public function apiEncodeState($returnTo, $clientID)
*/
public function apiCheckSubscriberExists($I, $emailAddress, $firstName = false, $customFields = false)
{
// Wait for the API to update.
$I->wait(3);

// Retry the API request as sometimes there's a lag before the subscriber is queryable via the API.
$results = $this->retryUntil(
function () use ($emailAddress) {
$results = $this->apiRequest(
'subscribers',
'GET',
[
'email_address' => $emailAddress,
'include_total_count' => true,
// Get the request the Plugin made to create the subscriber.
$request = $this->grabKitAPIRequest($I, 'POST', 'subscribers', $emailAddress);

// Check all subscriber states.
'status' => 'all',
]
);

// Return the results only if a subscriber was found, so
// retryUntil() will keep trying otherwise.
return ( $results['pagination']['total_count'] > 0 ) ? $results : false;
}
// Check the Plugin created the subscriber.
$I->assertNotFalse(
$request,
sprintf('The Plugin did not send a request to create the subscriber %s.', $emailAddress)
);
$I->assertLessThan(
300,
$request['code'],
sprintf('The API returned a %s response when the Plugin created the subscriber %s.', $request['code'], $emailAddress)
);

// Check at least one subscriber was returned and it matches the email address.
$I->assertNotFalse($results);
$I->assertGreaterThan(0, $results['pagination']['total_count']);
$I->assertEquals($emailAddress, $results['subscribers'][0]['email_address']);
// Fetch the subscriber by their ID, which returns strongly consistent results.
$results = $this->apiRequest('subscribers/' . $request['response']['subscriber']['id'], 'GET');

// Check the subscriber matches the email address.
$I->assertEquals($emailAddress, $results['subscriber']['email_address']);

// If a first name was provided, check it matches.
if ($firstName) {
$I->assertEquals($firstName, $results['subscribers'][0]['first_name']);
$I->assertEquals($firstName, $results['subscriber']['first_name']);
}

// If custom fields are provided, check they exist.
if ($customFields) {
foreach ($customFields as $customField => $customFieldValue) {
$I->assertEquals($results['subscribers'][0]['fields'][ $customField ], $customFieldValue);
$I->assertEquals($results['subscriber']['fields'][ $customField ], $customFieldValue);
}
}

// Return subscriber ID.
return $results['subscribers'][0]['id'];
return $results['subscriber']['id'];
}

/**
* Check the given email address does not exists as a subscriber.
*
* The Plugin's requests are inspected, instead of querying the API by email address,
* as querying by email address is subject to eventual consistency and would therefore
* return no subscriber even when one was created.
*
* @see https://developers.kit.com/api-reference/eventual-consistency
*
* @since 1.4.0
*
* @param EndToEndTester $I Tester.
* @param string $emailAddress Email Address.
*/
public function apiCheckSubscriberDoesNotExist($I, $emailAddress)
{
// Run request.
$results = $this->apiRequest(
'subscribers',
'GET',
[
'email_address' => $emailAddress,
'include_total_count' => true,

// Some test email addresses might bounce, so we want to check all subscriber states.
'status' => 'all',
]
// Get any requests the Plugin made to create the subscriber.
$requests = $this->grabKitAPIRequests($I, 'POST', 'subscribers', $emailAddress);

// Check the Plugin did not create the subscriber.
$I->assertCount(
0,
$requests,
sprintf('The Plugin sent a request to create the subscriber %s.', $emailAddress)
);

// Check no subscribers are returned by this request.
$I->assertEquals(0, $results['pagination']['total_count']);
}

/**
Expand All @@ -137,27 +219,35 @@ public function apiCheckSubscriberDoesNotExist($I, $emailAddress)
*/
public function apiCheckSubscriberHasForm($I, $subscriberID, $formID, $referrer = false)
{
// Run request.
$results = $this->apiRequest(
'forms/' . $formID . '/subscribers',
'GET',
[
// Check all subscriber states.
'status' => 'all',
]
);
// Wait for the subscriber to be assigned to the form, as list endpoints are eventually consistent.
$subscriber = $this->retryUntil(
function () use ($subscriberID, $formID) {
$results = $this->apiRequest(
'forms/' . $formID . '/subscribers',
'GET',
[
// Check all subscriber states.
'status' => 'all',
]
);

// Iterate through subscribers.
$subscriberHasForm = false;
foreach ($results['subscribers'] as $subscriber) {
if ($subscriber['id'] === $subscriberID) {
$subscriberHasForm = true;
break;
// Return the subscriber only if they're assigned to the form, so
// retryUntil() will keep trying otherwise.
foreach ($results['subscribers'] as $subscriber) {
if ( (int) $subscriber['id'] === (int) $subscriberID) {
return $subscriber;
}
}

return false;
}
}
);

// Assert if the subscriber has the form.
$this->assertTrue($subscriberHasForm);
// Assert the subscriber has the form.
$I->assertNotFalse(
$subscriber,
sprintf('Subscriber %s was not assigned to Form %s in time.', $subscriberID, $formID)
);

// If a referrer is specified, assert it matches the subscriber's referrer now.
if ($referrer) {
Expand All @@ -176,23 +266,31 @@ public function apiCheckSubscriberHasForm($I, $subscriberID, $formID, $referrer
*/
public function apiCheckSubscriberHasSequence($I, $subscriberID, $sequenceID)
{
// Run request.
$results = $this->apiRequest(
'sequences/' . $sequenceID . '/subscribers',
'GET'
);
// Wait for the subscriber to be assigned to the sequence, as list endpoints are eventually consistent.
$subscriber = $this->retryUntil(
function () use ($subscriberID, $sequenceID) {
$results = $this->apiRequest(
'sequences/' . $sequenceID . '/subscribers',
'GET'
);

// Iterate through subscribers.
$subscriberHasSequence = false;
foreach ($results['subscribers'] as $subscriber) {
if ($subscriber['id'] === $subscriberID) {
$subscriberHasSequence = true;
break;
// Return the subscriber only if they're assigned to the sequence, so
// retryUntil() will keep trying otherwise.
foreach ($results['subscribers'] as $subscriber) {
if ( (int) $subscriber['id'] === (int) $subscriberID) {
return $subscriber;
}
}

return false;
}
}
);

// Assert if the subscriber has the sequence.
$this->assertTrue($subscriberHasSequence);
// Assert the subscriber has the sequence.
$I->assertNotFalse(
$subscriber,
sprintf('Subscriber %s was not assigned to Sequence %s in time.', $subscriberID, $sequenceID)
);
}

/**
Expand All @@ -206,19 +304,29 @@ public function apiCheckSubscriberHasSequence($I, $subscriberID, $sequenceID)
*/
public function apiCheckSubscriberHasTag($I, $subscriberID, $tagID)
{
// Get subscriber tags.
$subscriberTags = $this->apiGetSubscriberTags($subscriberID);
// Wait for the tag to be assigned to the subscriber, as list endpoints are eventually consistent.
$tag = $this->retryUntil(
function () use ($subscriberID, $tagID) {
// Get subscriber tags.
$subscriberTags = $this->apiGetSubscriberTags($subscriberID);

$subscriberTagged = false;
foreach ($subscriberTags as $tag) {
if ( (int) $tag['id'] === (int) $tagID) {
$subscriberTagged = true;
break;
// Return the tag only if it's assigned to the subscriber, so
// retryUntil() will keep trying otherwise.
foreach ($subscriberTags as $tag) {
if ( (int) $tag['id'] === (int) $tagID) {
return $tag;
}
}

return false;
}
}
);

// Check that the Subscriber is tagged.
$I->assertTrue($subscriberTagged);
// Assert the subscriber has the tag.
$I->assertNotFalse(
$tag,
sprintf('Subscriber %s was not assigned Tag %s in time.', $subscriberID, $tagID)
);
}

/**
Expand Down Expand Up @@ -274,8 +382,8 @@ public function apiRequest($endpoint, $method = 'GET', $params = array())
[
'headers' => [
'Authorization' => 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'timeout' => 5,
],
'timeout' => 5,
]
);
break;
Expand All @@ -289,8 +397,8 @@ public function apiRequest($endpoint, $method = 'GET', $params = array())
'Accept' => 'application/json',
'Content-Type' => 'application/json; charset=utf-8',
'Authorization' => 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'timeout' => 5,
],
'timeout' => 5,
'body' => (string) json_encode($params), // phpcs:ignore WordPress.WP.AlternativeFunctions
]
);
Expand All @@ -306,8 +414,8 @@ public function apiRequest($endpoint, $method = 'GET', $params = array())
* the maximum number of attempts is reached.
*
* Use this to wrap API checks that can be flaky due to ingestion lag at
* Kit's end (e.g. a subscriber created via a form submission isn't always
* immediately queryable via the `subscribers` endpoint).
* Kit's end (e.g. a subscriber assigned to a form isn't always immediately
* returned by the `forms/{id}/subscribers` endpoint).
*
* @since 1.9.4
*
Expand Down
Loading