From c9a42b77045c3982caf0ac8afd083c0ce4677285 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 28 Jul 2026 15:47:05 +0800 Subject: [PATCH 01/15] Update Traits from PHP SDK --- src/class-convertkit-api-traits.php | 851 ++++++++++++++++++++++++++-- 1 file changed, 790 insertions(+), 61 deletions(-) diff --git a/src/class-convertkit-api-traits.php b/src/class-convertkit-api-traits.php index 1244373..8b73a8f 100644 --- a/src/class-convertkit-api-traits.php +++ b/src/class-convertkit-api-traits.php @@ -156,11 +156,12 @@ public function get_growth_stats(?\DateTime $starting = null, ?\DateTime $ending /** * List forms. * - * @param string $status Form status (active|archived|trashed|all). - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param string $status Form status (active|archived|trashed|all). + * @param array $include Additional fields to include: subscriber_count. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @see https://developers.kit.com/api-reference/forms/list-forms * @@ -168,18 +169,26 @@ public function get_growth_stats(?\DateTime $starting = null, ?\DateTime $ending */ public function get_forms( string $status = 'active', + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { + // Build parameters. + $options = [ + 'type' => 'embed', + 'status' => $status, + ]; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + return $this->get( 'forms', $this->build_total_count_and_pagination_params( - [ - 'type' => 'embed', - 'status' => $status, - ], + $options, $include_total_count, $after_cursor, $before_cursor, @@ -330,6 +339,7 @@ public function add_subscriber_to_legacy_form(int $form_id, int $subscriber_id) * @param \DateTime|null $created_before Filter subscribers who have been created before this date. * @param \DateTime|null $added_after Filter subscribers who have been added to the form after this date. * @param \DateTime|null $added_before Filter subscribers who have been added to the form before this date. + * @param boolean $slim When true, omits expensive optional fields from the response. * @param boolean $include_total_count To include the total count of records in the response, use true. * @param string $after_cursor Return results after the given pagination cursor. * @param string $before_cursor Return results before the given pagination cursor. @@ -346,13 +356,14 @@ public function get_form_subscriptions( ?\DateTime $created_before = null, ?\DateTime $added_after = null, ?\DateTime $added_before = null, + bool $slim = false, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { // Build parameters. - $options = []; + $options = ['slim' => $slim]; if (!empty($subscriber_state)) { $options['status'] = $subscriber_state; @@ -386,25 +397,34 @@ public function get_form_subscriptions( /** * List sequences * - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param array $include Additional fields to include: stats. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @see https://developers.kit.com/api-reference/sequences/list-sequences * * @return false|mixed */ public function get_sequences( + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { + // Build parameters. + $options = []; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + return $this->get( 'sequences', $this->build_total_count_and_pagination_params( - [], + $options, $include_total_count, $after_cursor, $before_cursor, @@ -413,6 +433,166 @@ public function get_sequences( ); } + /** + * Create a sequence + * + * @param string $name The name of the sequence. + * @param string $email_address The sending email address to use. Uses the account's sending email address if not provided. + * @param integer $email_template_id Id of the email template to use. + * @param array $send_days The days of the week to send the sequence on. Must be one of: `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`. + * @param integer $send_hour The hour of the day to send the sequence at. Must be an integer between 0 and 23. + * @param string $time_zone The timezone to use for the sequence. Must be a valid IANA timezone string. + * @param boolean $active Use `true` to activate the sequence, `false` to deactivate it. + * @param boolean $repeat When `true`, subscribers can restart the sequence multiple times. + * @param boolean $hold When `true`, subscribers added via Visual Automations stay in the sequence after receiving the last email. + * @param array> $exclude_subscriber_sources The subscriber sources to exclude from the sequence. Uses the account's default exclude subscriber sources if not provided. + * + * @see https://developers.kit.com/api-reference/sequences/create-a-sequence + * + * @return mixed|object + */ + public function create_sequence( + string $name, + string $email_address = '', + int $email_template_id = 0, + array $send_days = [], + int $send_hour = 0, + string $time_zone = '', + bool $active = true, + bool $repeat = false, + bool $hold = false, + array $exclude_subscriber_sources = [] + ) { + $options = [ + 'name' => $name, + 'email_address' => $email_address, + 'email_template_id' => $email_template_id, + 'send_hour' => $send_hour, + 'time_zone' => $time_zone, + 'active' => $active, + 'repeat' => $repeat, + 'hold' => $hold, + ]; + if (count($send_days)) { + $options['send_days'] = $send_days; + } + if (count($exclude_subscriber_sources)) { + $options['exclude_subscriber_sources'] = $exclude_subscriber_sources; + } + + // Iterate through options, removing blank entries. + foreach ($options as $key => $value) { + if (is_string($value) && strlen($value) === 0) { + unset($options[$key]); + } + } + + // Send request. + return $this->post( + 'sequences', + $options + ); + } + + /** + * Get a sequence. + * + * @param integer $id Sequence ID. + * @param array $include Additional fields to include: stats. + * + * @see https://developers.kit.com/api-reference/sequences/get-a-sequence + * + * @return mixed|object + */ + public function get_sequence( + int $id, + array $include = [] + ) { + // Build parameters. + $options = []; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + + return $this->get(sprintf('sequences/%s', $id), $options); + } + + /** + * Updates a sequence + * + * @param integer $sequence_id Sequence ID. + * @param string $name The name of the sequence. + * @param string $email_address The sending email address to use. Uses the account's sending email address if not provided. + * @param integer $email_template_id Id of the email template to use. + * @param array $send_days The days of the week to send the sequence on. Must be one of: `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`. + * @param integer $send_hour The hour of the day to send the sequence at. Must be an integer between 0 and 23. + * @param string $time_zone The timezone to use for the sequence. Must be a valid IANA timezone string. + * @param boolean $active Use `true` to activate the sequence, `false` to deactivate it. + * @param boolean $repeat When `true`, subscribers can restart the sequence multiple times. + * @param boolean $hold When `true`, subscribers added via Visual Automations stay in the sequence after receiving the last email. + * @param array> $exclude_subscriber_sources The subscriber sources to exclude from the sequence. Uses the account's default exclude subscriber sources if not provided. + * + * @see https://developers.kit.com/api-reference/sequences/create-a-sequence + * + * @return mixed|object + */ + public function update_sequence( + int $sequence_id, + string $name = '', + string $email_address = '', + int $email_template_id = 0, + array $send_days = [], + int $send_hour = 0, + string $time_zone = '', + bool $active = true, + bool $repeat = false, + bool $hold = false, + array $exclude_subscriber_sources = [] + ) { + $options = [ + 'name' => $name, + 'email_address' => $email_address, + 'email_template_id' => $email_template_id, + 'send_days' => $send_days, + 'send_hour' => $send_hour, + 'time_zone' => $time_zone, + 'active' => $active, + 'repeat' => $repeat, + 'hold' => $hold, + ]; + if (count($exclude_subscriber_sources)) { + $options['exclude_subscriber_sources'] = $exclude_subscriber_sources; + } + + // Iterate through options, removing blank entries. + foreach ($options as $key => $value) { + if (is_string($value) && strlen($value) === 0) { + unset($options[$key]); + } + } + + // Send request. + return $this->put( + sprintf('sequences/%s', $sequence_id), + $options + ); + } + + /** + * Deletes a sequence. + * + * @param integer $id Sequence ID. + * + * @see https://developers.kit.com/api-reference/sequences/delete-a-sequence + * + * @return mixed|object + */ + public function delete_sequence(int $id) + { + return $this->delete(sprintf('sequences/%s', $id)); + } + /** * Adds subscriber to sequence by email address * @@ -510,13 +690,371 @@ public function get_sequence_subscriptions( ); } + /** + * List sequence emails + * + * @param integer $sequence_id Sequence ID. + * @param array $include Additional fields to include: stats. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. + * + * @see https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails + * + * @return false|mixed + */ + public function get_sequence_emails( + int $sequence_id, + array $include = [], + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + // Build parameters. + $options = []; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + + return $this->get( + sprintf('sequences/%s/emails', $sequence_id), + $this->build_total_count_and_pagination_params( + $options, + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Create a sequence email + * + * @param integer $sequence_id Sequence ID. + * @param string $subject Subject line of the email. + * @param integer $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email + * + * @return mixed|object + */ + public function create_sequence_email( + int $sequence_id, + string $subject, + int $delay_value, + string $delay_unit, + ?string $preview_text = null, + ?string $content = null, + int|null $email_template_id = null, + bool $published = false, + array|null $send_days = null, + int|null $position = null, + ) { + $options = [ + 'subject' => $subject, + 'delay_value' => $delay_value, + 'delay_unit' => $delay_unit, + 'published' => $published, + 'send_days' => $send_days, + ]; + + if (!empty($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!empty($content)) { + $options['content'] = $content; + } + if (!empty($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!empty($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->post( + sprintf('sequences/%s/emails', $sequence_id), + $options + ); + } + + /** + * Get a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * @param array $include Additional fields to include: stats. + * + * @see https://developers.kit.com/api-reference/sequence-emails/get-a-sequence-email + * + * @return mixed|object + */ + public function get_sequence_email( + int $sequence_id, + int $email_id, + array $include = [] + ) { + // Build parameters. + $options = []; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + + return $this->get(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id), $options); + } + + /** + * Updates a sequence + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Sequence Email ID. + * @param string|null $subject Subject line of the email. + * @param integer|null $delay_value Number of days or hours to wait before sending this email after the previous one. + * @param string|null $delay_unit Unit for the send delay. Use `days` for schedule-aware delivery, `hours` for a fixed hourly delay. + * @param string|null $preview_text Preview text shown in email clients before the email is opened. + * @param string|null $content HTML body content of the email. + * @param integer|null $email_template_id ID of the email template to use for layout and styling. + * @param boolean|null $published Whether the email is active and will be sent to subscribers. + * @param array|null $send_days Days of the week this email may be sent. Defaults to all 7 days (inherits the sequence schedule). Pass a subset to restrict delivery, or null to reset to all days. + * @param integer|null $position Zero-based position of the email in the sequence. Assigned automatically after the last email if omitted. + * + * @see https://developers.kit.com/api-reference/sequences/create-a-sequence + * + * @return mixed|object + */ + public function update_sequence_email( + int $sequence_id, + int $email_id, + ?string $subject = null, + int|null $delay_value = null, + ?string $delay_unit = null, + ?string $preview_text = null, + ?string $content = null, + int|null $email_template_id = null, + bool|null $published = null, + array|null $send_days = null, + int|null $position = null, + ) { + // Build parameters. + $options = ['send_days' => $send_days]; + + if (!is_null($subject)) { + $options['subject'] = $subject; + } + if (!is_null($delay_value)) { + $options['delay_value'] = $delay_value; + } + if (!is_null($delay_unit)) { + $options['delay_unit'] = $delay_unit; + } + if (!is_null($preview_text)) { + $options['preview_text'] = $preview_text; + } + if (!is_null($content)) { + $options['content'] = $content; + } + if (!is_null($email_template_id)) { + $options['email_template_id'] = $email_template_id; + } + if (!is_null($published)) { + $options['published'] = $published; + } + if (!is_null($send_days)) { + $options['send_days'] = $send_days; + } + if (!is_null($position)) { + $options['position'] = $position; + } + + // Send request. + return $this->put( + sprintf('sequences/%s/emails/%s', $sequence_id, $email_id), + $options + ); + } + + /** + * Deletes a sequence email. + * + * @param integer $sequence_id Sequence ID. + * @param integer $email_id Email ID. + * + * @see https://developers.kit.com/api-reference/sequence-emails/delete-a-sequence-email + * + * @return mixed|object + */ + public function delete_sequence_email(int $sequence_id, int $email_id) + { + return $this->delete(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + + /** + * List snippets + * + * @param boolean $archived When `true`, returns only archived snippets. Defaults to `false`. + * @param boolean $include_content When `true`, includes both the content and document fields for each snippet in the response. Defaults to `false`. + * @param string|null $snippet_type Filter snippets by type. Use inline for text snippets or block for rich-text block snippets. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. + * + * @see https://developers.kit.com/api-reference/snippets/list-snippets + * + * @return false|mixed + */ + public function get_snippets( + bool $archived = false, + bool $include_content = false, + ?string $snippet_type = null, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + $options = [ + 'archived' => $archived, + 'include_content' => $include_content, + ]; + if (!is_null($snippet_type)) { + $options['snippet_type'] = $snippet_type; + } + return $this->get( + 'snippets', + $this->build_total_count_and_pagination_params( + $options, + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Create a snippet + * + * @param string $name Name of the snippet. + * @param string $snippet_type Type of snippet. Must be one of: `inline`, `block`. + * @param string $content Content of the snippet. + * + * @see https://developers.kit.com/api-reference/snippets/create-a-snippet + * + * @return mixed|object + */ + public function create_snippet( + string $name, + string $snippet_type, + string $content + ) { + $options = [ + 'name' => $name, + 'snippet_type' => $snippet_type, + ]; + + switch ($snippet_type) { + case 'inline': + $options['content'] = $content; + break; + + case 'block': + default: + $options['document_attributes'] = ['value_html' => $content]; + break; + } + + // Send request. + return $this->post( + 'snippets', + $options + ); + } + + /** + * Get a snippet. + * + * @param integer $id Snippet ID. + * + * @see https://developers.kit.com/api-reference/snippets/get-a-snippet + * + * @return mixed|object + */ + public function get_snippet(int $id) + { + return $this->get(sprintf('snippets/%s', $id)); + } + + /** + * Updates a snippet + * + * @param integer $snippet_id Snippet ID. + * @param string $name Name of the snippet. + * @param string $snippet_type Type of snippet. Must be one of: `inline`, `block`. + * @param boolean $archived Pass `true` to archive or `false` to restore the snippet. + * @param string $content Content of the snippet. + * + * @see https://developers.kit.com/api-reference/snippets/update-a-snippet + * + * @return mixed|object + */ + public function update_snippet( + int $snippet_id, + string $name = '', + string $snippet_type = '', + bool $archived = false, + string $content = '' + ) { + $options = [ + 'name' => $name, + 'snippet_type' => $snippet_type, + 'archived' => $archived, + ]; + + switch ($snippet_type) { + case 'inline': + $options['content'] = $content; + break; + + case 'block': + default: + $options['document_attributes'] = ['value_html' => $content]; + break; + } + + // Iterate through options, removing blank entries. + foreach ($options as $key => $value) { + if (is_string($value) && strlen($value) === 0) { + unset($options[$key]); + } + } + + // Send request. + return $this->put( + sprintf('snippets/%s', $snippet_id), + $options + ); + } + /** * List tags. * - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param array $include Additional fields to include: subscriber_count. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @see https://developers.kit.com/api-reference/tags/list-tags * @@ -525,15 +1063,23 @@ public function get_sequence_subscriptions( * @return mixed|array */ public function get_tags( + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { + // Build parameters. + $options = []; + + if (!empty($include)) { + $options['include'] = implode(',', $include); + } + return $this->get( 'tags', $this->build_total_count_and_pagination_params( - [], + $options, $include_total_count, $after_cursor, $before_cursor, @@ -594,6 +1140,41 @@ public function create_tags(array $tags, string $callback_url = '') ); } + /** + * Bulk delete tags. + * + * @param array $tag_ids Tag IDs. + * @param string $callback_url URL to notify for large batch size when async processing complete. + * + * @since 2.6.0 + * + * @see https://developers.kit.com/api-reference/tags/bulk-delete-tags + * + * @return false|mixed + */ + public function delete_tags(array $tag_ids, string $callback_url = '') + { + // Build parameters. + $options = [ + 'tags' => [], + ]; + foreach ($tag_ids as $i => $tag_id) { + $options['tags'][] = [ + 'id' => (int) $tag_id, + ]; + } + + if (!empty($callback_url)) { + $options['callback_url'] = $callback_url; + } + + // Send request. + return $this->delete( + 'bulk/tags', + $options + ); + } + /** * Updates the name of a tag. * @@ -723,6 +1304,7 @@ public function remove_tag_from_subscriber_by_email(int $tag_id, string $email_a * @param \DateTime|null $created_before Filter subscribers who have been created before this date. * @param \DateTime|null $tagged_after Filter subscribers who have been tagged after this date. * @param \DateTime|null $tagged_before Filter subscribers who have been tagged before this date. + * @param boolean $slim When true, omits expensive optional fields from the response. * @param boolean $include_total_count To include the total count of records in the response, use true. * @param string $after_cursor Return results after the given pagination cursor. * @param string $before_cursor Return results before the given pagination cursor. @@ -739,13 +1321,14 @@ public function get_tag_subscriptions( ?\DateTime $created_before = null, ?\DateTime $tagged_after = null, ?\DateTime $tagged_before = null, + bool $slim = false, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { // Build parameters. - $options = []; + $options = ['slim' => $slim]; if (!empty($subscriber_state)) { $options['status'] = $subscriber_state; @@ -809,6 +1392,57 @@ public function get_email_templates( ); } + /** + * List posts. + * + * @param boolean $include_content To include the content field on each post in the response, use true. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. + * + * @since 2.5.0 + * + * @see https://developers.kit.com/api-reference/posts/list-posts + * + * @return false|mixed + */ + public function get_posts( + bool $include_content = false, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + // Send request. + return $this->get( + 'posts', + $this->build_total_count_and_pagination_params( + ['include_content' => $include_content], + $include_total_count, + $after_cursor, + $before_cursor, + $per_page + ) + ); + } + + /** + * Get a post. + * + * @param integer $id Post ID. + * + * @since 2.5.0 + * + * @see https://developers.kit.com/api-reference/posts/get-a-post + * + * @return mixed|object + */ + public function get_post(int $id) + { + return $this->get(sprintf('posts/%s', $id)); + } + /** * List subscribers. * @@ -820,6 +1454,8 @@ public function get_email_templates( * @param \DateTime|null $updated_before Filter subscribers who have been updated before this date. * @param string $sort_field Sort Field (id|updated_at|cancelled_at). * @param string $sort_order Sort Order (asc|desc). + * @param array $include Additional fields to include: attribution, tags, location, canceled_at. + * @param boolean $slim When true, omits expensive optional fields from the response. * @param boolean $include_total_count To include the total count of records in the response, use true. * @param string $after_cursor Return results after the given pagination cursor. * @param string $before_cursor Return results before the given pagination cursor. @@ -840,13 +1476,15 @@ public function get_subscribers( ?\DateTime $updated_before = null, string $sort_field = 'id', string $sort_order = 'desc', + array $include = [], + bool $slim = false, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { // Build parameters. - $options = []; + $options = ['slim' => $slim]; if (!empty($subscriber_state)) { $options['status'] = $subscriber_state; @@ -872,6 +1510,9 @@ public function get_subscribers( if (!empty($sort_order)) { $options['sort_order'] = $sort_order; } + if (!empty($include)) { + $options['include'] = implode(',', $include); + } // Send request. return $this->get( @@ -961,17 +1602,22 @@ public function create_subscribers(array $subscribers, string $callback_url = '' /** * Filter subscribers based on engagement. * - * @param array> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have. - * - 'type' (string). - * - 'count_greater_than' (int|null). - * - 'count_less_than' (int|null). - * - 'after' (\DateTime|null). - * - 'before' (\DateTime|null). - * - 'any' (array|null). - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param list> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have. + * - 'type' (string). + * - 'count_greater_than' (int|null). + * - 'count_less_than' (int|null). + * - 'after' (?\DateTime). + * - 'before' (?\DateTime). + * - 'states' (array). + * - 'any' (array|null). + * @param string $counting_mode Controls how engagement-filter count thresholds are tallied. + * - 'raw' (default) counts every event — five opens of the same email = five. + * - 'unique_email' counts distinct emails on which the action occurred. + * @param list> $include Array of additional fields to embed on each subscriber row. + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @since 2.4.0 * @@ -981,6 +1627,8 @@ public function create_subscribers(array $subscribers, string $callback_url = '' */ public function filter_subscribers( array $all = [], + string $counting_mode = 'raw', + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', @@ -991,12 +1639,24 @@ public function filter_subscribers( foreach ($all as $condition) { $option = []; - if (array_key_exists('count_greater_than', $condition) && $condition['count_greater_than'] !== null) { - $option['count_greater_than'] = $condition['count_greater_than']; + if (array_key_exists('type', $condition) && !empty($condition['type'])) { + $option['type'] = $condition['type']; + } + + if (array_key_exists('count_greater_than', $condition) && is_numeric($condition['count_greater_than'])) { + $option['count_greater_than'] = (int) $condition['count_greater_than']; + } + + if (array_key_exists('count_greater_than_or_equal', $condition) && is_numeric($condition['count_greater_than_or_equal'])) { + $option['count_greater_than_or_equal'] = (int) $condition['count_greater_than_or_equal']; } - if (array_key_exists('count_less_than', $condition) && $condition['count_less_than'] !== null) { - $option['count_less_than'] = $condition['count_less_than']; + if (array_key_exists('count_less_than', $condition) && is_numeric($condition['count_less_than'])) { + $option['count_less_than'] = (int) $condition['count_less_than']; + } + + if (array_key_exists('count_less_than_or_equal', $condition) && is_numeric($condition['count_less_than_or_equal'])) { + $option['count_less_than_or_equal'] = (int) $condition['count_less_than_or_equal']; } if (array_key_exists('after', $condition) && $condition['after'] instanceof \DateTime) { @@ -1007,6 +1667,34 @@ public function filter_subscribers( $option['before'] = $condition['before']->format('Y-m-d'); } + if (array_key_exists('states', $condition) && !empty($condition['states'])) { + $option['states'] = (array) $condition['states']; + } + + if (array_key_exists('subscriber_custom_field_id', $condition) && is_numeric($condition['subscriber_custom_field_id'])) { + $option['subscriber_custom_field_id'] = (int) $condition['subscriber_custom_field_id']; + } + + if (array_key_exists('value', $condition) && $condition['value'] !== null) { + $option['value'] = $condition['value']; + } + + if (array_key_exists('comparison', $condition) && $condition['comparison'] !== null) { + $option['comparison'] = $condition['comparison']; + } + + if (array_key_exists('latitude', $condition) && is_numeric($condition['latitude'])) { + $option['latitude'] = (float) $condition['latitude']; + } + + if (array_key_exists('longitude', $condition) && is_numeric($condition['longitude'])) { + $option['longitude'] = (float) $condition['longitude']; + } + + if (array_key_exists('radius', $condition) && $condition['radius'] !== null) { + $option['radius'] = $condition['radius']; + } + if (array_key_exists('any', $condition) && !empty($condition['any'])) { $option['any'] = (array) $condition['any']; } @@ -1017,7 +1705,11 @@ public function filter_subscribers( return $this->post( 'subscribers/filter', $this->build_total_count_and_pagination_params( - ['all' => $options], + [ + 'all' => $options, + 'counting_mode' => $counting_mode, + 'include' => $include, + ], $include_total_count, $after_cursor, $before_cursor, @@ -1203,26 +1895,47 @@ public function get_subscriber_tags( /** * List broadcasts. * - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param \DateTime|null $sent_after Get broadcasts sent after the given date. + * @param \DateTime|null $sent_before Get broadcasts sent before the given date. + * @param boolean $slim When true, omits expensive optional fields from the response. + * @param string|null $status Get broadcasts with the given status (draft, scheduled, sending, completed, aborted). + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @see https://developers.kit.com/api-reference/broadcasts/list-broadcasts * * @return false|mixed */ public function get_broadcasts( + ?\DateTime $sent_after = null, + ?\DateTime $sent_before = null, + bool $slim = false, + ?string $status = null, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { + // Build parameters. + $options = ['slim' => $slim]; + + if (!is_null($status)) { + $options['status'] = $status; + } + if (!is_null($sent_after)) { + $options['sent_after'] = $sent_after->format('Y-m-d'); + } + if (!is_null($sent_before)) { + $options['sent_before'] = $sent_before->format('Y-m-d'); + } + // Send request. return $this->get( 'broadcasts', $this->build_total_count_and_pagination_params( - [], + $options, $include_total_count, $after_cursor, $before_cursor, @@ -1360,11 +2073,9 @@ public function get_broadcast_link_clicks( return $this->get( sprintf('broadcasts/%s/clicks', $id), $this->build_total_count_and_pagination_params( - [], - false, - $after_cursor, - $before_cursor, - $per_page + after_cursor: $after_cursor, + before_cursor: $before_cursor, + per_page: $per_page ) ); } @@ -1372,10 +2083,13 @@ public function get_broadcast_link_clicks( /** * List stats for a list of broadcasts. * - * @param boolean $include_total_count To include the total count of records in the response, use true. - * @param string $after_cursor Return results after the given pagination cursor. - * @param string $before_cursor Return results before the given pagination cursor. - * @param integer $per_page Number of results to return. + * @param \DateTime|null $sent_after Get broadcasts sent after the given date. + * @param \DateTime|null $sent_before Get broadcasts sent before the given date. + * @param string|null $status Get broadcasts with the given status (draft, scheduled, sending, completed, aborted). + * @param boolean $include_total_count To include the total count of records in the response, use true. + * @param string $after_cursor Return results after the given pagination cursor. + * @param string $before_cursor Return results before the given pagination cursor. + * @param integer $per_page Number of results to return. * * @since 2.2.1 * @@ -1384,16 +2098,32 @@ public function get_broadcast_link_clicks( * @return false|mixed */ public function get_broadcasts_stats( + ?\DateTime $sent_after = null, + ?\DateTime $sent_before = null, + ?string $status = null, bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', int $per_page = 100 ) { + // Build parameters. + $options = []; + + if (!is_null($status)) { + $options['status'] = $status; + } + if (!is_null($sent_after)) { + $options['sent_after'] = $sent_after->format('Y-m-d'); + } + if (!is_null($sent_before)) { + $options['sent_before'] = $sent_before->format('Y-m-d'); + } + // Send request. return $this->get( - 'broadcasts/stats', + 'broadcasts', $this->build_total_count_and_pagination_params( - [], + $options, $include_total_count, $after_cursor, $before_cursor, @@ -1402,7 +2132,6 @@ public function get_broadcasts_stats( ); } - /** * Update a broadcast. * @@ -2041,7 +2770,7 @@ public function get(string $endpoint, array $args = []) * Performs a POST request to the API. * * @param string $endpoint API Endpoint. - * @param array|boolean|integer|float|string>> $args Request arguments. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2053,8 +2782,8 @@ public function post(string $endpoint, array $args = []) /** * Performs a PUT request to the API. * - * @param string $endpoint API Endpoint. - * @param array|string> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2066,8 +2795,8 @@ public function put(string $endpoint, array $args = []) /** * Performs a DELETE request to the API. * - * @param string $endpoint API Endpoint. - * @param array|string> $args Request arguments. + * @param string $endpoint API Endpoint. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2081,7 +2810,7 @@ public function delete(string $endpoint, array $args = []) * * @param string $endpoint API Endpoint. * @param string $method Request method. - * @param array>> $args Request arguments. + * @param array>> $args Request arguments. * * @throws \Exception If JSON encoding arguments failed. * From e63a62be99f5d3b87d4687c94763d5f5617b407d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 28 Jul 2026 15:53:34 +0800 Subject: [PATCH 02/15] PHPStan compat. --- src/class-convertkit-api-traits.php | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/class-convertkit-api-traits.php b/src/class-convertkit-api-traits.php index 8b73a8f..7d564be 100644 --- a/src/class-convertkit-api-traits.php +++ b/src/class-convertkit-api-traits.php @@ -756,10 +756,10 @@ public function create_sequence_email( string $delay_unit, ?string $preview_text = null, ?string $content = null, - int|null $email_template_id = null, + ?int $email_template_id = null, bool $published = false, - array|null $send_days = null, - int|null $position = null, + ?array $send_days = null, + ?int $position = null ) { $options = [ 'subject' => $subject, @@ -838,14 +838,14 @@ public function update_sequence_email( int $sequence_id, int $email_id, ?string $subject = null, - int|null $delay_value = null, + ?int $delay_value = null, ?string $delay_unit = null, ?string $preview_text = null, ?string $content = null, - int|null $email_template_id = null, - bool|null $published = null, - array|null $send_days = null, - int|null $position = null, + ?int $email_template_id = null, + ?bool $published = null, + ?array $send_days = null, + ?int $position = null ) { // Build parameters. $options = ['send_days' => $send_days]; @@ -2073,9 +2073,11 @@ public function get_broadcast_link_clicks( return $this->get( sprintf('broadcasts/%s/clicks', $id), $this->build_total_count_and_pagination_params( - after_cursor: $after_cursor, - before_cursor: $before_cursor, - per_page: $per_page + array(), + false, + $after_cursor, + $before_cursor, + $per_page ) ); } @@ -2770,7 +2772,7 @@ public function get(string $endpoint, array $args = []) * Performs a POST request to the API. * * @param string $endpoint API Endpoint. - * @param array|boolean|integer|float|string>> $args Request arguments. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2783,7 +2785,7 @@ public function post(string $endpoint, array $args = []) * Performs a PUT request to the API. * * @param string $endpoint API Endpoint. - * @param array|boolean|integer|float|string>> $args Request arguments. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2796,7 +2798,7 @@ public function put(string $endpoint, array $args = []) * Performs a DELETE request to the API. * * @param string $endpoint API Endpoint. - * @param array|boolean|integer|float|string>> $args Request arguments. + * @param array|boolean|integer|float|string>> $args Request arguments. * * @return false|mixed */ @@ -2810,7 +2812,7 @@ public function delete(string $endpoint, array $args = []) * * @param string $endpoint API Endpoint. * @param string $method Request method. - * @param array>> $args Request arguments. + * @param array>> $args Request arguments. * * @throws \Exception If JSON encoding arguments failed. * From bc69f73160a3f5254606feb39c0dd474f1afe94e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 28 Jul 2026 19:30:36 +0800 Subject: [PATCH 03/15] Resources: Reorder method args --- src/class-convertkit-resource-v4.php | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/class-convertkit-resource-v4.php b/src/class-convertkit-resource-v4.php index 9a56a48..57a0d67 100644 --- a/src/class-convertkit-resource-v4.php +++ b/src/class-convertkit-resource-v4.php @@ -543,6 +543,16 @@ private function get_all_resources( $resource_type, $per_page = 100 ) { // Build array of arguments depending on the resource type. switch ( $resource_type ) { case 'forms': + $args = array( + 'active', + array(), + false, + '', + '', + $per_page, + ); + break; + case 'landing_pages': $args = array( 'active', @@ -563,6 +573,17 @@ private function get_all_resources( $resource_type, $per_page = 100 ) { ); break; + case 'tags': + case 'sequences': + $args = array( + array(), + false, + '', + '', + $per_page, + ); + break; + default: $args = array( false, @@ -597,6 +618,16 @@ private function get_all_resources( $resource_type, $per_page = 100 ) { // Build array of arguments depending on the resource type. switch ( $resource_type ) { case 'forms': + $args = array( + 'active', + array(), + false, + $response['pagination']['end_cursor'], + '', + $per_page, + ); + break; + case 'landing_pages': $args = array( 'active', @@ -617,6 +648,17 @@ private function get_all_resources( $resource_type, $per_page = 100 ) { ); break; + case 'tags': + case 'sequences': + $args = array( + array(), + false, + $response['pagination']['end_cursor'], + '', + $per_page, + ); + break; + default: $args = array( false, From bd0ff161483072bf0735bdc99ee1d944ea2f3441 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Aug 2026 14:58:44 +0800 Subject: [PATCH 04/15] Match SDK version number --- src/class-convertkit-api-v4.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 611d2bc..88fa6a6 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -21,7 +21,7 @@ class ConvertKit_API_V4 { * * @var string */ - public const VERSION = '2.0.0'; + public const VERSION = '2.6.0'; /** * Redirect URI. From c113b9772d3d7d7d4e954f6c82c838f1e02b2f4f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 13:56:16 +0800 Subject: [PATCH 05/15] Use TestsTrait from PHP SDK --- phpcs.tests.xml | 10 + src/class-convertkit-api-v4.php | 38 +- tests/Integration/APITest.php | 5120 ++-------------------- tests/Integration/TestsTrait.php | 6987 ++++++++++++++++++++++++++++++ 4 files changed, 7304 insertions(+), 4851 deletions(-) create mode 100644 tests/Integration/TestsTrait.php diff --git a/phpcs.tests.xml b/phpcs.tests.xml index 881d54b..5763df5 100644 --- a/phpcs.tests.xml +++ b/phpcs.tests.xml @@ -5,6 +5,13 @@ tests + + tests/Integration/TestsTrait.php + @@ -37,6 +44,9 @@ + + + diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 88fa6a6..5d55c87 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -79,6 +79,20 @@ class ConvertKit_API_V4 { */ protected $plugin_version; + /** + * The HTTP status code of the last API response. + * + * Set by request() after every call. Read via get_last_response_code(). + * Provides parity with the PHP SDK's getResponseInterface()->getStatusCode(), + * so tests that assert a specific status code (e.g. 204 for deletes) can be + * copy-pasted between the SDK and WP Libs without change. + * + * @since 2.0.5 + * + * @var int + */ + protected $last_response_code = 0; + /** * ConvertKit API endpoints that use the /oauth/ namespace * i.e. https://api.kit.com/oauth/endpoint @@ -226,7 +240,24 @@ private function generate_and_store_code_verifier() { } /** - * Base64URL the given code verifier, as PHP has no built in function for this. + * Returns the HTTP status code of the last API response. + * + * Mirrors the PHP SDK's `getResponseInterface()->getStatusCode()` so tests + * that assert a status code (for example 204 on delete) can be copy-pasted + * between the SDK and WP Libs unchanged. + * + * @since 2.0.5 + * + * @return int HTTP status code of the last API response (0 if none). + */ + public function get_last_response_code() { + + return $this->last_response_code; + + } + + /** + * Generates a PKCE Code Challenge for the given Code Verifier. * * @since 2.0.0 * @@ -1507,8 +1538,9 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i } // Fetch HTTP response code and body. - $http_response_code = wp_remote_retrieve_response_code( $result ); - $body = wp_remote_retrieve_body( $result ); + $http_response_code = wp_remote_retrieve_response_code( $result ); + $this->last_response_code = (int) $http_response_code; + $body = wp_remote_retrieve_body( $result ); // If the body is null i.e. a 204 No Content, don't attempt to JSON decode it. $response = ( ! empty( $body ) ? json_decode( $body, true ) : null ); diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index b4a9fc6..162a8a6 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -4,6 +4,9 @@ use lucatume\WPBrowser\TestCase\WPTestCase; +// Load the shared tests trait. +require_once __DIR__ . '/TestsTrait.php'; + /** * Tests for the ConvertKit_API class. * @@ -11,6 +14,8 @@ */ class APITest extends WPTestCase { + use \TestsTrait; + /** * The testing implementation. * @@ -135,6 +140,45 @@ public function tearDown(): void parent::tearDown(); } + /** + * Assert that the given callable produces an API-level error. + * + * In WP Libs an API-level error surfaces as a WP_Error return value + * (never as a thrown exception). We accept both return and throw so + * that any input-validation code that throws still counts. + * + * @since 2.0.5 + * + * @param callable $fn Callable that should fail. + * @return void + */ + protected function assertApiError(callable $fn): void + { + try { + $result = $fn(); + } catch (\Throwable $e) { + $this->assertTrue(true, 'Callable threw an exception as expected.'); + return; + } + $this->assertInstanceOf(\WP_Error::class, $result); + } + + /** + * Assert that the last API response had the given HTTP status code. + * + * Backed by ConvertKit_API_V4::get_last_response_code(), which mirrors + * the PHP SDK's getResponseInterface()->getStatusCode(). + * + * @since 2.0.5 + * + * @param int $expected Expected HTTP status code. + * @return void + */ + protected function assertLastResponseStatusCode(int $expected): void + { + $this->assertEquals($expected, $this->api->get_last_response_code()); + } + /** * Test that a log directory and file are created in the expected location, with .htaccess * and index.html protection, and that the name and email addresses are masked. @@ -719,4554 +763,381 @@ public function testGetAccessTokenByAPIKeyAndSecretWithTenantName() $this->assertArrayHasKey('refresh_token', $result['oauth']); $this->assertArrayHasKey('expires_at', $result['oauth']); } - - /** - * Test that supplying valid API credentials to the API class returns the expected account information. - * - * @since 1.0.0 - */ - public function testGetAccount() - { - $result = $this->api->get_account(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - $this->assertArrayHasKey('user', $result); - $this->assertArrayHasKey('account', $result); - - $this->assertArrayHasKey('name', $result['account']); - $this->assertArrayHasKey('plan_type', $result['account']); - $this->assertArrayHasKey('primary_email_address', $result['account']); - $this->assertEquals('wordpress@convertkit.com', $result['account']['primary_email_address']); - } - /** - * Test that get_account_colors() returns the expected data. + * Test that get_legacy_forms() returns the expected data. * * @since 2.0.0 * * @return void */ - public function testGetAccountColors() + public function testGetLegacyForms() { - $result = $this->api->get_account_colors(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); + $result = $this->api->get_legacy_forms(); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'legacy_landing_pages'); + $this->assertPaginationExists($result); + + // Iterate through each form, confirming no landing pages were included. + foreach ($result['legacy_landing_pages'] as $form) { + // Assert shape of object is valid. + $this->assertArrayHasKey('id', $form); + $this->assertArrayHasKey('name', $form); + $this->assertArrayHasKey('created_at', $form); + $this->assertArrayHasKey('type', $form); + $this->assertArrayHasKey('url', $form); - $this->assertArrayHasKey('colors', $result); - $this->assertIsArray($result['colors']); + // Assert form is not a landing page i.e it is an embed. + $this->assertEquals($form['type'], 'embed'); + } } /** - * Test that update_account_colors() updates the account's colors. + * Test that get_legacy_forms() returns the expected data + * when the total count is included. * * @since 2.0.0 * * @return void */ - public function testUpdateAccountColors() + public function testGetLegacyFormsWithTotalCount() { - $result = $this->api->update_account_colors( - colors: [ - '#111111', - ] + $result = $this->api->get_legacy_forms( + include_total_count: true ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('colors', $result); - $this->assertIsArray($result['colors']); - $this->assertEquals($result['colors'][0], '#111111'); - } + // Assert forms and pagination exist. + $this->assertDataExists($result, 'legacy_landing_pages'); + $this->assertPaginationExists($result); + // Assert total count is included. + $this->assertArrayHasKey('total_count', $result['pagination']); + $this->assertGreaterThan(0, $result['pagination']['total_count']); + } /** - * Test that get_creator_profile() returns the expected data. + * Test that get_legacy_landing_pages() returns the expected data. * * @since 2.0.0 * * @return void */ - public function testGetCreatorProfile() + public function testGetLegacyLandingPages() { - $result = $this->api->get_creator_profile(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); + $result = $this->api->get_legacy_landing_pages(); + + // Assert landing pages and pagination exist. + $this->assertDataExists($result, 'legacy_landing_pages'); + $this->assertPaginationExists($result); + + // Iterate through each landing page, confirming no forms were included. + foreach ($result['legacy_landing_pages'] as $form) { + // Assert shape of object is valid. + $this->assertArrayHasKey('id', $form); + $this->assertArrayHasKey('name', $form); + $this->assertArrayHasKey('created_at', $form); + $this->assertArrayHasKey('type', $form); + $this->assertArrayHasKey('url', $form); - $this->assertArrayHasKey('name', $result['profile']); - $this->assertArrayHasKey('byline', $result['profile']); - $this->assertArrayHasKey('bio', $result['profile']); - $this->assertArrayHasKey('image_url', $result['profile']); - $this->assertArrayHasKey('profile_url', $result['profile']); + // Assert landing page is not a form i.e it is hosted. + $this->assertEquals($form['type'], 'hosted'); + } } /** - * Test that get_email_stats() returns the expected data. + * Test that get_landing_pages() returns the expected data + * when the total count is included. * * @since 2.0.0 * * @return void */ - public function testGetEmailStats() + public function testGetLegacyLandingPagesWithTotalCount() { - $result = $this->api->get_email_stats(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); + $result = $this->api->get_legacy_landing_pages( + include_total_count: true + ); - $this->assertArrayHasKey('sent', $result['stats']); - $this->assertArrayHasKey('clicked', $result['stats']); - $this->assertArrayHasKey('opened', $result['stats']); - $this->assertArrayHasKey('email_stats_mode', $result['stats']); - $this->assertArrayHasKey('open_tracking_enabled', $result['stats']); - $this->assertArrayHasKey('click_tracking_enabled', $result['stats']); - $this->assertArrayHasKey('starting', $result['stats']); - $this->assertArrayHasKey('ending', $result['stats']); - } + // Assert forms and pagination exist. + $this->assertDataExists($result, 'legacy_landing_pages'); + $this->assertPaginationExists($result); + // Assert total count is included. + $this->assertArrayHasKey('total_count', $result['pagination']); + $this->assertGreaterThan(0, $result['pagination']['total_count']); + } /** - * Test that get_growth_stats() returns the expected data. + * Test that add_subscriber_to_form_by_email() returns a WP_Error when an invalid + * form is specified. * - * @since 2.0.0 + * @since 1.0.0 * * @return void */ - public function testGetGrowthStats() + public function testAddSubscriberToFormByEmailWithInvalidformID() { - $result = $this->api->get_growth_stats(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - $this->assertArrayHasKey('cancellations', $result['stats']); - $this->assertArrayHasKey('net_new_subscribers', $result['stats']); - $this->assertArrayHasKey('new_subscribers', $result['stats']); - $this->assertArrayHasKey('subscribers', $result['stats']); - $this->assertArrayHasKey('starting', $result['stats']); - $this->assertArrayHasKey('ending', $result['stats']); + $result = $this->api->add_subscriber_to_form_by_email( + form_id: 12345, + email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] + ); + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } - /** - * Test that get_growth_stats() returns the expected data - * when a start date is specified. + * Test that add_subscriber_to_form() returns a WP_Error when a legacy + * form ID is specified. * * @since 2.0.0 * * @return void */ - public function testGetGrowthStatsWithStartDate() + public function testAddSubscriberToFormWithLegacyFormID() { - // Define start and end dates. - $starting = new \DateTime('now'); - $starting->modify('-7 days'); - $ending = new \DateTime('now'); - - // Send request. - $result = $this->api->get_growth_stats( - starting: $starting + $result = $this->api->add_subscriber_to_form( + form_id: $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Confirm response object contains expected keys. - $this->assertArrayHasKey('cancellations', $result['stats']); - $this->assertArrayHasKey('net_new_subscribers', $result['stats']); - $this->assertArrayHasKey('new_subscribers', $result['stats']); - $this->assertArrayHasKey('subscribers', $result['stats']); - $this->assertArrayHasKey('starting', $result['stats']); - $this->assertArrayHasKey('ending', $result['stats']); - - // Assert start and end dates were honored. - $timezone = ( new \DateTime() )->setTimezone(new \DateTimeZone('America/New_York'))->format('P'); // Gets timezone offset for New York (-04:00 during DST, -05:00 otherwise). - $this->assertEquals($result['stats']['starting'], $starting->format('Y-m-d') . 'T00:00:00' . $timezone); - $this->assertEquals($result['stats']['ending'], $ending->format('Y-m-d') . 'T23:59:59' . $timezone); + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } /** - * Test that get_growth_stats() returns the expected data - * when an end date is specified. + * Test that add_subscriber_to_form() returns a WP_Error when an invalid + * email address is specified. * * @since 2.0.0 * * @return void */ - public function testGetGrowthStatsWithEndDate() + public function testAddSubscriberToformWithInvalidSubscriberID() { - // Define start and end dates. - $starting = new \DateTime('now'); - $starting->modify('-90 days'); - $ending = new \DateTime('now'); - $ending->modify('-7 days'); - - // Send request. - $result = $this->api->get_growth_stats( - ending: $ending + $result = $this->api->add_subscriber_to_form( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: 12345 ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Confirm response object contains expected keys. - $this->assertArrayHasKey('cancellations', $result['stats']); - $this->assertArrayHasKey('net_new_subscribers', $result['stats']); - $this->assertArrayHasKey('new_subscribers', $result['stats']); - $this->assertArrayHasKey('subscribers', $result['stats']); - $this->assertArrayHasKey('starting', $result['stats']); - $this->assertArrayHasKey('ending', $result['stats']); - - // Assert start and end dates were honored. - $timezone = ( new \DateTime() )->setTimezone(new \DateTimeZone('America/New_York'))->format('P'); // Gets timezone offset for New York (-04:00 during DST, -05:00 otherwise). - $this->assertEquals($result['stats']['starting'], $starting->format('Y-m-d') . 'T00:00:00' . $timezone); - $this->assertEquals($result['stats']['ending'], $ending->format('Y-m-d') . 'T23:59:59' . $timezone); + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } /** - * Test that get_forms() returns the expected data. + * Test that add_subscriber_to_legacy_form() returns the expected data. * - * @since 1.0.0 + * @since 2.0.0 * * @return void */ - public function testGetForms() + public function testAddSubscriberToLegacyForm() { - $result = $this->api->get_forms(); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); + // Create subscriber. + $subscriber = $this->api->create_subscriber($this->generateEmailAddress()); - // Iterate through each form, confirming no landing pages were included. - foreach ($result['forms'] as $form) { - // Assert shape of object is valid. - $this->assertArrayHasKey('id', $form); - $this->assertArrayHasKey('name', $form); - $this->assertArrayHasKey('created_at', $form); - $this->assertArrayHasKey('type', $form); - $this->assertArrayHasKey('format', $form); - $this->assertArrayHasKey('embed_js', $form); - $this->assertArrayHasKey('embed_url', $form); - $this->assertArrayHasKey('archived', $form); + $this->assertNotInstanceOf(\WP_Error::class, $subscriber); + $this->assertIsArray($subscriber); - // Assert form is not a landing page i.e embed. - $this->assertEquals($form['type'], 'embed'); + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber['subscriber']['id']; - // Assert form is not archived. - $this->assertFalse($form['archived']); - } + // Add subscriber to legacy form. + $result = $this->api->add_subscriber_to_legacy_form( + form_id: (int) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + subscriber_id: $subscriber['subscriber']['id'] + ); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertArrayHasKey('subscriber', $result); + $this->assertArrayHasKey('id', $result['subscriber']); + $this->assertEquals($result['subscriber']['id'], $subscriber['subscriber']['id']); } /** - * Test that get_forms() returns the expected data when - * the status is set to archived. + * Test that add_subscriber_to_legacy_form() returns a WP_Error when an invalid + * form ID is specified. * * @since 2.0.0 * * @return void */ - public function testGetFormsWithArchivedStatus() + public function testAddSubscriberToLegacyFormWithInvalidFormID() { - $result = $this->api->get_forms( - status: 'archived' + $result = $this->api->add_subscriber_to_legacy_form( + form_id: 12345, + subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Iterate through each form, confirming no landing pages were included. - foreach ($result['forms'] as $form) { - // Assert shape of object is valid. - $this->assertArrayHasKey('id', $form); - $this->assertArrayHasKey('name', $form); - $this->assertArrayHasKey('created_at', $form); - $this->assertArrayHasKey('type', $form); - $this->assertArrayHasKey('format', $form); - $this->assertArrayHasKey('embed_js', $form); - $this->assertArrayHasKey('embed_url', $form); - $this->assertArrayHasKey('archived', $form); - - // Assert form is not a landing page i.e embed. - $this->assertEquals($form['type'], 'embed'); - - // Assert form is archived. - $this->assertTrue($form['archived']); - } + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } /** - * Test that get_forms() returns the expected data - * when the total count is included. + * Test that add_subscriber_to_legacy_form() returns a WP_Error when a non-legacy + * form ID is specified. * * @since 2.0.0 * * @return void */ - public function testGetFormsWithTotalCount() + public function testAddSubscriberToLegacyFormWithNonLegacyFormID() { - $result = $this->api->get_forms( - status: 'active', - include_total_count: true + $result = $this->api->add_subscriber_to_legacy_form( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } /** - * Test that get_forms() returns the expected data when pagination parameters - * and per_page limits are specified. + * Test that add_subscriber_to_legacy_form() returns a WP_Error when an invalid + * email address is specified. * * @since 2.0.0 * * @return void */ - public function testGetFormsPagination() + public function testAddSubscriberToLegacyFormWithInvalidSubscriberID() { - // Return one form. - $result = $this->api->get_forms( - status: 'active', - per_page: 1 - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single form was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_forms( - status: 'active', - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single form was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_forms( - status: 'active', - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 + $result = $this->api->add_subscriber_to_legacy_form( + form_id: (int) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + subscriber_id: 12345 ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single form was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); + $this->assertInstanceOf(\WP_Error::class, $result); + $this->assertEquals($result->get_error_code(), $this->errorCode); } - /** - * Test that get_legacy_forms() returns the expected data. + * Test that create_tag() returns the expected data. * - * @since 2.0.0 + * @since 1.0.0 * * @return void */ - public function testGetLegacyForms() + public function testCreateTag() { - $result = $this->api->get_legacy_forms(); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'legacy_landing_pages'); - $this->assertPaginationExists($result); - - // Iterate through each form, confirming no landing pages were included. - foreach ($result['legacy_landing_pages'] as $form) { - // Assert shape of object is valid. - $this->assertArrayHasKey('id', $form); - $this->assertArrayHasKey('name', $form); - $this->assertArrayHasKey('created_at', $form); - $this->assertArrayHasKey('type', $form); - $this->assertArrayHasKey('url', $form); - - // Assert form is not a landing page i.e it is an embed. - $this->assertEquals($form['type'], 'embed'); - } - } - - /** - * Test that get_legacy_forms() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLegacyFormsWithTotalCount() - { - $result = $this->api->get_legacy_forms( - include_total_count: true - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'legacy_landing_pages'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_landing_pages() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetLandingPages() - { - $result = $this->api->get_landing_pages(); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Iterate through each landing page, confirming no forms were included. - foreach ($result['forms'] as $form) { - // Assert shape of object is valid. - $this->assertArrayHasKey('id', $form); - $this->assertArrayHasKey('name', $form); - $this->assertArrayHasKey('created_at', $form); - $this->assertArrayHasKey('type', $form); - $this->assertArrayHasKey('format', $form); - $this->assertArrayHasKey('embed_js', $form); - $this->assertArrayHasKey('embed_url', $form); - $this->assertArrayHasKey('archived', $form); - - // Assert form is a landing page i.e. hosted. - $this->assertEquals($form['type'], 'hosted'); - - // Assert form is not archived. - $this->assertFalse($form['archived']); - } - } - - /** - * Test that get_landing_pages() returns the expected data when - * the status is set to archived. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLandingPagesWithArchivedStatus() - { - $result = $this->api->get_forms( - status: 'archived' - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert no landing pages are returned, as the account doesn't have any archived landing pages. - $this->assertCount(0, $result['forms']); - } - - /** - * Test that get_landing_pages() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLandingPagesWithTotalCount() - { - $result = $this->api->get_landing_pages( - status: 'active', - include_total_count: true - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_landing_pages() returns the expected data when pagination parameters - * and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLandingPagesPagination() - { - // Return one landing page. - $result = $this->api->get_landing_pages( - status: 'active', - per_page: 1 - ); - - // Assert landing pages and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single landing page was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_landing_pages( - status: 'active', - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert landing pages and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single landing page was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertFalse($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_landing_pages( - status: 'active', - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert landing pages and pagination exist. - $this->assertDataExists($result, 'forms'); - $this->assertPaginationExists($result); - - // Assert a single landing page was returned. - $this->assertCount(1, $result['forms']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that get_legacy_landing_pages() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLegacyLandingPages() - { - $result = $this->api->get_legacy_landing_pages(); - - // Assert landing pages and pagination exist. - $this->assertDataExists($result, 'legacy_landing_pages'); - $this->assertPaginationExists($result); - - // Iterate through each landing page, confirming no forms were included. - foreach ($result['legacy_landing_pages'] as $form) { - // Assert shape of object is valid. - $this->assertArrayHasKey('id', $form); - $this->assertArrayHasKey('name', $form); - $this->assertArrayHasKey('created_at', $form); - $this->assertArrayHasKey('type', $form); - $this->assertArrayHasKey('url', $form); - - // Assert landing page is not a form i.e it is hosted. - $this->assertEquals($form['type'], 'hosted'); - } - } - - /** - * Test that get_landing_pages() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetLegacyLandingPagesWithTotalCount() - { - $result = $this->api->get_legacy_landing_pages( - include_total_count: true - ); - - // Assert forms and pagination exist. - $this->assertDataExists($result, 'legacy_landing_pages'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetFormSubscriptions() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'] - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithTotalCount() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - include_total_count: true - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and the subscription status - * is cancelled. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithCancelledSubscriberState() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'cancelled' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertEquals($result['subscribers'][0]['state'], 'cancelled'); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and the added_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithAddedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - added_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['added_at'])) - ); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and the added_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithAddedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - added_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['added_at'])) - ); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and the created_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithCreatedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - created_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and the created_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithCreatedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - created_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_form_subscriptions() returns the expected data - * when a valid Form ID is specified and pagination parameters - * and per_page limits are specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsPagination() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_form_subscriptions() throws a ClientException when an invalid - * Form ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithInvalidFormID() - { - $result = $this->api->get_form_subscriptions(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_form_subscriptions() throws a ClientException when an invalid - * subscriber state is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithInvalidSubscriberState() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'not-a-valid-state' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_form_subscriptions() throws a ClientException when invalid - * pagination parameters are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetFormSubscriptionsWithInvalidPagination() - { - $result = $this->api->get_form_subscriptions( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_state: 'active', - after_cursor: 'not-a-valid-cursor' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_form_by_email() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmail() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form_by_email( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - email_address: $emailAddress - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['email_address'], - $emailAddress - ); - } - - /** - * Test that add_subscriber_to_form_by_email() returns the expected data - * when a referrer is specified. - * - * @since 2.1.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmailWithReferrer() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form_by_email( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - email_address: $emailAddress, - referrer: 'https://mywebsite.com/bfpromo/' - ); - - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['email_address'], - $emailAddress - ); - - // Assert referrer data set for form subscriber. - $this->assertEquals( - $result['subscriber']['referrer'], - 'https://mywebsite.com/bfpromo/' - ); - } - - /** - * Test that add_subscriber_to_form_by_email() returns the expected data - * when a referrer is specified that includes UTM parameters. - * - * @since 2.1.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmailWithReferrerUTMParams() - { - // Define referrer. - $referrerUTMParams = [ - 'utm_source' => 'facebook', - 'utm_medium' => 'cpc', - 'utm_campaign' => 'black_friday', - 'utm_term' => 'car_owners', - 'utm_content' => 'get_10_off', - ]; - $referrer = 'https://mywebsite.com/bfpromo/?' . http_build_query($referrerUTMParams); - - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form_by_email( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - email_address: $emailAddress, - referrer: $referrer - ); - - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['email_address'], - $emailAddress - ); - - // Assert referrer data set for form subscriber. - $this->assertEquals( - $result['subscriber']['referrer'], - $referrer - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['source'], - $referrerUTMParams['utm_source'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['medium'], - $referrerUTMParams['utm_medium'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['campaign'], - $referrerUTMParams['utm_campaign'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['term'], - $referrerUTMParams['utm_term'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['content'], - $referrerUTMParams['utm_content'] - ); - } - - /** - * Test that add_subscriber_to_form_by_email() returns a WP_Error when an invalid - * form is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmailWithInvalidformID() - { - $result = $this->api->add_subscriber_to_form_by_email( - form_id: 12345, - email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_form_by_email() returns a WP_Error when an invalid - * email address is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmailWithInvalidEmailAddress() - { - $result = $this->api->add_subscriber_to_form_by_email( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_form() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToForm() - { - // Create subscriber. - $subscriber = $this->api->create_subscriber($this->generateEmailAddress()); - - $this->assertNotInstanceOf(\WP_Error::class, $subscriber); - $this->assertIsArray($subscriber); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals($result['subscriber']['id'], $subscriber['subscriber']['id']); - } - - /** - * Test that add_subscriber_to_form() returns the expected data - * when a referrer is specified. - * - * @since 2.1.0 - * - * @return void - */ - public function testAddSubscriberToFormWithReferrer() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: $subscriber['subscriber']['id'], - referrer: 'https://mywebsite.com/bfpromo/' - ); - - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['id'], - $subscriber['subscriber']['id'] - ); - - // Assert referrer data set for form subscriber. - $this->assertEquals( - $result['subscriber']['referrer'], - 'https://mywebsite.com/bfpromo/' - ); - } - - /** - * Test that add_subscriber_to_form() returns the expected data - * when a referrer is specified that includes UTM parameters. - * - * @since 2.1.0 - * - * @return void - */ - public function testAddSubscriberToFormWithReferrerUTMParams() - { - // Define referrer. - $referrerUTMParams = [ - 'utm_source' => 'facebook', - 'utm_medium' => 'cpc', - 'utm_campaign' => 'black_friday', - 'utm_term' => 'car_owners', - 'utm_content' => 'get_10_off', - ]; - $referrer = 'https://mywebsite.com/bfpromo/?' . http_build_query($referrerUTMParams); - - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to form. - $result = $this->api->add_subscriber_to_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: $subscriber['subscriber']['id'], - referrer: $referrer - ); - - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['id'], - $subscriber['subscriber']['id'] - ); - - // Assert referrer data set for form subscriber. - $this->assertEquals( - $result['subscriber']['referrer'], - $referrer - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['source'], - $referrerUTMParams['utm_source'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['medium'], - $referrerUTMParams['utm_medium'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['campaign'], - $referrerUTMParams['utm_campaign'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['term'], - $referrerUTMParams['utm_term'] - ); - $this->assertEquals( - $result['subscriber']['referrer_utm_parameters']['content'], - $referrerUTMParams['utm_content'] - ); - } - - /** - * Test that add_subscriber_to_form() returns a WP_Error when an invalid - * form ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToFormWithInvalidFormID() - { - $result = $this->api->add_subscriber_to_form( - form_id: 12345, - subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_form() returns a WP_Error when a legacy - * form ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToFormWithLegacyFormID() - { - $result = $this->api->add_subscriber_to_form( - form_id: $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], - subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_form() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToformWithInvalidSubscriberID() - { - $result = $this->api->add_subscriber_to_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_legacy_form() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToLegacyForm() - { - // Create subscriber. - $subscriber = $this->api->create_subscriber($this->generateEmailAddress()); - - $this->assertNotInstanceOf(\WP_Error::class, $subscriber); - $this->assertIsArray($subscriber); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to legacy form. - $result = $this->api->add_subscriber_to_legacy_form( - form_id: (int) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals($result['subscriber']['id'], $subscriber['subscriber']['id']); - } - - /** - * Test that add_subscriber_to_legacy_form() returns a WP_Error when an invalid - * form ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToLegacyFormWithInvalidFormID() - { - $result = $this->api->add_subscriber_to_legacy_form( - form_id: 12345, - subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_legacy_form() returns a WP_Error when a non-legacy - * form ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToLegacyFormWithNonLegacyFormID() - { - $result = $this->api->add_subscriber_to_legacy_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_legacy_form() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToLegacyFormWithInvalidSubscriberID() - { - $result = $this->api->add_subscriber_to_legacy_form( - form_id: (int) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_sequences() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSequences() - { - $result = $this->api->get_sequences(); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'sequences'); - $this->assertPaginationExists($result); - - // Check first sequence in resultset has expected data. - $sequence = $result['sequences'][0]; - $this->assertArrayHasKey('id', $sequence); - $this->assertArrayHasKey('name', $sequence); - $this->assertArrayHasKey('hold', $sequence); - $this->assertArrayHasKey('repeat', $sequence); - $this->assertArrayHasKey('created_at', $sequence); - } - - /** - * Test that get_sequences() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequencesWithTotalCount() - { - $result = $this->api->get_sequences( - include_total_count: true - ); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'sequences'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_sequences() returns the expected data when - * pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequencesPagination() - { - // Return one sequence. - $result = $this->api->get_sequences( - per_page: 1 - ); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'sequences'); - $this->assertPaginationExists($result); - - // Assert a single sequence was returned. - $this->assertCount(1, $result['sequences']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_sequences( - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'sequences'); - $this->assertPaginationExists($result); - - // Assert a single sequence was returned. - $this->assertCount(1, $result['sequences']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertFalse($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_sequences( - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'sequences'); - $this->assertPaginationExists($result); - - // Assert a single sequence was returned. - $this->assertCount(1, $result['sequences']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that add_subscriber_to_sequence_by_email() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToSequenceByEmail() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Add subscriber to sequence. - $result = $this->api->add_subscriber_to_sequence_by_email( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email_address: $emailAddress - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals( - $result['subscriber']['email_address'], - $emailAddress - ); - } - - /** - * Test that add_subscriber_to_sequence_by_email() returns a WP_Error when an invalid - * sequence is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToSequenceByEmailWithInvalidSequenceID() - { - $result = $this->api->add_subscriber_to_sequence_by_email( - sequence_id: 12345, - email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_sequence_by_email() returns a WP_Error when an invalid - * email address is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToSequenceByEmailWithInvalidEmailAddress() - { - $result = $this->api->add_subscriber_to_sequence_by_email( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_sequence() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToSequence() - { - // Create subscriber. - $subscriber = $this->api->create_subscriber($this->generateEmailAddress()); - - $this->assertNotInstanceOf(\WP_Error::class, $subscriber); - $this->assertIsArray($subscriber); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $subscriber['subscriber']['id']; - - // Add subscriber to sequence. - $result = $this->api->add_subscriber_to_sequence( - sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertEquals($result['subscriber']['id'], $subscriber['subscriber']['id']); - } - - /** - * Test that add_subscriber_to_sequence() returns a WP_Error when an invalid - * sequence ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToSequenceWithInvalidSequenceID() - { - $result = $this->api->add_subscriber_to_sequence( - sequence_id: 12345, - subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that add_subscriber_to_sequence() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToSequenceWithInvalidSubscriberID() - { - $result = $this->api->add_subscriber_to_sequence( - sequence_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptions() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'] - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithTotalCount() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - include_total_count: true - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and the subscription status - * is cancelled. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithCancelledSubscriberState() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'cancelled' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertEquals($result['subscribers'][0]['state'], 'cancelled'); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and the added_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithAddedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - added_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['added_at'])) - ); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and the added_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithAddedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - added_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['added_at'])) - ); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and the created_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithCreatedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - created_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and the created_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithCreatedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - created_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_sequence_subscriptions() returns the expected data - * when a valid Sequence ID is specified and pagination parameters - * and per_page limits are specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsPagination() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_sequence_subscriptions( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'active', - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_sequence_subscriptions() returns a WP_Error when an invalid - * Sequence ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithInvalidSequenceID() - { - $result = $this->api->get_sequence_subscriptions(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_sequence_subscriptions() returns a WP_Error when an invalid - * subscriber state is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithInvalidSubscriberState() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - subscriber_state: 'not-a-valid-state' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_sequence_subscriptions() returns a WP_Error when invalid - * pagination parameters are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSequenceSubscriptionsWithInvalidPagination() - { - $result = $this->api->get_sequence_subscriptions( - sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - after_cursor: 'not-a-valid-cursor' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_tags() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetTags() - { - $result = $this->api->get_tags(); - - // Assert sequences and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Check first tag in resultset has expected data. - $tag = $result['tags'][0]; - $this->assertArrayHasKey('id', $tag); - $this->assertArrayHasKey('name', $tag); - $this->assertArrayHasKey('created_at', $tag); - } - - /** - * Test that get_tags() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagsWithTotalCount() - { - $result = $this->api->get_tags(true); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_tags() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagsPagination() - { - $result = $this->api->get_tags( - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert a single tag was returned. - $this->assertCount(1, $result['tags']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_tags( - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['tags']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_tags( - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - } - - /** - * Test that create_tag() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testCreateTag() - { - $tagName = 'Tag Test ' . wp_rand(); - - // Add mock handler for this API request, as the API doesn't provide - // a method to delete tags to cleanup the test. - $this->mockResponses( - 201, - 'Created', - wp_json_encode( - array( - 'tag' => array( - 'id' => 12345, - 'name' => $tagName, - 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', - ), - ) - ) - ); - - // Send request. - $result = $this->api->create_tag($tagName); - - // Assert response contains correct data. - $this->assertArrayHasKey('id', $result['tag']); - $this->assertArrayHasKey('name', $result['tag']); - $this->assertArrayHasKey('created_at', $result['tag']); - $this->assertEquals($result['tag']['name'], $tagName); - } - - /** - * Test that create_tag() returns a WP_Error when creating - * a blank tag. - * - * @since 1.0.0 - * - * @return void - */ - public function testCreateTagBlank() - { - $result = $this->api->create_tag(''); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_tag() returns the expected data when creating - * a tag that already exists. - * - * @since 1.0.0 - * - * @return void - */ - public function testCreateTagThatExists() - { - $result = $this->api->create_tag($_ENV['CONVERTKIT_API_TAG_NAME']); - - // Assert response contains correct data. - $this->assertArrayHasKey('id', $result['tag']); - $this->assertArrayHasKey('name', $result['tag']); - $this->assertArrayHasKey('created_at', $result['tag']); - $this->assertEquals($result['tag']['name'], $_ENV['CONVERTKIT_API_TAG_NAME']); - } - - /** - * Test that create_tags() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateTags() - { - $tagNames = [ - 'Tag Test ' . wp_rand(), - 'Tag Test ' . wp_rand(), - ]; - - // Add mock handler for this API request, as the API doesn't provide - // a method to delete tags to cleanup the test. - $this->mockResponses( - 201, - 'Created', - wp_json_encode( - array( - 'tags' => array( - array( - 'id' => 12345, - 'name' => $tagNames[0], - 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', - ), - array( - 'id' => 23456, - 'name' => $tagNames[1], - 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', - ), - ), - 'failures' => array(), - ) - ) - ); - - $result = $this->api->create_tags($tagNames); - - // Assert no failures. - $this->assertCount(0, $result['failures']); - } - - /** - * Test that create_tags() returns failures when attempting - * to create blank tags. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateTagsBlank() - { - $result = $this->api->create_tags( - [ - '', - '', - ] - ); - - // Assert failures. - $this->assertCount(2, $result['failures']); - } - - /** - * Test that create_tags() returns the expected data when creating - * tags that already exist. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateTagsThatExist() - { - $result = $this->api->create_tags( - [ - $_ENV['CONVERTKIT_API_TAG_NAME'], - $_ENV['CONVERTKIT_API_TAG_NAME_2'], - ] - ); - - // Assert existing tags are returned. - $this->assertCount(2, $result['tags']); - $this->assertEquals($result['tags'][1]['name'], $_ENV['CONVERTKIT_API_TAG_NAME']); - $this->assertEquals($result['tags'][0]['name'], $_ENV['CONVERTKIT_API_TAG_NAME_2']); - } - - /** - * Test that tag_subscriber_by_email() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testTagSubscriberByEmail() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $this->api->create_subscriber($emailAddress); - - // Tag subscriber by email. - $subscriber = $this->api->tag_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: $emailAddress - ); - $this->assertArrayHasKey('subscriber', $subscriber); - $this->assertArrayHasKey('id', $subscriber['subscriber']); - $this->assertArrayHasKey('tagged_at', $subscriber['subscriber']); - - // Confirm the subscriber is tagged. - $result = $this->api->get_subscriber_tags($subscriber['subscriber']['id']); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert correct tag was assigned. - $this->assertEquals($result['tags'][0]['id'], $_ENV['CONVERTKIT_API_TAG_ID']); - } - - /** - * Test that tag_subscriber_by_email() returns a WP_Error when an invalid - * tag is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testTagSubscriberByEmailWithInvalidTagID() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $this->api->create_subscriber($emailAddress); - - $result = $this->api->tag_subscriber_by_email( - tag_id: 12345, - email_address: $emailAddress - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that tag_subscriber_by_email() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testTagSubscriberByEmailWithInvalidEmailAddress() - { - $result = $this->api->tag_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that tag_subscriber() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testTagSubscriber() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - // Tag subscriber by email. - $result = $this->api->tag_subscriber( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertArrayHasKey('subscriber', $result); - $this->assertArrayHasKey('id', $result['subscriber']); - $this->assertArrayHasKey('tagged_at', $result['subscriber']); - - // Confirm the subscriber is tagged. - $result = $this->api->get_subscriber_tags($result['subscriber']['id']); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert correct tag was assigned. - $this->assertEquals($result['tags'][0]['id'], $_ENV['CONVERTKIT_API_TAG_ID']); - } - - /** - * Test that tag_subscriber() returns a WP_Error when an invalid - * sequence ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testTagSubscriberWithInvalidTagID() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $subscriber = $this->api->create_subscriber($emailAddress); - - $result = $this->api->tag_subscriber( - tag_id: 12345, - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that tag_subscriber() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testTagSubscriberWithInvalidSubscriberID() - { - $result = $this->api->tag_subscriber( - tag_id: $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that remove_tag_from_subscriber() works. - * - * @since 1.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriber() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $this->api->create_subscriber($emailAddress); - - // Tag subscriber by email. - $subscriber = $this->api->tag_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: $emailAddress - ); - - // Remove tag from subscriber. - $result = $this->api->remove_tag_from_subscriber( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - - // Confirm that the subscriber no longer has the tag. - $result = $this->api->get_subscriber_tags($subscriber['subscriber']['id']); - $this->assertIsArray($result['tags']); - $this->assertCount(0, $result['tags']); - } - - /** - * Test that remove_tag_from_subscriber() returns a WP_Error when an invalid - * tag ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriberWithInvalidTagID() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $this->api->create_subscriber($emailAddress); - - // Tag subscriber by email. - $subscriber = $this->api->tag_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: $emailAddress - ); - - // Remove tag from subscriber. - $result = $this->api->remove_tag_from_subscriber( - tag_id: 12345, - subscriber_id: $subscriber['subscriber']['id'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that remove_tag_from_subscriber() returns a WP_Error when an invalid - * subscriber ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriberWithInvalidSubscriberID() - { - $result = $this->api->remove_tag_from_subscriber( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that remove_tag_from_subscriber() works. - * - * @since 1.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriberByEmail() - { - // Create subscriber. - $emailAddress = $this->generateEmailAddress(); - $this->api->create_subscriber($emailAddress); - - // Tag subscriber by email. - $subscriber = $this->api->tag_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: $emailAddress - ); - - // Remove tag from subscriber. - $result = $this->api->remove_tag_from_subscriber( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_id: $subscriber['subscriber']['id'] - ); - - // Confirm that the subscriber no longer has the tag. - $result = $this->api->get_subscriber_tags($subscriber['subscriber']['id']); - $this->assertIsArray($result['tags']); - $this->assertCount(0, $result['tags']); - } - - /** - * Test that remove_tag_from_subscriber() returns a WP_Error when an invalid - * tag ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriberByEmailWithInvalidTagID() - { - $result = $this->api->remove_tag_from_subscriber_by_email( - tag_id: 12345, - email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that remove_tag_from_subscriber() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testRemoveTagFromSubscriberByEmailWithInvalidEmailAddress() - { - $result = $this->api->remove_tag_from_subscriber_by_email( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetTagSubscriptions() - { - $result = $this->api->get_tag_subscriptions( (int) $_ENV['CONVERTKIT_API_TAG_ID']); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithTotalCount() - { - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - include_total_count: true - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and the subscription status - * is bounced. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithCancelledSubscriberState() - { - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'cancelled' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertEquals($result['subscribers'][0]['state'], 'cancelled'); - } - - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and the added_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithTaggedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - tagged_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['tagged_at'])) - ); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and the tagged_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithTaggedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - tagged_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['tagged_at'])) - ); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and the created_after parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithCreatedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - created_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and the created_before parameter - * is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithCreatedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - created_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_tag_subscriptions() returns the expected data - * when a valid Tag ID is specified and pagination parameters - * and per_page limits are specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsPagination() - { - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_tag_subscriptions( - tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], - subscriber_state: 'active', - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_tag_subscriptions() returns a WP_Error when - * an invalid Tag ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetTagSubscriptionsWithInvalidTagID() - { - $result = $this->api->get_tag_subscriptions(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_subscribers() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribers() - { - $result = $this->api->get_subscribers(); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_subscribers() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithTotalCount() - { - $result = $this->api->get_subscribers( - include_total_count: true - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_subscribers() returns the expected data when - * searching by an email address. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersByEmailAddress() - { - $result = $this->api->get_subscribers( - email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert correct subscriber returned. - $this->assertEquals( - $result['subscribers'][0]['email_address'], - $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - } - - /** - * Test that get_subscribers() returns the expected data - * when the subscription status is cancelled. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscribersWithCancelledSubscriberState() - { - $result = $this->api->get_subscribers( - subscriber_state: 'cancelled' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertEquals($result['subscribers'][0]['state'], 'cancelled'); - } - - /** - * Test that get_subscribers() returns the expected data - * when the created_after parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithCreatedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_subscribers( - created_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertGreaterThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_subscribers() returns the expected data - * when the created_before parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithCreatedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_subscribers( - created_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Check the correct subscribers were returned. - $this->assertLessThanOrEqual( - $date->format('Y-m-d'), - date('Y-m-d', strtotime($result['subscribers'][0]['created_at'])) - ); - } - - /** - * Test that get_subscribers() returns the expected data - * when the updated_after parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithUpdatedAfterParam() - { - $date = new \DateTime('2022-01-01'); - $result = $this->api->get_subscribers( - updated_after: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_subscribers() returns the expected data - * when the updated_before parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithUpdatedBeforeParam() - { - $date = new \DateTime('2024-01-01'); - $result = $this->api->get_subscribers( - updated_before: $date - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_subscribers() returns the expected data - * when the sort_field parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithSortFieldParam() - { - $result = $this->api->get_subscribers( - sort_field: 'id' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert sorting is honored by ID in descending (default) order. - $this->assertLessThanOrEqual( - $result['subscribers'][0]['id'], - $result['subscribers'][1]['id'] - ); - } - - /** - * Test that get_subscribers() returns the expected data - * when the sort_order parameter is used. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithSortOrderParam() - { - $result = $this->api->get_subscribers( - sort_field: 'id', - sort_order: 'asc' - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert sorting is honored by ID (default) in ascending order. - $this->assertGreaterThanOrEqual( - $result['subscribers'][0]['id'], - $result['subscribers'][1]['id'] - ); - } - - /** - * Test that get_subscribers() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersPagination() - { - // Return one broadcast. - $result = $this->api->get_subscribers( - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single subscriber was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_subscribers( - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - - // Assert a single broadcast was returned. - $this->assertCount(1, $result['subscribers']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_subscribers( - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert subscribers and pagination exist. - $this->assertDataExists($result, 'subscribers'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_subscribers() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithInvalidEmailAddress() - { - $result = $this->api->get_subscribers( - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscribers() returns a WP_Error when an invalid - * subscriber state is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithInvalidSubscriberState() - { - $result = $this->api->get_subscribers( - subscriber_state: 'not-a-valid-state' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscribers() returns a WP_Error when an invalid - * sort field is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithInvalidSortFieldParam() - { - $result = $this->api->get_subscribers( - sort_field: 'not-a-valid-sort-field' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscribers() returns a WP_Error when an invalid - * sort order is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithInvalidSortOrderParam() - { - $result = $this->api->get_subscribers( - sort_order: 'not-a-valid-sort-order' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscribers() returns a WP_Error when an invalid - * pagination parameters are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscribersWithInvalidPagination() - { - $result = $this->api->get_subscribers( - after_cursor: 'not-a-valid-cursor' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that create_subscriber() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriber() - { - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - } - - /** - * Test that create_subscriber() returns the expected data - * when a first name is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithFirstName() - { - $firstName = 'FirstName'; - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber( - email_address: $emailAddress, - first_name: $firstName - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - $this->assertEquals($result['subscriber']['first_name'], $firstName); - } - - /** - * Test that create_subscriber() returns the expected data - * when a subscriber state is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithSubscriberState() - { - $subscriberState = 'cancelled'; - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber( - email_address: $emailAddress, - subscriber_state: $subscriberState - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - $this->assertEquals($result['subscriber']['state'], $subscriberState); - } - - /** - * Test that create_subscriber() returns the expected data - * when custom field data is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithCustomFields() - { - $lastName = 'LastName'; - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber( - email_address: $emailAddress, - fields: [ - 'last_name' => $lastName, - ] - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - $this->assertEquals($result['subscriber']['fields']['last_name'], $lastName); - } - - /** - * Test that create_subscriber() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithInvalidEmailAddress() - { - $result = $this->api->create_subscriber( - email_address: 'not-an-email-address' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_subscriber() returns a WP_Error when an invalid - * subscriber state is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithInvalidSubscriberState() - { - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber( - email_address: $emailAddress, - subscriber_state: 'not-a-valid-state' - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_subscriber() returns the expected data - * when an invalid custom field is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscriberWithInvalidCustomFields() - { - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber( - email_address: $emailAddress, - fields: [ - 'not_a_custom_field' => 'value', - ] - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - } - - /** - * Test that create_subscribers() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscribers() - { - $subscribers = [ - [ - 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), - ], - [ - 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), - ], - ]; - $result = $this->api->create_subscribers($subscribers); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - foreach ($result['subscribers'] as $i => $subscriber) { - $this->subscriber_ids[] = $subscriber['id']; - } - - // Assert no failures. - $this->assertCount(0, $result['failures']); - - // Assert subscribers exists with correct data. - foreach ($result['subscribers'] as $i => $subscriber) { - $this->assertEquals($subscriber['email_address'], $subscribers[ $i ]['email_address']); - } - } - - /** - * Test that create_subscribers() returns a WP_Error when no data is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscribersWithBlankData() - { - $result = $this->api->create_subscribers( - [ - [], - ] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_subscribers() returns the expected data when invalid email addresses - * are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateSubscribersWithInvalidEmailAddresses() - { - $subscribers = [ - [ - 'email_address' => 'not-an-email-address', - ], - [ - 'email_address' => 'not-an-email-address-again', - ], - ]; - $result = $this->api->create_subscribers($subscribers); - - // Assert no subscribers were added. - $this->assertCount(0, $result['subscribers']); - $this->assertCount(2, $result['failures']); - } - - /** - * Test that get_subscriber_id() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberID() - { - $subscriber_id = $this->api->get_subscriber_id($_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); - $this->assertIsInt($subscriber_id); - $this->assertEquals($subscriber_id, (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - } - - /** - * Test that get_subscriber_id() returns a WP_Error when an invalid - * email address is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberIDWithInvalidEmailAddress() - { - $result = $this->api->get_subscriber_id('not-an-email-address'); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_subscriber_id() return false when no subscriber found - * matching the given email address. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberIDWithNotSubscribedEmailAddress() - { - $result = $this->api->get_subscriber_id('not-a-subscriber@test.com'); - $this->assertFalse($result); - } - - /** - * Test that get_subscriber() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriber() - { - $result = $this->api->get_subscriber( (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['id'], $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $this->assertEquals($result['subscriber']['email_address'], $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); - } - - /** - * Test that get_subscriber() returns a WP_Error when an invalid - * subscriber ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberWithInvalidSubscriberID() - { - $result = $this->api->get_subscriber(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that update_subscriber() works when no changes are made. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateSubscriberWithNoChanges() - { - $result = $this->api->update_subscriber($_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - - // Assert subscriber exists with correct data. - $this->assertEquals($result['subscriber']['id'], $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $this->assertEquals($result['subscriber']['email_address'], $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); - } - - /** - * Test that update_subscriber() works when updating the subscriber's first name. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateSubscriberFirstName() - { - // Add a subscriber. - $firstName = 'FirstName'; - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber created with no first name. - $this->assertNull($result['subscriber']['first_name']); - - // Get subscriber ID. - $subscriberID = $result['subscriber']['id']; - - // Update subscriber's first name. - $result = $this->api->update_subscriber( - subscriber_id: $subscriberID, - first_name: $firstName - ); - - // Assert changes were made. - $this->assertEquals($result['subscriber']['id'], $subscriberID); - $this->assertEquals($result['subscriber']['first_name'], $firstName); - } - - /** - * Test that update_subscriber() works when updating the subscriber's email address. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateSubscriberEmailAddress() - { - // Add a subscriber. - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber created. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - - // Get subscriber ID. - $subscriberID = $result['subscriber']['id']; - - // Update subscriber's email address. - $newEmail = $this->generateEmailAddress(); - $result = $this->api->update_subscriber( - subscriber_id: $subscriberID, - email_address: $newEmail - ); - - // Assert changes were made. - $this->assertEquals($result['subscriber']['id'], $subscriberID); - $this->assertEquals($result['subscriber']['email_address'], $newEmail); - } - - /** - * Test that update_subscriber() works when updating the subscriber's custom fields. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateSubscriberCustomFields() - { - // Add a subscriber. - $lastName = 'LastName'; - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set subscriber_id to ensure subscriber is unsubscribed after test. - $this->subscriber_ids[] = $result['subscriber']['id']; - - // Assert subscriber created. - $this->assertEquals($result['subscriber']['email_address'], $emailAddress); - - // Get subscriber ID. - $subscriberID = $result['subscriber']['id']; - - // Update subscriber's custom fields. - $result = $this->api->update_subscriber( - subscriber_id: $subscriberID, - fields: [ - 'last_name' => $lastName, - ] - ); - - // Assert changes were made. - $this->assertEquals($result['subscriber']['id'], $subscriberID); - $this->assertEquals($result['subscriber']['fields']['last_name'], $lastName); - } - - /** - * Test that update_subscriber() returns a WP_Error when an invalid - * subscriber ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateSubscriberWithInvalidSubscriberID() - { - $result = $this->api->update_subscriber(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that unsubscribe_by_email() works with a valid subscriber email address. - * - * @since 1.0.0 - * - * @return void - */ - public function testUnsubscribeByEmail() - { - // Add a subscriber. - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Wait a moment to ensure subscriber is created. - sleep(3); - - // Unsubscribe. - $this->assertNull($this->api->unsubscribe_by_email($emailAddress)); - } - - /** - * Test that unsubscribe_by_email() returns a WP_Error when an email - * address is specified that is not subscribed. - * - * @since 1.0.0 - * - * @return void - */ - public function testUnsubscribeByEmailWithNotSubscribedEmailAddress() - { - $result = $this->api->unsubscribe_by_email('not-subscribed@kit.com'); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that unsubscribe_by_email() returns a WP_Error when an invalid - * email address is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testUnsubscribeByEmailWithInvalidEmailAddress() - { - $result = $this->api->unsubscribe_by_email('invalid-email'); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that unsubscribe() works with a valid subscriber ID. - * - * @since 2.0.0 - * - * @return void - */ - public function testUnsubscribe() - { - // Add a subscriber. - $emailAddress = $this->generateEmailAddress(); - $result = $this->api->create_subscriber($emailAddress); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Wait a moment to ensure subscriber is created. - sleep(3); - - // Unsubscribe. - $this->assertNull($this->api->unsubscribe($result['subscriber']['id'])); - } - - /** - * Test that unsubscribe() returns a WP_Error when an invalid - * subscriber ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testUnsubscribeWithInvalidSubscriberID() - { - $result = $this->api->unsubscribe(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscriber_tags() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberTags() - { - $result = $this->api->get_subscriber_tags( (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_subscriber_tags() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscriberTagsWithTotalCount() - { - $result = $this->api->get_subscriber_tags( - subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], - include_total_count: true - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_subscriber_tags() returns a WP_Error when an invalid - * subscriber ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSubscriberTagsWithInvalidSubscriberID() - { - $result = $this->api->get_subscriber_tags(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_subscriber_tags() returns the expected data - * when a valid Subscriber ID is specified and pagination parameters - * and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSubscriberTagsPagination() - { - $result = $this->api->get_subscriber_tags( - subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], - include_total_count: false, - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert a single tag was returned. - $this->assertCount(1, $result['tags']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_subscriber_tags( - subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], - include_total_count: false, - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert a single tag was returned. - $this->assertCount(1, $result['tags']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_subscriber_tags( - subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], - include_total_count: false, - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert tags and pagination exist. - $this->assertDataExists($result, 'tags'); - $this->assertPaginationExists($result); - - // Assert a single tag was returned. - $this->assertCount(1, $result['tags']); - } - - /** - * Test that get_email_templates() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetEmailTemplates() - { - $result = $this->api->get_email_templates(); - - // Assert email templates and pagination exist. - $this->assertDataExists($result, 'email_templates'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_email_templates() returns the expected data - * when the total count is included. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetEmailTemplatesWithTotalCount() - { - $result = $this->api->get_email_templates(true); - - // Assert email templates and pagination exist. - $this->assertDataExists($result, 'email_templates'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_email_templates() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetEmailTemplatesPagination() - { - // Return one broadcast. - $result = $this->api->get_email_templates( - include_total_count: false, - per_page: 1 - ); - - // Assert email templates and pagination exist. - $this->assertDataExists($result, 'email_templates'); - $this->assertPaginationExists($result); - - // Assert a single email template was returned. - $this->assertCount(1, $result['email_templates']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_email_templates( - include_total_count: false, - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert email templates and pagination exist. - $this->assertDataExists($result, 'email_templates'); - $this->assertPaginationExists($result); - - // Assert a single email template was returned. - $this->assertCount(1, $result['email_templates']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_email_templates( - include_total_count: false, - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert email templates and pagination exist. - $this->assertDataExists($result, 'email_templates'); - $this->assertPaginationExists($result); - - // Assert a single email template was returned. - $this->assertCount(1, $result['email_templates']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that get_broadcasts() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetBroadcastsPagination() - { - // Return one broadcast. - $result = $this->api->get_broadcasts( - include_total_count: false, - per_page: 1 - ); - - // Assert broadcasts and pagination exist. - $this->assertDataExists($result, 'broadcasts'); - $this->assertPaginationExists($result); - - // Assert a single broadcast was returned. - $this->assertCount(1, $result['broadcasts']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_broadcasts( - include_total_count: false, - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert broadcasts and pagination exist. - $this->assertDataExists($result, 'broadcasts'); - $this->assertPaginationExists($result); - - // Assert a single broadcast was returned. - $this->assertCount(1, $result['broadcasts']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_broadcasts( - include_total_count: false, - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert broadcasts and pagination exist. - $this->assertDataExists($result, 'broadcasts'); - $this->assertPaginationExists($result); - - // Assert a single broadcast was returned. - $this->assertCount(1, $result['broadcasts']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that create_broadcast(), update_broadcast() and delete_broadcast() works - * when specifying valid published_at and send_at values. - * - * We do all tests in a single function, so we don't end up with unnecessary Broadcasts remaining - * on the ConvertKit account when running tests, which might impact - * other tests that expect (or do not expect) specific Broadcasts. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateUpdateAndDeleteDraftBroadcast() - { - // Create a broadcast first. - $result = $this->api->create_broadcast( - subject: 'Test Subject', - content: 'Test Content', - description: 'Test Broadcast from WordPress Libraries' - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Store Broadcast ID. - $broadcastID = $result['broadcast']['id']; - - // Confirm the Broadcast saved. - $this->assertArrayHasKey('broadcast', $result); - $this->assertArrayHasKey('id', $result['broadcast']); - $this->assertEquals('Test Subject', $result['broadcast']['subject']); - $this->assertEquals('Test Content', $result['broadcast']['content']); - $this->assertEquals('Test Broadcast from WordPress Libraries', $result['broadcast']['description']); - $this->assertEquals(null, $result['broadcast']['published_at']); - $this->assertEquals(null, $result['broadcast']['send_at']); - - // Update the existing broadcast. - $result = $this->api->update_broadcast( - id: $broadcastID, - subject: 'New Test Subject', - content: 'New Test Content', - description: 'New Test Broadcast from WordPress Libraries' - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Confirm the changes saved. - $this->assertArrayHasKey('broadcast', $result); - $this->assertArrayHasKey('id', $result['broadcast']); - $this->assertEquals('New Test Subject', $result['broadcast']['subject']); - $this->assertEquals('New Test Content', $result['broadcast']['content']); - $this->assertEquals('New Test Broadcast from WordPress Libraries', $result['broadcast']['description']); - $this->assertEquals(null, $result['broadcast']['published_at']); - $this->assertEquals(null, $result['broadcast']['send_at']); - - // Delete Broadcast. - $result = $this->api->delete_broadcast($broadcastID); - $this->assertNotInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that create_broadcast() works when specifying valid published_at and send_at values. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreatePublicBroadcastWithValidDates() - { - // Create DateTime object. - $publishedAt = new \DateTime('now'); - $publishedAt->modify('+7 days'); - $sendAt = new \DateTime('now'); - $sendAt->modify('+14 days'); - - // Create broadcast first. - $result = $this->api->create_broadcast( - subject: 'Test Subject', - content: 'Test Content', - description: 'Test Broadcast from WordPress Libraries', - public: true, - published_at: $publishedAt, - send_at: $sendAt - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Store Broadcast ID. - $broadcastID = $result['broadcast']['id']; - - // Set broadcast_id to ensure broadcast is deleted after test. - $this->broadcast_ids[] = $broadcastID; - - // Confirm the Broadcast saved. - $this->assertArrayHasKey('id', $result['broadcast']); - $this->assertEquals('Test Subject', $result['broadcast']['subject']); - $this->assertEquals('Test Content', $result['broadcast']['content']); - $this->assertEquals('Test Broadcast from WordPress Libraries', $result['broadcast']['description']); - $this->assertEquals( - $publishedAt->format('Y-m-d') . 'T' . $publishedAt->format('H:i:s') . 'Z', - $result['broadcast']['published_at'] - ); - $this->assertEquals( - $sendAt->format('Y-m-d') . 'T' . $sendAt->format('H:i:s') . 'Z', - $result['broadcast']['send_at'] - ); - } - - /** - * Test that get_broadcast() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetBroadcast() - { - $result = $this->api->get_broadcast($_ENV['CONVERTKIT_API_BROADCAST_ID']); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('id', $result['broadcast']); - $this->assertEquals($result['broadcast']['id'], $_ENV['CONVERTKIT_API_BROADCAST_ID']); - } - - /** - * Test that get_broadcast() returns a WP_Error when an invalid - * broadcast ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetBroadcastWithInvalidBroadcastID() - { - $result = $this->api->get_broadcast(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_broadcast_stats() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetBroadcastStats() - { - $result = $this->api->get_broadcast_stats($_ENV['CONVERTKIT_API_BROADCAST_ID']); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - $this->assertArrayHasKey('broadcast', $result); - $this->assertArrayHasKey('id', $result['broadcast']); - $this->assertArrayHasKey('stats', $result['broadcast']); - $this->assertEquals($result['broadcast']['stats']['recipients'], 1); - $this->assertEquals($result['broadcast']['stats']['open_rate'], 0); - $this->assertEquals($result['broadcast']['stats']['click_rate'], 0); - $this->assertEquals($result['broadcast']['stats']['unsubscribes'], 0); - $this->assertEquals($result['broadcast']['stats']['total_clicks'], 0); - } - - /** - * Test that get_broadcast_stats() returns a WP_Error when an invalid - * broadcast ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetBroadcastStatsWithInvalidBroadcastID() - { - $result = $this->api->get_broadcast_stats(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that update_broadcast() returns a WP_Error when an invalid - * broadcast ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testUpdateBroadcastWithInvalidBroadcastID() - { - $result = $this->api->update_broadcast(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that delete_broadcast() returns a WP_Error when an invalid - * broadcast ID is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testDeleteBroadcastWithInvalidBroadcastID() - { - $result = $this->api->delete_broadcast(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that get_webhooks() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetWebhooksPagination() - { - // Create webhooks first. - $results = [ - $this->api->create_webhook( - 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), - 'subscriber.subscriber_activate' - ), - $this->api->create_webhook( - 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), - 'subscriber.subscriber_activate' - ), - ]; - - // Set webhook_ids to ensure webhooks are deleted after test. - $this->webhook_ids = [ - $results[0]['webhook']['id'], - $results[1]['webhook']['id'], - ]; - - // Get webhooks. - $result = $this->api->get_webhooks( - include_total_count: false, - per_page: 1 - ); - - // Assert webhooks and pagination exist. - $this->assertDataExists($result, 'webhooks'); - $this->assertPaginationExists($result); - - // Assert a single webhook was returned. - $this->assertCount(1, $result['webhooks']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_webhooks( - include_total_count: false, - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert webhooks and pagination exist. - $this->assertDataExists($result, 'webhooks'); - $this->assertPaginationExists($result); - - // Assert a single webhook was returned. - $this->assertCount(1, $result['webhooks']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertFalse($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_webhooks( - include_total_count: false, - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert webhooks and pagination exist. - $this->assertDataExists($result, 'webhooks'); - $this->assertPaginationExists($result); - - // Assert a single webhook was returned. - $this->assertCount(1, $result['webhooks']); - } - - /** - * Test that create_webhook(), get_webhooks() and delete_webhook() works. - * - * We do both, so we don't end up with unnecessary webhooks remaining - * on the ConvertKit account when running tests. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateGetAndDeleteWebhook() - { - // Create a webhook first. - $result = $this->api->create_webhook( - url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), - event: 'subscriber.subscriber_activate' - ); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Store ID. - $id = $result['webhook']['id']; - - // Get webhooks. - $result = $this->api->get_webhooks(); - - // Assert webhooks and pagination exist. - $this->assertDataExists($result, 'webhooks'); - $this->assertPaginationExists($result); - - // Get webhooks including total count. - $result = $this->api->get_webhooks(true); - - // Assert webhooks and pagination exist. - $this->assertDataExists($result, 'webhooks'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - - // Delete the webhook. - $result = $this->api->delete_webhook($id); - $this->assertNotInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that create_webhook() works with an event parameter. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateWebhookWithEventParameter() - { - // Create a webhook. - $url = 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'); - $result = $this->api->create_webhook( - url: $url, - event: 'subscriber.form_subscribe', - parameter: $_ENV['CONVERTKIT_API_FORM_ID'] - ); - - // Confirm webhook created with correct data. - $this->assertArrayHasKey('webhook', $result); - $this->assertArrayHasKey('id', $result['webhook']); - $this->assertArrayHasKey('target_url', $result['webhook']); - $this->assertEquals($result['webhook']['target_url'], $url); - $this->assertEquals($result['webhook']['event']['name'], 'form_subscribe'); - $this->assertEquals($result['webhook']['event']['form_id'], $_ENV['CONVERTKIT_API_FORM_ID']); - - // Delete the webhook. - $result = $this->api->delete_webhook($result['webhook']['id']); - } - - /** - * Test that create_webhook() throws an InvalidArgumentException when an invalid - * event is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateWebhookWithInvalidEvent() - { - $this->expectException(\InvalidArgumentException::class); - $this->api->create_webhook( - url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), - event: 'invalid.event' - ); - } - - /** - * Test that delete_webhook() returns a WP_Error when an invalid - * ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testDeleteWebhookWithInvalidID() - { - $result = $this->api->delete_webhook(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_custom_fields() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetCustomFields() - { - $result = $this->api->get_custom_fields(); - - // Assert custom fields and pagination exist. - $this->assertDataExists($result, 'custom_fields'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_custom_fields() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetCustomFieldsWithTotalCount() - { - $result = $this->api->get_custom_fields(true); - - // Assert custom fields and pagination exist. - $this->assertDataExists($result, 'custom_fields'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_custom_fields() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetCustomFieldsPagination() - { - // Return one custom field. - $result = $this->api->get_custom_fields( - include_total_count: false, - per_page: 1 - ); - - // Assert custom fields and pagination exist. - $this->assertDataExists($result, 'custom_fields'); - $this->assertPaginationExists($result); - - // Assert a single custom field was returned. - $this->assertCount(1, $result['custom_fields']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_custom_fields( - include_total_count: false, - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert custom fields and pagination exist. - $this->assertDataExists($result, 'custom_fields'); - $this->assertPaginationExists($result); - - // Assert a single custom field was returned. - $this->assertCount(1, $result['custom_fields']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); + $tagName = 'Tag Test ' . wp_rand(); - // Use pagination to fetch previous page. - $result = $this->api->get_custom_fields( - include_total_count: false, - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 + // Add mock handler for this API request, as the API doesn't provide + // a method to delete tags to cleanup the test. + $this->mockResponses( + 201, + 'Created', + wp_json_encode( + array( + 'tag' => array( + 'id' => 12345, + 'name' => $tagName, + 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', + ), + ) + ) ); - // Assert custom fields and pagination exist. - $this->assertDataExists($result, 'custom_fields'); - $this->assertPaginationExists($result); - - // Assert a single custom field was returned. - $this->assertCount(1, $result['custom_fields']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that create_custom_field() works. - * - * @since 1.0.0 - * - * @return void - */ - public function testCreateCustomField() - { - $label = 'Custom Field ' . wp_rand(); - $result = $this->api->create_custom_field($label); - - // Test array was returned. - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Set custom_field_ids to ensure custom fields are deleted after test. - $this->custom_field_ids[] = $result['custom_field']['id']; - - $this->assertArrayHasKey('custom_field', $result); - $this->assertArrayHasKey('id', $result['custom_field']); - $this->assertArrayHasKey('name', $result['custom_field']); - $this->assertArrayHasKey('key', $result['custom_field']); - $this->assertArrayHasKey('label', $result['custom_field']); - $this->assertEquals($result['custom_field']['label'], $label); - } + // Send request. + $result = $this->api->create_tag($tagName); - /** - * Test that create_custom_field() returns a WP_Error when a blank - * label is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreateCustomFieldWithBlankLabel() - { - $result = $this->api->create_custom_field(''); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); + // Assert response contains correct data. + $this->assertArrayHasKey('id', $result['tag']); + $this->assertArrayHasKey('name', $result['tag']); + $this->assertArrayHasKey('created_at', $result['tag']); + $this->assertEquals($result['tag']['name'], $tagName); } - /** - * Test that create_custom_fields() works. + * Test that create_tags() returns the expected data. * * @since 2.0.0 * * @return void */ - public function testCreateCustomFields() + public function testCreateTags() { - $labels = [ - 'Custom Field ' . wp_rand(), - 'Custom Field ' . wp_rand(), + $tagNames = [ + 'Tag Test ' . wp_rand(), + 'Tag Test ' . wp_rand(), ]; - $result = $this->api->create_custom_fields($labels); - // Test array was returned. - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); + // Add mock handler for this API request, as the API doesn't provide + // a method to delete tags to cleanup the test. + $this->mockResponses( + 201, + 'Created', + wp_json_encode( + array( + 'tags' => array( + array( + 'id' => 12345, + 'name' => $tagNames[0], + 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', + ), + array( + 'id' => 23456, + 'name' => $tagNames[1], + 'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z', + ), + ), + 'failures' => array(), + ) + ) + ); - // Set custom_field_ids to ensure custom fields are deleted after test. - foreach ($result['custom_fields'] as $index => $customField) { - $this->custom_field_ids[] = $customField['id']; - } + $result = $this->api->create_tags($tagNames); // Assert no failures. $this->assertCount(0, $result['failures']); - - // Confirm result is an array comprising of each custom field that was created. - $this->assertIsArray($result['custom_fields']); } - /** - * Test that update_custom_field() works. + * Test that create_broadcast(), update_broadcast() and delete_broadcast() works + * when specifying valid published_at and send_at values. + * + * We do all tests in a single function, so we don't end up with unnecessary Broadcasts remaining + * on the ConvertKit account when running tests, which might impact + * other tests that expect (or do not expect) specific Broadcasts. * * @since 2.0.0 * * @return void */ - public function testUpdateCustomField() + public function testCreateUpdateAndDeleteDraftBroadcast() { - // Create custom field. - $label = 'Custom Field ' . wp_rand(); - $result = $this->api->create_custom_field($label); - - // Test array was returned. + // Create a broadcast first. + $result = $this->api->create_broadcast( + subject: 'Test Subject', + content: 'Test Content', + description: 'Test Broadcast from WordPress Libraries' + ); $this->assertNotInstanceOf(\WP_Error::class, $result); $this->assertIsArray($result); - // Store ID. - $id = $result['custom_field']['id']; - - // Set custom_field_ids to ensure custom fields are deleted after test. - $this->custom_field_ids[] = $result['custom_field']['id']; - - // Change label. - $newLabel = 'Custom Field ' . wp_rand(); - $this->api->update_custom_field($id, $newLabel); - - // Confirm label changed. - $customFields = $this->api->get_custom_fields(); - foreach ($customFields['custom_fields'] as $customField) { - if ($customField['id'] === $id) { - $this->assertEquals($customField['label'], $newLabel); - } - } - } - - /** - * Test that update_custom_field() returns a WP_Error when an - * invalid custom field ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testUpdateCustomFieldWithInvalidID() - { - $result = $this->api->update_custom_field(12345, 'Something'); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } + // Store Broadcast ID. + $broadcastID = $result['broadcast']['id']; - /** - * Test that delete_custom_field() works. - * - * @since 2.0.0 - * - * @return void - */ - public function testDeleteCustomField() - { - // Create custom field. - $label = 'Custom Field ' . wp_rand(); - $result = $this->api->create_custom_field($label); + // Confirm the Broadcast saved. + $this->assertArrayHasKey('broadcast', $result); + $this->assertArrayHasKey('id', $result['broadcast']); + $this->assertEquals('Test Subject', $result['broadcast']['subject']); + $this->assertEquals('Test Content', $result['broadcast']['content']); + $this->assertEquals('Test Broadcast from WordPress Libraries', $result['broadcast']['description']); + $this->assertEquals(null, $result['broadcast']['published_at']); + $this->assertEquals(null, $result['broadcast']['send_at']); - // Test array was returned. + // Update the existing broadcast. + $result = $this->api->update_broadcast( + id: $broadcastID, + subject: 'New Test Subject', + content: 'New Test Content', + description: 'New Test Broadcast from WordPress Libraries' + ); $this->assertNotInstanceOf(\WP_Error::class, $result); $this->assertIsArray($result); - // Store ID. - $id = $result['custom_field']['id']; - - // Delete custom field as tests passed. - $this->api->delete_custom_field($id); - - // Confirm custom field no longer exists. - $customFields = $this->api->get_custom_fields(); - foreach ($customFields['custom_fields'] as $customField) { - $this->assertNotEquals($customField['id'], $id); - } - } - - /** - * Test that delete_custom_field() returns a WP_Error when an - * invalid custom field ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testDeleteCustomFieldWithInvalidID() - { - $result = $this->api->delete_custom_field(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } + // Confirm the changes saved. + $this->assertArrayHasKey('broadcast', $result); + $this->assertArrayHasKey('id', $result['broadcast']); + $this->assertEquals('New Test Subject', $result['broadcast']['subject']); + $this->assertEquals('New Test Content', $result['broadcast']['content']); + $this->assertEquals('New Test Broadcast from WordPress Libraries', $result['broadcast']['description']); + $this->assertEquals(null, $result['broadcast']['published_at']); + $this->assertEquals(null, $result['broadcast']['send_at']); + // Delete Broadcast. + $result = $this->api->delete_broadcast($broadcastID); + $this->assertNotInstanceOf(\WP_Error::class, $result); + } /** * Test that the `form_subscribe()` function returns the expected data. * @@ -5566,34 +1437,6 @@ public function testSequenceSubscribeWithInvalidEmailAddress() $this->assertInstanceOf(\WP_Error::class, $result); $this->assertEquals($result->get_error_code(), $this->errorCode); } - - /** - * Test that the `get_posts()` function returns expected data. - * - * @since 1.0.0 - */ - public function testGetPosts() - { - $result = $this->api->get_posts(); - - // Test array was returned. - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - - // Test expected response keys exist. - $this->assertArrayHasKey('total_posts', $result); - $this->assertArrayHasKey('page', $result); - $this->assertArrayHasKey('total_pages', $result); - $this->assertArrayHasKey('posts', $result); - - // Test first post within posts array. - $this->assertArrayHasKey('id', reset($result['posts'])); - $this->assertArrayHasKey('title', reset($result['posts'])); - $this->assertArrayHasKey('url', reset($result['posts'])); - $this->assertArrayHasKey('published_at', reset($result['posts'])); - $this->assertArrayHasKey('is_paid', reset($result['posts'])); - } - /** * Test that the `get_posts()` function returns a blank array when no data * exists on the ConvertKit account. @@ -6010,373 +1853,6 @@ public function testProfilesWithNoSignedSubscriberID() $this->assertInstanceOf(\WP_Error::class, $result); $this->assertEquals($result->get_error_code(), $this->errorCode); } - - /** - * Test that get_purchases() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetPurchases() - { - $result = $this->api->get_purchases(); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_purchases() returns the expected data - * when the total count is included. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetPurchasesWithTotalCount() - { - $result = $this->api->get_purchases( - include_total_count: true - ); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_purchases() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetPurchasesPagination() - { - $result = $this->api->get_purchases( - per_page: 1 - ); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - - // Assert a single purchase was returned. - $this->assertCount(1, $result['purchases']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_purchases( - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - - // Assert a single purchase was returned. - $this->assertCount(1, $result['purchases']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_purchases( - before_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - - // Assert a single purchase was returned. - $this->assertCount(1, $result['purchases']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - - /** - * Test that get_purchases() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetPurchase() - { - $result = $this->api->get_purchases( - per_page: 1 - ); - - // Assert purchases and pagination exist. - $this->assertDataExists($result, 'purchases'); - $this->assertPaginationExists($result); - - // Assert a single purchase was returned. - $this->assertCount(1, $result['purchases']); - - // Get ID. - $id = $result['purchases'][0]['id']; - - // Get purchase. - $result = $this->api->get_purchase($id); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertEquals($result['purchase']['id'], $id); - } - - /** - * Test that get_purchases() returns a WP_Error when an invalid - * purchase ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetPurchaseWithInvalidID() - { - $result = $this->api->get_purchase(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_purchase() returns the expected data. - * - * @since 1.0.0 - * - * @return void - */ - public function testCreatePurchase() - { - $result = $this->api->create_purchase( - // Required fields. - email_address: $this->generateEmailAddress(), - transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), - products: [ - [ - 'name' => 'Floppy Disk (512k)', - 'sku' => '7890-ijkl', - 'pid' => 9999, - 'lid' => 7777, - 'quantity' => 2, - 'unit_price' => 5.00, - ], - [ - 'name' => 'Telephone Cord (data)', - 'sku' => 'mnop-1234', - 'pid' => 5555, - 'lid' => 7778, - 'quantity' => 1, - 'unit_price' => 10.00, - ], - ], - // Optional fields. - currency: 'usd', - first_name: 'Tim', - status: 'paid', - subtotal: 20.00, - tax: 2.00, - shipping: 2.00, - discount: 3.00, - total: 21.00, - transaction_time: new \DateTime('now') - ); - - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('transaction_id', $result['purchase']); - } - - /** - * Test that create_purchase() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreatePurchaseWithInvalidEmailAddress() - { - $result = $this->api->create_purchase( - email_address: 'not-an-email-address', - transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), - products: [ - [ - 'name' => 'Floppy Disk (512k)', - 'sku' => '7890-ijkl', - 'pid' => 9999, - 'lid' => 7777, - 'quantity' => 2, - 'unit_price' => 5.00, - ], - ] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_purchase() returns a WP_Error when a blank - * transaction ID is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreatePurchaseWithBlankTransactionID() - { - $result = $this->api->create_purchase( - email_address: $this->generateEmailAddress(), - transaction_id: '', - products: [ - [ - 'name' => 'Floppy Disk (512k)', - 'sku' => '7890-ijkl', - 'pid' => 9999, - 'lid' => 7777, - 'quantity' => 2, - 'unit_price' => 5.00, - ], - ] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that create_purchase() returns a WP_Error when no products - * are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testCreatePurchaseWithNoProducts() - { - $result = $this->api->create_purchase( - email_address: $this->generateEmailAddress(), - transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), - products: [] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - - /** - * Test that get_segments() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSegments() - { - $result = $this->api->get_segments(); - - // Assert segments and pagination exist. - $this->assertDataExists($result, 'segments'); - $this->assertPaginationExists($result); - } - - /** - * Test that get_segments() returns the expected data - * when the total count is included. - * - * @since 1.0.0 - * - * @return void - */ - public function testGetSegmentsWithTotalCount() - { - $result = $this->api->get_segments( - include_total_count: true - ); - - // Assert segments and pagination exist. - $this->assertDataExists($result, 'segments'); - $this->assertPaginationExists($result); - - // Assert total count is included. - $this->assertArrayHasKey('total_count', $result['pagination']); - $this->assertGreaterThan(0, $result['pagination']['total_count']); - } - - /** - * Test that get_segments() returns the expected data - * when pagination parameters and per_page limits are specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetSegmentsPagination() - { - $result = $this->api->get_segments( - per_page: 1 - ); - - // Assert segments and pagination exist. - $this->assertDataExists($result, 'segments'); - $this->assertPaginationExists($result); - - // Assert a single segment was returned. - $this->assertCount(1, $result['segments']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch next page. - $result = $this->api->get_segments( - after_cursor: $result['pagination']['end_cursor'], - per_page: 1 - ); - - // Assert segments and pagination exist. - $this->assertDataExists($result, 'segments'); - $this->assertPaginationExists($result); - - // Assert a single segment was returned. - $this->assertCount(1, $result['segments']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertTrue($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - - // Use pagination to fetch previous page. - $result = $this->api->get_segments( - before_cursor: $result['pagination']['start_cursor'], - per_page: 1 - ); - - // Assert segments and pagination exist. - $this->assertDataExists($result, 'segments'); - $this->assertPaginationExists($result); - - // Assert a single segment was returned. - $this->assertCount(1, $result['segments']); - - // Assert has_previous_page and has_next_page are correct. - $this->assertFalse($result['pagination']['has_previous_page']); - $this->assertTrue($result['pagination']['has_next_page']); - } - /** * Test that the `recommendations_script()` function returns expected data * for a ConvertKit account that has the Creator Network enabled. @@ -6647,56 +2123,4 @@ public function mockRefreshTokenResponse( $response, $parsed_args, $url ) 'http_response' => null, ); } - - /** - * Helper method to assert the given key exists as an array - * in the API response. - * - * @since 2.0.0 - * - * @param array $result API Result. - * @param string $key Key. - */ - private function assertDataExists($result, $key) - { - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertArrayHasKey($key, $result); - $this->assertIsArray($result[ $key ]); - } - - /** - * Helper method to assert pagination object exists in response. - * - * @since 2.0.0 - * - * @param array $result API Result. - */ - private function assertPaginationExists($result) - { - $this->assertArrayHasKey('pagination', $result); - $pagination = $result['pagination']; - $this->assertArrayHasKey('has_previous_page', $pagination); - $this->assertArrayHasKey('has_next_page', $pagination); - $this->assertArrayHasKey('start_cursor', $pagination); - $this->assertArrayHasKey('end_cursor', $pagination); - $this->assertArrayHasKey('per_page', $pagination); - } - - /** - * Generates a unique email address for use in a test, comprising of a prefix, - * date + time and PHP version number. - * - * This ensures that if tests are run in parallel, the same email address - * isn't used for two tests across parallel testing runs. - * - * @since 2.0.0 - * - * @param string $domain Domain (default: kit.com). - * - * @return string - */ - private function generateEmailAddress($domain = 'kit.com') - { - return 'php-sdk-' . date('Y-m-d-H-i-s') . '-php-' . PHP_VERSION_ID . '@' . $domain; - } } diff --git a/tests/Integration/TestsTrait.php b/tests/Integration/TestsTrait.php new file mode 100644 index 0000000..15c1657 --- /dev/null +++ b/tests/Integration/TestsTrait.php @@ -0,0 +1,6987 @@ +api->get_account(); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $this->assertArrayHasKey('user', $result); + $this->assertArrayHasKey('account', $result); + + $account = get_object_vars($result['account']); + $this->assertArrayHasKey('id', $account); + $this->assertArrayHasKey('name', $account); + $this->assertArrayHasKey('plan_type', $account); + $this->assertArrayHasKey('primary_email_address', $account); + $this->assertArrayHasKey('created_at', $account); + $this->assertArrayHasKey('plan', $account); + $this->assertArrayHasKey('sending_addresses', $account); + $this->assertArrayHasKey('timezone', $account); + + $plan = get_object_vars($account['plan']); + $this->assertArrayHasKey('plan_type', $plan); + $this->assertArrayHasKey('interval', $plan); + $this->assertArrayHasKey('subscriber_limit', $plan); + $this->assertArrayHasKey('on_trial', $plan); + $this->assertArrayHasKey('trial_lapse_date', $plan); + $this->assertArrayHasKey('renews_at', $plan); + $this->assertArrayHasKey('cancels_at', $plan); + + $timezone = get_object_vars($account['timezone']); + $this->assertArrayHasKey('name', $timezone); + $this->assertArrayHasKey('friendly_name', $timezone); + $this->assertArrayHasKey('utc_offset', $timezone); + } + + /** + * Test that get_account_colors() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetAccountColors() + { + $result = $this->api->get_account_colors(); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $this->assertArrayHasKey('colors', $result); + $this->assertIsArray($result['colors']); + } + + /** + * Test that update_account_colors() updates the account's colors. + * + * @since 2.0.0 + * + * @return void + */ + public function testUpdateAccountColors() + { + $result = $this->api->update_account_colors([ + '#111111', + ]); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $this->assertArrayHasKey('colors', $result); + $this->assertIsArray($result['colors']); + $this->assertEquals($result['colors'][0], '#111111'); + } + + /** + * Test that get_creator_profile() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetCreatorProfile() + { + $result = $this->api->get_creator_profile(); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $profile = get_object_vars($result['profile']); + $this->assertArrayHasKey('name', $profile); + $this->assertArrayHasKey('byline', $profile); + $this->assertArrayHasKey('bio', $profile); + $this->assertArrayHasKey('image_url', $profile); + $this->assertArrayHasKey('profile_url', $profile); + } + + /** + * Test that get_email_stats() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetEmailStats() + { + $result = $this->api->get_email_stats(); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $stats = get_object_vars($result['stats']); + $this->assertArrayHasKey('sent', $stats); + $this->assertArrayHasKey('clicked', $stats); + $this->assertArrayHasKey('opened', $stats); + $this->assertArrayHasKey('email_stats_mode', $stats); + $this->assertArrayHasKey('open_tracking_enabled', $stats); + $this->assertArrayHasKey('click_tracking_enabled', $stats); + $this->assertArrayHasKey('starting', $stats); + $this->assertArrayHasKey('ending', $stats); + } + + /** + * Test that get_growth_stats() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetGrowthStats() + { + $result = $this->api->get_growth_stats(); + $this->assertInstanceOf('stdClass', $result); + + $result = get_object_vars($result); + $stats = get_object_vars($result['stats']); + $this->assertArrayHasKey('cancellations', $stats); + $this->assertArrayHasKey('net_new_subscribers', $stats); + $this->assertArrayHasKey('new_subscribers', $stats); + $this->assertArrayHasKey('subscribers', $stats); + $this->assertArrayHasKey('starting', $stats); + $this->assertArrayHasKey('ending', $stats); + } + + /** + * Test that get_growth_stats() returns the expected data + * when a start date is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetGrowthStatsWithStartDate() + { + // Define start and end dates. + $starting = new DateTime('now'); + $starting->modify('-7 days'); + $ending = new DateTime('now'); + + // Send request. + $result = $this->api->get_growth_stats( + starting: $starting + ); + $this->assertInstanceOf('stdClass', $result); + + // Confirm response object contains expected keys. + $result = get_object_vars($result); + $stats = get_object_vars($result['stats']); + $this->assertArrayHasKey('cancellations', $stats); + $this->assertArrayHasKey('net_new_subscribers', $stats); + $this->assertArrayHasKey('new_subscribers', $stats); + $this->assertArrayHasKey('subscribers', $stats); + $this->assertArrayHasKey('starting', $stats); + $this->assertArrayHasKey('ending', $stats); + + // Assert start and end dates were honored. + // Gets timezone offset for New York (-04:00 during DST, -05:00 otherwise). + $timezone = ( new DateTime() )->setTimezone(new DateTimeZone('America/New_York'))->format('P'); + $this->assertEquals($stats['starting'], $starting->format('Y-m-d') . 'T00:00:00' . $timezone); + $this->assertEquals($stats['ending'], $ending->format('Y-m-d') . 'T23:59:59' . $timezone); + } + + /** + * Test that get_growth_stats() returns the expected data + * when an end date is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetGrowthStatsWithEndDate() + { + // Define start and end dates. + $starting = new DateTime('now'); + $starting->modify('-90 days'); + $ending = new DateTime('now'); + $ending->modify('-7 days'); + + // Send request. + $result = $this->api->get_growth_stats( + ending: $ending + ); + $this->assertInstanceOf('stdClass', $result); + + // Confirm response object contains expected keys. + $result = get_object_vars($result); + $stats = get_object_vars($result['stats']); + $this->assertArrayHasKey('cancellations', $stats); + $this->assertArrayHasKey('net_new_subscribers', $stats); + $this->assertArrayHasKey('new_subscribers', $stats); + $this->assertArrayHasKey('subscribers', $stats); + $this->assertArrayHasKey('starting', $stats); + $this->assertArrayHasKey('ending', $stats); + + // Assert start and end dates were honored. + // Gets timezone offset for New York (-04:00 during DST, -05:00 otherwise). + $timezone = ( new DateTime() )->setTimezone(new DateTimeZone('America/New_York'))->format('P'); + $this->assertEquals($stats['starting'], $starting->format('Y-m-d') . 'T00:00:00' . $timezone); + $this->assertEquals($stats['ending'], $ending->format('Y-m-d') . 'T23:59:59' . $timezone); + } + + /** + * Test that get_forms() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetForms() + { + $result = $this->api->get_forms(); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Iterate through each form, confirming no landing pages were included. + foreach ($result->forms as $form) { + $form = get_object_vars($form); + + // Assert shape of object is valid. + $this->assertArrayHasKey('id', $form); + $this->assertArrayHasKey('name', $form); + $this->assertArrayHasKey('created_at', $form); + $this->assertArrayHasKey('type', $form); + $this->assertArrayHasKey('format', $form); + $this->assertArrayHasKey('embed_js', $form); + $this->assertArrayHasKey('embed_url', $form); + $this->assertArrayHasKey('archived', $form); + + // Assert form is not a landing page i.e embed. + $this->assertEquals($form['type'], 'embed'); + + // Assert form is not archived. + $this->assertFalse($form['archived']); + } + } + + /** + * Test that get_forms() returns the expected data when + * the status is set to archived. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormsWithArchivedStatus() + { + $result = $this->api->get_forms( + status: 'archived' + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Iterate through each form, confirming no landing pages were included. + foreach ($result->forms as $form) { + $form = get_object_vars($form); + + // Assert shape of object is valid. + $this->assertArrayHasKey('id', $form); + $this->assertArrayHasKey('name', $form); + $this->assertArrayHasKey('created_at', $form); + $this->assertArrayHasKey('type', $form); + $this->assertArrayHasKey('format', $form); + $this->assertArrayHasKey('embed_js', $form); + $this->assertArrayHasKey('embed_url', $form); + $this->assertArrayHasKey('archived', $form); + + // Assert form is not a landing page i.e embed. + $this->assertEquals($form['type'], 'embed'); + + // Assert form is not archived. + $this->assertTrue($form['archived']); + } + } + + /** + * Test that get_forms() returns the subscriber count + * when included in the `include` argument. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetFormsWithSubscriberCount() + { + $result = $this->api->get_forms( + include: ['subscriber_count'] + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert subscriber count is included. + $this->assertArrayHasKey('subscriber_count', get_object_vars($result->forms[0])); + } + + /** + * Test that get_forms() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormsWithTotalCount() + { + $result = $this->api->get_forms( + include_total_count: true + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_forms() returns the expected data when pagination parameters + * and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormsPagination() + { + $result = $this->api->get_forms( + per_page: 1 + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_forms( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_forms( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that get_landing_pages() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetLandingPages() + { + $result = $this->api->get_landing_pages(); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Iterate through each landing page, confirming no forms were included. + foreach ($result->forms as $form) { + $form = get_object_vars($form); + + // Assert shape of object is valid. + $this->assertArrayHasKey('id', $form); + $this->assertArrayHasKey('name', $form); + $this->assertArrayHasKey('created_at', $form); + $this->assertArrayHasKey('type', $form); + $this->assertArrayHasKey('format', $form); + $this->assertArrayHasKey('embed_js', $form); + $this->assertArrayHasKey('embed_url', $form); + $this->assertArrayHasKey('archived', $form); + + // Assert form is a landing page i.e. hosted. + $this->assertEquals($form['type'], 'hosted'); + + // Assert form is not archived. + $this->assertFalse($form['archived']); + } + } + + /** + * Test that get_landing_pages() returns the expected data when + * the status is set to archived. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetLandingPagesWithArchivedStatus() + { + $result = $this->api->get_forms( + status: 'archived' + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert no landing pages are returned, as the account doesn't have any archived landing pages. + $this->assertCount(0, $result->forms); + } + + /** + * Test that get_landing_pages() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetLandingPagesWithTotalCount() + { + $result = $this->api->get_landing_pages( + include_total_count: true + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_landing_pages() returns the expected data when pagination parameters + * and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetLandingPagesPagination() + { + $result = $this->api->get_landing_pages( + per_page: 1 + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_landing_pages( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_landing_pages( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'forms'); + $this->assertPaginationExists($result); + + // Assert a single form was returned. + $this->assertCount(1, $result->forms); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetFormSubscriptions() + { + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when the slim parameter is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetFormSubscriptionsWithSlimParameter() + { + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + slim: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Confirm custom field values are excluded from the data. + $subscriber = get_object_vars($result->subscribers[0]); + $this->assertArrayNotHasKey('fields', $subscriber); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithTotalCount() + { + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and the subscription status + * is cancelled. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithCancelledSubscriberState() + { + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_state: 'cancelled' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertEquals($result->subscribers[0]->state, 'cancelled'); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and the added_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithAddedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + added_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->added_at)) + ); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and the added_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithAddedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + added_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->added_at)) + ); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and the created_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithCreatedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + created_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and the created_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithCreatedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + created_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_form_subscriptions() returns the expected data + * when a valid Form ID is specified and pagination parameters + * and per_page limits are specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsPagination() + { + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_form_subscriptions() throws a ClientException when an invalid + * Form ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithInvalidFormID() + { + $this->assertApiError(function () { + return $this->api->get_form_subscriptions( + form_id: 12345 + ); + }); + } + + /** + * Test that get_form_subscriptions() throws a ClientException when an invalid + * subscriber state is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithInvalidSubscriberState() + { + $this->assertApiError(function () { + return $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_state: 'not-a-valid-state' + ); + }); + } + + /** + * Test that get_form_subscriptions() throws a ClientException when invalid + * pagination parameters are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetFormSubscriptionsWithInvalidPagination() + { + $this->assertApiError(function () { + return $this->api->get_form_subscriptions( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + after_cursor: 'not-a-valid-cursor' + ); + }); + } + + /** + * Test that get_sequences() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSequences() + { + $result = $this->api->get_sequences(); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Check first sequence in resultset has expected data. + $sequence = get_object_vars($result->sequences[0]); + $this->assertArrayHasKey('id', $sequence); + $this->assertArrayHasKey('name', $sequence); + $this->assertArrayHasKey('hold', $sequence); + $this->assertArrayHasKey('repeat', $sequence); + $this->assertArrayHasKey('created_at', $sequence); + } + + /** + * Test that get_sequences() returns the expected data + * when the include parameter is used. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetSequencesWithIncludeParam() + { + $result = $this->api->get_sequences( + include: ['stats'] + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Assert fields are included. + $this->assertArrayHasKey('stats', get_object_vars($result->sequences[0])); + } + + /** + * Test that get_sequences() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequencesWithTotalCount() + { + $result = $this->api->get_sequences( + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_sequences() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequencesPagination() + { + $result = $this->api->get_sequences( + per_page: 1 + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->sequences); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_sequences( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->sequences); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_sequences( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'sequences'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->sequences); + } + + /** + * Test that create_sequence(), update_sequence() and delete_sequence() works. + * + * We do all tests in a single function, so we don't end up with unnecessary + * Sequences remaining on the Kit account when running tests, which might impact + * other tests that expect (or do not expect) specific Sequences. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateUpdateAndDeleteSequence() + { + // Create a sequence. + $result = $this->api->create_sequence( + name: 'Test Sequence', + email_address: 'wordpress@convertkit.com', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + send_days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + send_hour: 12, + time_zone: 'America/Los_Angeles', + active: false, + repeat: false, + hold: false + ); + $sequenceID = $result->sequence->id; + + // Confirm the Sequence saved. + $result = get_object_vars($result->sequence); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Sequence', $result['name']); + $this->assertEquals('wordpress@convertkit.com', $result['email_address']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], $result['send_days']); + $this->assertEquals(12, $result['send_hour']); + $this->assertEquals('America/Los_Angeles', $result['time_zone']); + $this->assertEquals(false, $result['active']); + $this->assertEquals(false, $result['repeat']); + $this->assertEquals(false, $result['hold']); + + // Update the existing sequence. + $result = $this->api->update_sequence( + sequence_id: $sequenceID, + name: 'Edited Test Sequence', + email_address: 'wordpress@convertkit.com', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + send_days: ['saturday', 'sunday'], + send_hour: 13, + time_zone: 'America/New_York', + active: true, + repeat: true, + hold: true + ); + + // Confirm the changes saved. + $result = get_object_vars($result->sequence); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Sequence', $result['name']); + $this->assertEquals('wordpress@convertkit.com', $result['email_address']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(['saturday', 'sunday'], $result['send_days']); + $this->assertEquals(13, $result['send_hour']); + $this->assertEquals('America/New_York', $result['time_zone']); + $this->assertEquals(true, $result['active']); + $this->assertEquals(true, $result['repeat']); + $this->assertEquals(true, $result['hold']); + + // Delete Sequence. + $this->api->delete_sequence($sequenceID); + $this->assertLastResponseStatusCode(204); + } + + /** + * Test that get_sequence() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequence() + { + $result = $this->api->get_sequence((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID']); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('sequence', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->sequence)); + } + + /** + * Test that get_sequence() returns the expected data. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetSequenceWithIncludeParam() + { + $result = $this->api->get_sequence( + (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include: ['stats'] + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('sequence', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->sequence)); + + // Assert stats are included. + $this->assertArrayHasKey('stats', get_object_vars($result->sequence)); + } + + /** + * Test that update_sequence() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->update_sequence(12345); + }); + } + + /** + * Test that delete_sequence() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->delete_sequence(12345); + }); + } + + /** + * Test that add_subscriber_to_sequence_by_email() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToSequenceByEmail() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to sequence. + $result = $this->api->add_subscriber_to_sequence_by_email( + sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_address: $emailAddress + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals( + get_object_vars($result->subscriber)['email_address'], + $emailAddress + ); + } + + /** + * Test that add_subscriber_to_sequence_by_email() throws a ClientException when an invalid + * sequence is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToSequenceByEmailWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_sequence_by_email( + sequence_id: 12345, + email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] + ); + }); + } + + /** + * Test that add_subscriber_to_sequence_by_email() throws a ClientException when an invalid + * email address is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToSequenceByEmailWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_sequence_by_email( + sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that add_subscriber_to_sequence() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToSequence() + { + // Create subscriber. + $subscriber = $this->api->create_subscriber( + email_address: $this->generateEmailAddress() + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to sequence. + $result = $this->api->add_subscriber_to_sequence( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subscriber_id: $subscriber->subscriber->id + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals(get_object_vars($result->subscriber)['id'], $subscriber->subscriber->id); + } + + /** + * Test that add_subscriber_to_sequence() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToSequenceWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_sequence( + sequence_id: 12345, + subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] + ); + }); + } + + /** + * Test that add_subscriber_to_sequence() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToSequenceWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_sequence( + sequence_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], + subscriber_id: 12345 + ); + }); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptions() + { + $result = $this->api->get_sequence_subscriptions( + sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithTotalCount() + { + $result = $this->api->get_sequence_subscriptions( + sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and the subscription status + * is cancelled. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithCancelledSubscriberState() + { + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subscriber_state: 'cancelled' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertEquals($result->subscribers[0]->state, 'cancelled'); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and the added_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithAddedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + added_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->added_at)) + ); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and the added_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithAddedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + added_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->added_at)) + ); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and the created_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithCreatedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + created_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and the created_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithCreatedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + created_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_sequence_subscriptions() returns the expected data + * when a valid Sequence ID is specified and pagination parameters + * and per_page limits are specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsPagination() + { + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_sequence_subscriptions() throws a ClientException when an invalid + * Sequence ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->get_sequence_subscriptions( + sequence_id: 12345 + ); + }); + } + + /** + * Test that get_sequence_subscriptions() throws a ClientException when an invalid + * subscriber state is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithInvalidSubscriberState() + { + $this->assertApiError(function () { + return $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subscriber_state: 'not-a-valid-state' + ); + }); + } + + /** + * Test that get_sequence_subscriptions() throws a ClientException when invalid + * pagination parameters are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSequenceSubscriptionsWithInvalidPagination() + { + $this->assertApiError(function () { + return $this->api->get_sequence_subscriptions( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + after_cursor: 'not-a-valid-cursor' + ); + }); + } + + /** + * Test that get_sequence_emails() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmails() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'] + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Check first sequence in resultset has expected data. + $email = get_object_vars($result->emails[0]); + $this->assertArrayHasKey('id', $email); + $this->assertArrayHasKey('sequence_id', $email); + $this->assertArrayHasKey('subject', $email); + $this->assertArrayHasKey('preview_text', $email); + $this->assertArrayHasKey('email_address', $email); + $this->assertArrayHasKey('email_template_id', $email); + $this->assertArrayHasKey('published', $email); + $this->assertArrayHasKey('position', $email); + $this->assertArrayHasKey('delay_value', $email); + $this->assertArrayHasKey('delay_unit', $email); + $this->assertArrayHasKey('send_days', $email); + } + + /** + * Test that get_sequence_emails() returns the expected data + * when the include parameter is used. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetSequenceEmailsWithIncludeParam() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include: ['stats'] + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('emails', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->emails[0])); + + // Assert stats are included. + $this->assertArrayHasKey('stats', get_object_vars($result->emails[0])); + } + + /** + * Test that get_sequence_emails() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithTotalCount() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_sequence_emails() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsPagination() + { + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1 + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_sequence_emails( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert emails and pagination exist. + $this->assertDataExists($result, 'emails'); + $this->assertPaginationExists($result); + + // Assert a single email was returned. + $this->assertCount(1, $result->emails); + } + + /** + * Test that get_sequence_emails() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailsWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->get_sequence_emails( + sequence_id: 12345 + ); + }); + } + + /** + * Test that create_sequence_email(), get_sequence_email(), update_sequence_email() + * and delete_sequence_email() works. + * + * We do all tests in a single function, so we don't end up with unnecessary + * Sequence Emails remaining on the Kit account when running tests, which might impact + * other tests that expect (or do not expect) specific Sequence Emails. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateGetUpdateAndDeleteSequenceEmail() + { + // Create a sequence email. + $result = $this->api->create_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + subject: 'Test Sequence Email', + delay_value: 1, + delay_unit: 'days', + preview_text: 'Test Preview Text', + content: 'Test Content', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: true, + send_days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + position: 0 + ); + $sequenceEmailID = $result->email->id; + + // Confirm the Sequence Email saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Sequence Email', $result['subject']); + $this->assertEquals(1, $result['delay_value']); + $this->assertEquals('days', $result['delay_unit']); + $this->assertEquals('Test Preview Text', $result['preview_text']); + $this->assertEquals('Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(true, $result['published']); + $this->assertEquals(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], $result['send_days']); + $this->assertEquals(2, $result['position']); + + // Get the sequence email. + $result = $this->api->get_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID, + include: ['stats'] + ); + + // Update the existing sequence email. + $result = $this->api->update_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: $sequenceEmailID, + subject: 'Edited Test Sequence Email', + preview_text: 'Edited Test Preview Text', + content: 'Edited Test Content', + delay_value: 2, + delay_unit: 'hours', + email_template_id: (int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], + published: true, + send_days: ['saturday', 'sunday'], + position: 2, + ); + + // Confirm the changes saved. + $result = get_object_vars($result->email); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Sequence Email', $result['subject']); + $this->assertEquals(2, $result['delay_value']); + $this->assertEquals('hours', $result['delay_unit']); + $this->assertEquals('Edited Test Preview Text', $result['preview_text']); + $this->assertEquals('Edited Test Content', $result['content']); + $this->assertEquals((int) $_ENV['CONVERTKIT_API_EMAIL_TEMPLATE_ID'], $result['email_template_id']); + $this->assertEquals(true, $result['published']); + $this->assertEquals(['saturday', 'sunday'], $result['send_days']); + $this->assertEquals(2, $result['position']); + + // Delete Sequence Email. + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], $sequenceEmailID); + $this->assertLastResponseStatusCode(204); + } + + /** + * Test that get_sequence_email() returns the expected data. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetSequenceEmail() + { + $result = $this->api->get_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_EMAIL_ID'] + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('id', get_object_vars($result->email)); + } + + /** + * Test that get_sequence_email() returns the expected data + * when the include parameter is used. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetSequenceEmailWithIncludeParam() + { + $result = $this->api->get_sequence_email( + sequence_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], + email_id: (int) $_ENV['CONVERTKIT_API_SEQUENCE_EMAIL_ID'], + include: ['stats'] + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('id', get_object_vars($result->email)); + + // Assert stats are included. + $this->assertArrayHasKey('stats', get_object_vars($result->email)); + } + + /** + * Test that get_sequence_email() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->get_sequence_email(12345, 12345); + }); + } + + /** + * Test that get_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceEmailWithInvalidEmailID() + { + $this->assertApiError(function () { + return $this->api->get_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + }); + } + + /** + * Test that update_sequence_email() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->update_sequence_email(12345, 12345); + }); + } + + /** + * Test that update_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceEmailWithInvalidEmailID() + { + $this->assertApiError(function () { + return $this->api->update_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + }); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * sequence email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidSequenceID() + { + $this->assertApiError(function () { + return $this->api->delete_sequence_email(12345, 12345); + }); + } + + /** + * Test that delete_sequence_email() throws a ClientException when an invalid + * email ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testDeleteSequenceEmailWithInvalidEmailID() + { + $this->assertApiError(function () { + return $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], 12345); + }); + } + + /** + * Test that get_snippets() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippets() + { + $result = $this->api->get_snippets(); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Check first snippet in resultset has expected data. + $snippet = get_object_vars($result->snippets[0]); + $this->assertArrayHasKey('id', $snippet); + $this->assertArrayHasKey('name', $snippet); + $this->assertArrayHasKey('snippet_type', $snippet); + $this->assertArrayHasKey('archived', $snippet); + $this->assertArrayHasKey('key', $snippet); + $this->assertArrayHasKey('created_at', $snippet); + $this->assertArrayHasKey('updated_at', $snippet); + } + + /** + * Test that get_snippets() returns the expected data when + * the snippet type is inline. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetInlineSnippets() + { + $result = $this->api->get_snippets( + snippet_type: 'inline' + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert snippets were returned. + $this->assertGreaterThan(0, count($result->snippets)); + } + + /** + * Test that get_snippets() returns the expected data when + * the snippet type is block. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetBlockSnippets() + { + $result = $this->api->get_snippets( + snippet_type: 'block' + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert no snippets were returned. + $this->assertCount(0, $result->snippets); + } + + /** + * Test that get_snippets() returns the expected data when + * the archived parameter is used. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetsWithArchivedParam() + { + $result = $this->api->get_snippets( + archived: true + ); + + // Assert snippets and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert snippets were returned. + $this->assertGreaterThan(0, count($result->snippets)); + } + + /** + * Test that get_snippets() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetsWithTotalCount() + { + $result = $this->api->get_snippets( + include_total_count: true + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_snippets() returns the expected data when + * pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetsPagination() + { + $result = $this->api->get_snippets( + per_page: 1 + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_snippets( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_snippets( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'snippets'); + $this->assertPaginationExists($result); + + // Assert a single sequence was returned. + $this->assertCount(1, $result->snippets); + } + + // Note: testCreateSnippet lives on each consumer class (SDK / WP Libs). + // It depends on a platform-specific HTTP mock (Guzzle MockHandler in the + // SDK, pre_http_request filter in WP Libs), so it cannot live in this + // portable trait. + + /** + * Test that update_snippet() works. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSnippet() + { + $result = $this->api->update_snippet( + snippet_id: (int) $_ENV['CONVERTKIT_API_SNIPPET_ID'], + name: 'Edited Test Snippet', + snippet_type: 'inline', + content: 'Edited Test Content' + ); + + // Confirm the changes saved. + $result = get_object_vars($result->snippet); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Edited Test Snippet', $result['name']); + $this->assertEquals('inline', $result['snippet_type']); + $this->assertEquals('Edited Test Content', $result['content']); + } + + /** + * Test that get_snippet() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippet() + { + $result = $this->api->get_snippet((int) $_ENV['CONVERTKIT_API_SNIPPET_ID']); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('snippet', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->snippet)); + } + + /** + * Test that get_snippet() throws a ClientException when an invalid + * snippet ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSnippetWithInvalidSnippetID() + { + $this->assertApiError(function () { + return $this->api->get_snippet(12345); + }); + } + + /** + * Test that update_snippet() throws a ClientException when an invalid + * snippet ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSnippetWithInvalidSnippetID() + { + $this->assertApiError(function () { + return $this->api->update_snippet(12345); + }); + } + + /** + * Test that get_tags() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetTags() + { + $result = $this->api->get_tags(); + + // Assert sequences and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Check first tag in resultset has expected data. + $tag = get_object_vars($result->tags[0]); + $this->assertArrayHasKey('id', $tag); + $this->assertArrayHasKey('name', $tag); + $this->assertArrayHasKey('created_at', $tag); + } + + /** + * Test that get_tags() returns the subscriber count + * when included in the `include` argument. + * + * @since 2.6.0 + * + * @return void + */ + public function testGetTagsWithSubscriberCount() + { + $result = $this->api->get_tags( + include: ['subscriber_count'] + ); + + // Assert forms and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert subscriber count is included. + $this->assertArrayHasKey('subscriber_count', get_object_vars($result->tags[0])); + } + + /** + * Test that get_tags() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagsWithTotalCount() + { + $result = $this->api->get_tags( + include_total_count: true + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_tags() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagsPagination() + { + $result = $this->api->get_tags( + per_page: 1 + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert a single tag was returned. + $this->assertCount(1, $result->tags); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_tags( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->tags); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_tags( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + } + + // Note: testCreateTag lives on each consumer class (SDK / WP Libs). + // Same reason as testCreateSnippet — the create endpoint has no matching + // delete endpoint, so tests must mock the HTTP layer to avoid polluting + // the account, and the mock is platform-specific. + + /** + * Test that create_tag() throws a ClientException when creating + * a blank tag. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateTagBlank() + { + $this->assertApiError(function () { + return $this->api->create_tag(''); + }); + } + + /** + * Test that create_tag() returns the expected data when creating + * a tag that already exists. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateTagThatExists() + { + $result = $this->api->create_tag($_ENV['CONVERTKIT_API_TAG_NAME']); + + // Assert response contains correct data. + $tag = get_object_vars($result->tag); + $this->assertArrayHasKey('id', $tag); + $this->assertArrayHasKey('name', $tag); + $this->assertArrayHasKey('created_at', $tag); + $this->assertEquals($tag['name'], $_ENV['CONVERTKIT_API_TAG_NAME']); + } + + /** + * Test that create_tags() and delete_tags() returns the expected data. + * + * @since 1.1.0 + * + * @return void + */ + public function testCreateAndDeleteTags() + { + $tagNames = [ + 'Tag Test ' . mt_rand(), + 'Tag Test ' . mt_rand(), + ]; + + // Create tags. + $result = $this->api->create_tags($tagNames); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Build tag IDs array. + $ids = []; + foreach ($result->tags as $tag) { + $ids[] = $tag->id; + } + + // Delete tags. + $result = $this->api->delete_tags($ids); + + // Assert no failures. + $this->assertCount(0, $result->failures); + } + + /** + * Test that create_tags() returns failures when attempting + * to create blank tags. + * + * @since 1.1.0 + * + * @return void + */ + public function testCreateTagsBlank() + { + $result = $this->api->create_tags([ + '', + '', + ]); + + // Assert failures. + $this->assertCount(2, $result->failures); + } + + /** + * Test that create_tags() throws a ClientException when creating + * tags that already exists. + * + * @since 1.1.0 + * + * @return void + */ + public function testCreateTagsThatExist() + { + $result = $this->api->create_tags( + [ + $_ENV['CONVERTKIT_API_TAG_NAME'], + $_ENV['CONVERTKIT_API_TAG_NAME_2'], + ] + ); + + // Assert existing tags are returned. + $this->assertCount(2, $result->tags); + $this->assertEquals($result->tags[1]->name, $_ENV['CONVERTKIT_API_TAG_NAME']); + $this->assertEquals($result->tags[0]->name, $_ENV['CONVERTKIT_API_TAG_NAME_2']); + } + + /** + * Test that update_tag_name() returns the expected data. + * + * @since 2.2.1 + * + * @return void + */ + public function testUpdateTagName() + { + $result = $this->api->update_tag_name( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + name: $_ENV['CONVERTKIT_API_TAG_NAME'], + ); + + // Assert existing tag is returned. + $this->assertEquals($result->tag->id, (int) $_ENV['CONVERTKIT_API_TAG_ID']); + $this->assertEquals($result->tag->name, $_ENV['CONVERTKIT_API_TAG_NAME']); + } + + /** + * Test that update_tag_name() throws a ClientException when an invalid + * tag ID is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testUpdateTagNameWithInvalidTagID() + { + $this->assertApiError(function () { + return $this->api->update_tag_name( + tag_id: 12345, + name: $_ENV['CONVERTKIT_API_TAG_NAME'], + ); + }); + } + + /** + * Test that update_tag_name() throws a ClientException when a blank + * name is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testUpdateTagNameWithBlankName() + { + $this->assertApiError(function () { + return $this->api->update_tag_name( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + name: '' + ); + }); + } + + /** + * Test that get_subscriber_stats() returns the expected data + * when using a valid subscriber ID. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetSubscriberStats() + { + $result = $this->api->get_subscriber_stats( + id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] + ); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertArrayHasKey('stats', get_object_vars($result->subscriber)); + $this->assertArrayHasKey('sent', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('opened', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('clicked', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('bounced', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('open_rate', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('click_rate', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('last_sent', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('last_opened', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('last_clicked', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('sends_since_last_open', get_object_vars($result->subscriber->stats)); + $this->assertArrayHasKey('sends_since_last_click', get_object_vars($result->subscriber->stats)); + } + + /** + * Test that get_subscriber_stats() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetSubscriberStatsWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->get_subscriber_stats(12345); + }); + } + + /** + * Test that tag_subscribers() returns the expected data. + * + * @since 2.2.1 + * + * @return void + */ + public function testTagSubscribers() + { + // Create subscribers. + $subscribers = [ + [ + 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + foreach ($result->subscribers as $i => $subscriber) { + $this->subscriber_ids[] = $subscriber->id; + } + + // Tag subscribers. + $result = $this->api->tag_subscribers( + [ + [ + 'tag_id' => (int) $_ENV['CONVERTKIT_API_TAG_ID'], + 'subscriber_id' => $this->subscriber_ids[0] + ], + [ + 'tag_id' => (int) $_ENV['CONVERTKIT_API_TAG_ID'], + 'subscriber_id' => $this->subscriber_ids[1] + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each subscriber that was created. + $this->assertIsArray($result->subscribers); + $this->assertCount(2, $result->subscribers); + } + + /** + * Test that tag_subscribers() returns failures when an invalid + * tag ID is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testTagSubscribersWithInvalidTagID() + { + // Create subscribers. + $subscribers = [ + [ + 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + foreach ($result->subscribers as $i => $subscriber) { + $this->subscriber_ids[] = $subscriber->id; + } + + // Tag subscribers. + $result = $this->api->tag_subscribers( + [ + [ + 'tag_id' => 12345, + 'subscriber_id' => $this->subscriber_ids[0] + ], + [ + 'tag_id' => 12345, + 'subscriber_id' => $this->subscriber_ids[1] + ], + ] + ); + + // Assert failures. + $this->assertCount(2, $result->failures); + } + + /** + * Test that tag_subscribers() returns failures when an invalid + * subscriber ID is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testTagSubscribersWithInvalidSubscriberID() + { + // Tag subscribers that do not exist. + $result = $this->api->tag_subscribers( + [ + [ + 'tag_id' => (int) $_ENV['CONVERTKIT_API_TAG_ID'], + 'subscriber_id' => 12345, + ], + [ + 'tag_id' => (int) $_ENV['CONVERTKIT_API_TAG_ID'], + 'subscriber_id' => 67890, + ], + ] + ); + + // Assert failures. + $this->assertCount(2, $result->failures); + } + + /** + * Test that tag_subscriber_by_email() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testTagSubscriberByEmail() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Tag subscriber by email. + $subscriber = $this->api->tag_subscriber_by_email( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: $emailAddress, + ); + $this->assertArrayHasKey('subscriber', get_object_vars($subscriber)); + $this->assertArrayHasKey('id', get_object_vars($subscriber->subscriber)); + $this->assertArrayHasKey('tagged_at', get_object_vars($subscriber->subscriber)); + + // Confirm the subscriber is tagged. + $result = $this->api->get_subscriber_tags( + subscriber_id: $subscriber->subscriber->id + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert correct tag was assigned. + $this->assertEquals($result->tags[0]->id, $_ENV['CONVERTKIT_API_TAG_ID']); + } + + /** + * Test that tag_subscriber_by_email() throws a ClientException when an invalid + * tag is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testTagSubscriberByEmailWithInvalidTagID() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $this->api->create_subscriber( + email_address: $emailAddress + ); + + $this->assertApiError(function () { + return $this->api->tag_subscriber_by_email( + tag_id: 12345, + email_address: $emailAddress + ); + }); + } + + /** + * Test that tag_subscriber_by_email() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testTagSubscriberByEmailWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->tag_subscriber_by_email( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that tag_subscriber() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testTagSubscriber() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Tag subscriber by email. + $result = $this->api->tag_subscriber( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_id: $subscriber->subscriber->id, + ); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertArrayHasKey('tagged_at', get_object_vars($result->subscriber)); + + // Confirm the subscriber is tagged. + $result = $this->api->get_subscriber_tags( + subscriber_id: $result->subscriber->id + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert correct tag was assigned. + $this->assertEquals($result->tags[0]->id, $_ENV['CONVERTKIT_API_TAG_ID']); + } + + /** + * Test that tag_subscriber() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testTagSubscriberWithInvalidTagID() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + $this->assertApiError(function () { + return $this->api->tag_subscriber( + tag_id: 12345, + subscriber_id: $subscriber->subscriber->id + ); + }); + } + + /** + * Test that tag_subscriber() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testTagSubscriberWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->tag_subscriber( + tag_id: $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_id: 12345 + ); + }); + } + + /** + * Test that remove_tag_from_subscriber() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriber() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Tag subscriber by email. + $subscriber = $this->api->tag_subscriber_by_email( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: $emailAddress, + ); + + // Remove tag from subscriber. + $result = $this->api->remove_tag_from_subscriber( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_id: $subscriber->subscriber->id + ); + + // Confirm that the subscriber no longer has the tag. + $result = $this->api->get_subscriber_tags($subscriber->subscriber->id); + $this->assertIsArray($result->tags); + $this->assertCount(0, $result->tags); + } + + /** + * Test that remove_tag_from_subscriber() throws a ClientException when an invalid + * tag ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriberWithInvalidTagID() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Tag subscriber by email. + $subscriber = $this->api->tag_subscriber_by_email( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: $emailAddress, + ); + + // Remove tag from subscriber. + $this->assertApiError(function () { + return $this->api->remove_tag_from_subscriber( + tag_id: 12345, + subscriber_id: $subscriber->subscriber->id + ); + }); + } + + /** + * Test that remove_tag_from_subscriber() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriberWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->remove_tag_from_subscriber( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_id: 12345 + ); + }); + } + + /** + * Test that remove_tag_from_subscriber() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriberByEmail() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Tag subscriber by email. + $subscriber = $this->api->tag_subscriber_by_email( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: $emailAddress, + ); + + // Remove tag from subscriber. + $result = $this->api->remove_tag_from_subscriber( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_id: $subscriber->subscriber->id + ); + + // Confirm that the subscriber no longer has the tag. + $result = $this->api->get_subscriber_tags($subscriber->subscriber->id); + $this->assertIsArray($result->tags); + $this->assertCount(0, $result->tags); + } + + /** + * Test that remove_tag_from_subscriber() throws a ClientException when an invalid + * tag ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriberByEmailWithInvalidTagID() + { + $this->assertApiError(function () { + return $this->api->remove_tag_from_subscriber_by_email( + tag_id: 12345, + email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] + ); + }); + } + + /** + * Test that remove_tag_from_subscriber() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testRemoveTagFromSubscriberByEmailWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->remove_tag_from_subscriber_by_email( + tag_id: $_ENV['CONVERTKIT_API_TAG_ID'], + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetTagSubscriptions() + { + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when the slim parameter is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetTagSubscriptionsSlim() + { + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + slim: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Confirm custom field values are excluded from the data. + $broadcast = get_object_vars($result->subscribers[0]); + $this->assertArrayNotHasKey('fields', $broadcast); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithTotalCount() + { + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and the subscription status + * is cancelled. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithCancelledSubscriberState() + { + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + subscriber_state: 'cancelled' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertEquals($result->subscribers[0]->state, 'cancelled'); + } + + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and the added_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithTaggedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + tagged_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->tagged_at)) + ); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and the tagged_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithTaggedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + tagged_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->tagged_at)) + ); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and the created_after parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithCreatedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + created_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and the created_before parameter + * is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithCreatedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + created_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified and pagination parameters + * and per_page limits are specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsPagination() + { + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_tag_subscriptions( + tag_id: (int) $_ENV['CONVERTKIT_API_TAG_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_tag_subscriptions() returns the expected data + * when a valid Tag ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetTagSubscriptionsWithInvalidTagID() + { + $this->assertApiError(function () { + return $this->api->get_tag_subscriptions(12345); + }); + } + + /** + * Test that add_subscribers_to_forms() returns the expected data. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscribersToForms() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscribers to forms. + $result = $this->api->add_subscribers_to_forms( + forms_subscribers_ids: [ + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID'], + 'subscriber_id' => $subscriber->subscriber->id, + ], + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID_2'], + 'subscriber_id' => $subscriber->subscriber->id, + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each subscriber that was created. + $this->assertIsArray($result->subscribers); + } + + /** + * Test that add_subscribers_to_forms() returns the expected data + * when a referrer URL is specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscribersToFormsWithReferrer() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscribers to forms. + $result = $this->api->add_subscribers_to_forms( + forms_subscribers_ids: [ + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID'], + 'subscriber_id' => $subscriber->subscriber->id, + 'referrer' => 'https://mywebsite.com/bfpromo/', + ], + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID_2'], + 'subscriber_id' => $subscriber->subscriber->id, + 'referrer' => 'https://mywebsite.com/bfpromo/', + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each subscriber that was created. + $this->assertIsArray($result->subscribers); + + // Assert referrer data set for subscribers. + foreach ($result->subscribers as $subscriber) { + $this->assertEquals( + $subscriber->referrer, + 'https://mywebsite.com/bfpromo/' + ); + } + } + + /** + * Test that add_subscribers_to_forms() returns the expected data + * when a referrer URL with UTM parameters is specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscribersToFormsWithReferrerUTMParams() + { + // Define referrer. + $referrerUTMParams = [ + 'utm_source' => 'facebook', + 'utm_medium' => 'cpc', + 'utm_campaign' => 'black_friday', + 'utm_term' => 'car_owners', + 'utm_content' => 'get_10_off', + ]; + $referrer = 'https://mywebsite.com/bfpromo/?' . http_build_query($referrerUTMParams); + + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscribers to forms. + $result = $this->api->add_subscribers_to_forms( + forms_subscribers_ids: [ + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID'], + 'subscriber_id' => $subscriber->subscriber->id, + 'referrer' => $referrer, + ], + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID_2'], + 'subscriber_id' => $subscriber->subscriber->id, + 'referrer' => $referrer, + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each subscriber that was created. + $this->assertIsArray($result->subscribers); + + // Assert referrer data set for subscribers. + foreach ($result->subscribers as $subscriber) { + $this->assertEquals( + $subscriber->referrer, + $referrer + ); + $this->assertEquals( + $subscriber->referrer_utm_parameters->source, + $referrerUTMParams['utm_source'] + ); + $this->assertEquals( + $subscriber->referrer_utm_parameters->medium, + $referrerUTMParams['utm_medium'] + ); + $this->assertEquals( + $subscriber->referrer_utm_parameters->campaign, + $referrerUTMParams['utm_campaign'] + ); + $this->assertEquals( + $subscriber->referrer_utm_parameters->term, + $referrerUTMParams['utm_term'] + ); + $this->assertEquals( + $subscriber->referrer_utm_parameters->content, + $referrerUTMParams['utm_content'] + ); + } + } + + /** + * Test that add_subscribers_to_forms() returns the expected errors + * when invalid Form IDs are specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscribersToFormsWithInvalidFormIDs() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscribers to forms. + $result = $this->api->add_subscribers_to_forms( + forms_subscribers_ids: [ + [ + 'form_id' => 9999999, + 'subscriber_id' => $subscriber->subscriber->id, + ], + [ + 'form_id' => 9999999, + 'subscriber_id' => $subscriber->subscriber->id, + ], + ] + ); + + // Assert failures. + $this->assertCount(2, $result->failures); + foreach ($result->failures as $failure) { + $this->assertEquals( + $failure->errors[0], + 'Form does not exist' + ); + } + } + + /** + * Test that add_subscribers_to_forms() returns the expected errors + * when invalid Subscriber IDs are specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscribersToFormsWithInvalidSubscriberIDs() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscribers to forms. + $result = $this->api->add_subscribers_to_forms( + forms_subscribers_ids: [ + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID'], + 'subscriber_id' => 999999, + ], + [ + 'form_id' => (int) $_ENV['CONVERTKIT_API_FORM_ID_2'], + 'subscriber_id' => 999999, + ], + ] + ); + + // Assert failures. + $this->assertCount(2, $result->failures); + foreach ($result->failures as $failure) { + $this->assertEquals( + $failure->errors[0], + 'Subscriber does not exist' + ); + } + } + + /** + * Test that add_subscriber_to_form_by_email() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToFormByEmail() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to form. + $result = $this->api->add_subscriber_to_form_by_email( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + email_address: $emailAddress + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals( + get_object_vars($result->subscriber)['email_address'], + $emailAddress + ); + } + + /** + * Test that add_subscriber_to_form_by_email() returns the expected data + * when a referrer is specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscriberToFormByEmailWithReferrer() + { + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress, + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to form. + $result = $this->api->add_subscriber_to_form_by_email( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + email_address: $emailAddress, + referrer: 'https://mywebsite.com/bfpromo/', + ); + + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals( + get_object_vars($result->subscriber)['email_address'], + $emailAddress + ); + + // Assert referrer data set for form subscriber. + $this->assertEquals( + $result->subscriber->referrer, + 'https://mywebsite.com/bfpromo/' + ); + } + + /** + * Test that add_subscriber_to_form_by_email() returns the expected data + * when a referrer is specified that includes UTM parameters. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscriberToFormByEmailWithReferrerUTMParams() + { + // Define referrer. + $referrerUTMParams = [ + 'utm_source' => 'facebook', + 'utm_medium' => 'cpc', + 'utm_campaign' => 'black_friday', + 'utm_term' => 'car_owners', + 'utm_content' => 'get_10_off', + ]; + $referrer = 'https://mywebsite.com/bfpromo/?' . http_build_query($referrerUTMParams); + + // Create subscriber. + $emailAddress = $this->generateEmailAddress(); + $subscriber = $this->api->create_subscriber( + email_address: $emailAddress, + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to form. + $result = $this->api->add_subscriber_to_form_by_email( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + email_address: $emailAddress, + referrer: $referrer, + ); + + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals( + get_object_vars($result->subscriber)['email_address'], + $emailAddress + ); + + // Assert referrer data set for form subscriber. + $this->assertEquals( + $result->subscriber->referrer, + $referrer + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->source, + $referrerUTMParams['utm_source'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->medium, + $referrerUTMParams['utm_medium'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->campaign, + $referrerUTMParams['utm_campaign'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->term, + $referrerUTMParams['utm_term'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->content, + $referrerUTMParams['utm_content'] + ); + } + + /** + * Test that add_subscriber_to_form_by_email() throws a ClientException when an invalid + * form ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToFormByEmailWithInvalidFormID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_form_by_email( + form_id: 12345, + email_address: $this->generateEmailAddress() + ); + }); + } + + /** + * Test that add_subscriber_to_form() throws a ClientException when an invalid + * email address is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testAddSubscriberToFormByEmailWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_form_by_email( + form_id: $_ENV['CONVERTKIT_API_FORM_ID'], + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that add_subscriber_to_form() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToForm() + { + // Create subscriber. + $subscriber = $this->api->create_subscriber( + email_address: $this->generateEmailAddress() + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + $result = $this->api->add_subscriber_to_form( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: $subscriber->subscriber->id + ); + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals(get_object_vars($result->subscriber)['id'], $subscriber->subscriber->id); + } + + /** + * Test that add_subscriber_to_form() returns the expected data + * when a referrer is specified. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscriberToFormWithReferrer() + { + // Create subscriber. + $subscriber = $this->api->create_subscriber( + email_address: $this->generateEmailAddress() + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to form. + $result = $this->api->add_subscriber_to_form( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: $subscriber->subscriber->id, + referrer: 'https://mywebsite.com/bfpromo/', + ); + + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals(get_object_vars($result->subscriber)['id'], $subscriber->subscriber->id); + + // Assert referrer data set for form subscriber. + $this->assertEquals( + $result->subscriber->referrer, + 'https://mywebsite.com/bfpromo/' + ); + } + + /** + * Test that add_subscriber_to_form() returns the expected data + * when a referrer is specified that includes UTM parameters. + * + * @since 2.1.0 + * + * @return void + */ + public function testAddSubscriberToFormWithReferrerUTMParams() + { + // Define referrer. + $referrerUTMParams = [ + 'utm_source' => 'facebook', + 'utm_medium' => 'cpc', + 'utm_campaign' => 'black_friday', + 'utm_term' => 'car_owners', + 'utm_content' => 'get_10_off', + ]; + $referrer = 'https://mywebsite.com/bfpromo/?' . http_build_query($referrerUTMParams); + + // Create subscriber. + $subscriber = $this->api->create_subscriber( + email_address: $this->generateEmailAddress() + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $subscriber->subscriber->id; + + // Add subscriber to form. + $result = $this->api->add_subscriber_to_form( + form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: $subscriber->subscriber->id, + referrer: $referrer, + ); + + $this->assertInstanceOf('stdClass', $result); + $this->assertArrayHasKey('subscriber', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->subscriber)); + $this->assertEquals(get_object_vars($result->subscriber)['id'], $subscriber->subscriber->id); + + // Assert referrer data set for form subscriber. + $this->assertEquals( + $result->subscriber->referrer, + $referrer + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->source, + $referrerUTMParams['utm_source'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->medium, + $referrerUTMParams['utm_medium'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->campaign, + $referrerUTMParams['utm_campaign'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->term, + $referrerUTMParams['utm_term'] + ); + $this->assertEquals( + $result->subscriber->referrer_utm_parameters->content, + $referrerUTMParams['utm_content'] + ); + } + + /** + * Test that add_subscriber_to_form() throws a ClientException when an invalid + * form ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToFormWithInvalidFormID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_form( + form_id: 12345, + subscriber_id: $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'] + ); + }); + } + + /** + * Test that add_subscriber_to_form() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testAddSubscriberToFormWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->add_subscriber_to_form( + form_id: $_ENV['CONVERTKIT_API_FORM_ID'], + subscriber_id: 12345 + ); + }); + } + + /** + * Test that get_subscribers() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribers() + { + $result = $this->api->get_subscribers(); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_subscribers() returns the expected data + * when the slim parameter is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetSubscribersWithSlimParameter() + { + $result = $this->api->get_subscribers( + slim: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Confirm custom field values are excluded from the data. + $subscriber = get_object_vars($result->subscribers[0]); + $this->assertArrayNotHasKey('fields', $subscriber); + } + + /** + * Test that get_subscribers() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithTotalCount() + { + $result = $this->api->get_subscribers( + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_subscribers() returns the expected data when + * searching by an email address. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersByEmailAddress() + { + $result = $this->api->get_subscribers( + email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert correct subscriber returned. + $this->assertEquals( + $result->subscribers[0]->email_address, + $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] + ); + } + + /** + * Test that get_subscribers() returns the expected data + * when the subscription status is cancelled. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscribersWithCancelledSubscriberState() + { + $result = $this->api->get_subscribers( + subscriber_state: 'cancelled' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertEquals($result->subscribers[0]->state, 'cancelled'); + } + + /** + * Test that get_subscribers() returns the expected data + * when the created_after parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithCreatedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_subscribers( + created_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertGreaterThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_subscribers() returns the expected data + * when the created_before parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithCreatedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_subscribers( + created_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertLessThanOrEqual( + $date->format('Y-m-d'), + date('Y-m-d', strtotime($result->subscribers[0]->created_at)) + ); + } + + /** + * Test that get_subscribers() returns the expected data + * when the updated_after parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithUpdatedAfterParam() + { + $date = new \DateTime('2022-01-01'); + $result = $this->api->get_subscribers( + updated_after: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_subscribers() returns the expected data + * when the updated_before parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithUpdatedBeforeParam() + { + $date = new \DateTime('2024-01-01'); + $result = $this->api->get_subscribers( + updated_before: $date + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_subscribers() returns the expected data + * when the sort_field parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithSortFieldParam() + { + $result = $this->api->get_subscribers( + sort_field: 'id' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert sorting is honored by ID in descending (default) order. + $this->assertLessThanOrEqual( + $result->subscribers[0]->id, + $result->subscribers[1]->id + ); + } + + /** + * Test that get_subscribers() returns the expected data + * when the sort_order parameter is used. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithSortOrderParam() + { + $result = $this->api->get_subscribers( + sort_order: 'asc' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert sorting is honored by ID (default) in ascending order. + $this->assertGreaterThanOrEqual( + $result->subscribers[0]->id, + $result->subscribers[1]->id + ); + } + + /** + * Test that get_subscribers() returns the expected data + * when the include parameter is used. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSubscribersWithIncludeParam() + { + $result = $this->api->get_subscribers( + include: ['tags'] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert fields are included. + $this->assertArrayHasKey('tags', get_object_vars($result->subscribers[0])); + } + + /** + * Test that get_subscribers() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersPagination() + { + $result = $this->api->get_subscribers( + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_subscribers( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_subscribers( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->get_subscribers( + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * subscriber state is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidSubscriberState() + { + $this->assertApiError(function () { + return $this->api->get_subscribers( + subscriber_state: 'not-an-valid-state' + ); + }); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * sort field is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidSortFieldParam() + { + $this->assertApiError(function () { + return $this->api->get_subscribers( + sort_field: 'not-a-valid-sort-field' + ); + }); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * sort order is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidSortOrderParam() + { + $this->assertApiError(function () { + return $this->api->get_subscribers( + sort_order: 'not-a-valid-sort-order' + ); + }); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * pagination parameters are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidPagination() + { + $this->assertApiError(function () { + return $this->api->get_subscribers( + after_cursor: 'not-a-valid-cursor' + ); + }); + } + + /** + * Test that create_subscriber() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriber() + { + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + } + + /** + * Test that create_subscriber() returns the expected data + * when a first name is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithFirstName() + { + $firstName = 'FirstName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress, + first_name: $firstName + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->first_name, $firstName); + } + + /** + * Test that create_subscriber() returns the expected data + * when a subscriber state is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithSubscriberState() + { + $subscriberState = 'cancelled'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress, + subscriber_state: $subscriberState + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->state, $subscriberState); + } + + /** + * Test that create_subscriber() returns the expected data + * when custom field data is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithCustomFields() + { + $lastName = 'LastName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress, + fields: [ + 'last_name' => $lastName + ] + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->fields->last_name, $lastName); + } + + /** + * Test that create_subscriber() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->create_subscriber( + email_address: 'not-an-email-address' + ); + }); + } + + /** + * Test that create_subscriber() throws a ClientException when an invalid + * subscriber state is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithInvalidSubscriberState() + { + $emailAddress = $this->generateEmailAddress(); + $this->assertApiError(function () use ($emailAddress) { + return $this->api->create_subscriber( + email_address: $emailAddress, + subscriber_state: 'not-a-valid-state' + ); + }); + } + + /** + * Test that create_subscriber() returns the expected warnings + * when an invalid custom field is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithInvalidCustomFields() + { + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress, + fields: [ + 'not_a_custom_field' => 'value' + ] + ); + $this->assertArrayHasKey('warnings', get_object_vars($result)); + } + + /** + * Test that create_subscribers() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscribers() + { + $subscribers = [ + [ + 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + foreach ($result->subscribers as $i => $subscriber) { + $this->subscriber_ids[] = $subscriber->id; + } + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Assert subscribers exists with correct data. + foreach ($result->subscribers as $i => $subscriber) { + $this->assertEquals($subscriber->email_address, $subscribers[$i]['email_address']); + } + } + + /** + * Test that create_subscribers() throws a ClientException when no data is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscribersWithBlankData() + { + $this->assertApiError(function () { + return $this->api->create_subscribers([ + [], + ]); + }); + } + + /** + * Test that create_subscribers() returns the expected data when invalid email addresses + * are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscribersWithInvalidEmailAddresses() + { + $subscribers = [ + [ + 'email_address' => 'not-an-email-address', + ], + [ + 'email_address' => 'not-an-email-address-again', + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Assert no subscribers were added. + $this->assertCount(0, $result->subscribers); + $this->assertCount(2, $result->failures); + } + + /** + * Test that filter_subscribers() returns the expected data. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribers() + { + $result = $this->api->filter_subscribers( + [ + [ + 'type' => 'opens', + 'count_greater_than' => 10, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + 'states' => [ + 'active', + ], + ] + ] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when multiple all conditions are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithMultipleConditions() + { + $result = $this->api->filter_subscribers( + [ + [ + 'type' => 'opens', + 'count_greater_than' => 10, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + 'states' => [ + 'active', + ], + ], + [ + 'type' => 'clicks', + 'count_greater_than' => 1, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + ] + ] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when a counting mode is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testFilterSubscribersWithCountingMode() + { + $result = $this->api->filter_subscribers( + all: [ + [ + 'type' => 'opens', + 'count_greater_than' => 10, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2027-01-01'), + 'states' => [ + 'active', + ], + ] + ], + counting_mode: 'unique_email' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() returns the expected data + * when the `include` parameter is specified. + * + * @since 2.6.0 + * + * @return void + */ + public function testFilterSubscribersWithInclude() + { + $result = $this->api->filter_subscribers( + all: [ + [ + 'type' => 'opens', + 'count_greater_than' => 0, + 'count_less_than' => 100, + 'after' => new \DateTime('2024-01-01'), + 'before' => new \DateTime('2029-01-01'), + 'states' => [ + 'active', + ], + ] + ], + include: [ + [ + 'type' => 'tags', + ], + ] + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert tags are included. + $this->assertArrayHasKey('tags', get_object_vars($result->subscribers[0])); + } + + /** + * Test that filter_subscribers() returns the expected data + * when no parameters are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithNoParameters() + { + $result = $this->api->filter_subscribers(); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + } + + /** + * Test that filter_subscribers() throws a ServerException + * when invalid parameters are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithInvalidParameters() + { + $this->assertApiError(function () { + return $this->api->filter_subscribers( + [ + [ + 'foo' => 'bar', + ], + [ + 'type' => 'not-a-real-type', + ] + ] + ); + }); + } + + /** + * Test that filter_subscribers() returns the expected data + * when the total count is included. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersWithTotalCount() + { + $result = $this->api->filter_subscribers( + include_total_count: true + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that filter_subscribers() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.4.0 + * + * @return void + */ + public function testFilterSubscribersPagination() + { + $result = $this->api->filter_subscribers( + per_page: 1 + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->filter_subscribers( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->filter_subscribers( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Assert a single subscriber was returned. + $this->assertCount(1, $result->subscribers); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + } + + /** + * Test that get_subscriber_id() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberID() + { + $subscriber_id = $this->api->get_subscriber_id($_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); + $this->assertIsInt($subscriber_id); + $this->assertEquals($subscriber_id, (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + } + + /** + * Test that get_subscriber_id() throws a ClientException when an invalid + * email address is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberIDWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->get_subscriber_id('not-an-email-address'); + }); + } + + /** + * Test that get_subscriber_id() return false when no subscriber found + * matching the given email address. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberIDWithNotSubscribedEmailAddress() + { + $result = $this->api->get_subscriber_id('not-a-subscriber@test.com'); + $this->assertFalse($result); + } + + /** + * Test that get_subscriber() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriber() + { + $result = $this->api->get_subscriber((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->id, $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + $this->assertEquals($result->subscriber->email_address, $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); + } + + /** + * Test that get_subscriber() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->get_subscriber(12345); + }); + } + + /** + * Test that update_subscriber() works when no changes are made. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateSubscriberWithNoChanges() + { + $result = $this->api->update_subscriber($_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->id, $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + $this->assertEquals($result->subscriber->email_address, $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']); + } + + /** + * Test that update_subscriber() works when updating the subscriber's first name. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateSubscriberFirstName() + { + // Add a subscriber. + $firstName = 'FirstName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber created with no first name. + $this->assertNull($result->subscriber->first_name); + + // Get subscriber ID. + $subscriberID = $result->subscriber->id; + + // Update subscriber's first name. + $result = $this->api->update_subscriber( + subscriber_id: $subscriberID, + first_name: $firstName + ); + + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->first_name, $firstName); + } + + /** + * Test that update_subscriber() works when updating the subscriber's email address. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateSubscriberEmailAddress() + { + // Add a subscriber. + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber created. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + + // Get subscriber ID. + $subscriberID = $result->subscriber->id; + + // Update subscriber's email address. + $newEmail = $this->generateEmailAddress(); + $result = $this->api->update_subscriber( + subscriber_id: $subscriberID, + email_address: $newEmail + ); + + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->email_address, $newEmail); + } + + /** + * Test that update_subscriber() works when updating the subscriber's custom fields. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateSubscriberCustomFields() + { + // Add a subscriber. + $lastName = 'LastName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + $this->subscriber_ids[] = $result->subscriber->id; + + // Assert subscriber created. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + + // Get subscriber ID. + $subscriberID = $result->subscriber->id; + + // Update subscriber's custom fields. + $result = $this->api->update_subscriber( + subscriber_id: $subscriberID, + fields: [ + 'last_name' => $lastName, + ] + ); + + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->fields->last_name, $lastName); + } + + /** + * Test that update_subscriber() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateSubscriberWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->update_subscriber(12345); + }); + } + + /** + * Test that unsubscribe_by_email() works with a valid subscriber email address. + * + * @since 1.0.0 + * + * @return void + */ + public function testUnsubscribeByEmail() + { + // Add a subscriber. + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Wait a moment to ensure subscriber is created. + sleep(3); + + // Unsubscribe. + $this->assertNull($this->api->unsubscribe_by_email($emailAddress)); + } + + /** + * Test that unsubscribe_by_email() throws a ClientException when an email + * address is specified that is not subscribed. + * + * @since 1.0.0 + * + * @return void + */ + public function testUnsubscribeByEmailWithNotSubscribedEmailAddress() + { + $this->assertApiError(function () { + return $this->api->unsubscribe_by_email('not-subscribed@kit.com'); + }); + } + + /** + * Test that unsubscribe_by_email() throws a ClientException when an invalid + * email address is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testUnsubscribeByEmailWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->unsubscribe_by_email('invalid-email'); + }); + } + + /** + * Test that unsubscribe() works with a valid subscriber ID. + * + * @since 2.0.0 + * + * @return void + */ + public function testUnsubscribe() + { + // Add a subscriber. + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Wait a moment to ensure subscriber is created. + sleep(3); + + // Unsubscribe. + $this->assertNull($this->api->unsubscribe($result->subscriber->id)); + } + + /** + * Test that unsubscribe() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testUnsubscribeWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->unsubscribe(12345); + }); + } + + /** + * Test that get_subscriber_tags() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberTags() + { + $result = $this->api->get_subscriber_tags((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_subscriber_tags() returns the expected data + * when the total count is included. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscriberTagsWithTotalCount() + { + $result = $this->api->get_subscriber_tags( + subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], + include_total_count: true + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_subscriber_tags() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscriberTagsWithInvalidSubscriberID() + { + $this->assertApiError(function () { + return $this->api->get_subscriber_tags(12345); + }); + } + + /** + * Test that get_subscriber_tags() returns the expected data + * when a valid Subscriber ID is specified and pagination parameters + * and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscriberTagsPagination() + { + $result = $this->api->get_subscriber_tags( + subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], + per_page: 1 + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert a single tag was returned. + $this->assertCount(1, $result->tags); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_subscriber_tags( + subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert a single tag was returned. + $this->assertCount(1, $result->tags); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_subscriber_tags( + subscriber_id: (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); + + // Assert a single tag was returned. + $this->assertCount(1, $result->tags); + } + + /** + * Test that get_email_templates() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetEmailTemplates() + { + $result = $this->api->get_email_templates(); + + // Assert email templates and pagination exist. + $this->assertDataExists($result, 'email_templates'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_email_templates() returns the expected data + * when the total count is included. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetEmailTemplatesWithTotalCount() + { + $result = $this->api->get_email_templates( + include_total_count: true + ); + + // Assert email templates and pagination exist. + $this->assertDataExists($result, 'email_templates'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_email_templates() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetEmailTemplatesPagination() + { + $result = $this->api->get_email_templates( + per_page: 1 + ); + + // Assert email templates and pagination exist. + $this->assertDataExists($result, 'email_templates'); + $this->assertPaginationExists($result); + + // Assert a single email template was returned. + $this->assertCount(1, $result->email_templates); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_email_templates( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert email templates and pagination exist. + $this->assertDataExists($result, 'email_templates'); + $this->assertPaginationExists($result); + + // Assert a single email template was returned. + $this->assertCount(1, $result->email_templates); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_email_templates( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert email templates and pagination exist. + $this->assertDataExists($result, 'email_templates'); + $this->assertPaginationExists($result); + + // Assert a single email template was returned. + $this->assertCount(1, $result->email_templates); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that get_posts() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPosts() + { + $result = $this->api->get_posts(); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert content is not included. + $this->assertArrayNotHasKey('content', get_object_vars($result->posts[0])); + } + + /** + * Test that get_posts() returns the expected data + * when the post content is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsWithIncludeContent() + { + $result = $this->api->get_posts( + include_content: true, + per_page: 1 + ); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert content is included. + $this->assertArrayHasKey('content', get_object_vars($result->posts[0])); + } + + /** + * Test that get_posts() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsWithTotalCount() + { + $result = $this->api->get_posts( + include_total_count: true + ); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_posts() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsPagination() + { + $result = $this->api->get_posts( + per_page: 1 + ); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert a single post was returned. + $this->assertCount(1, $result->posts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_posts( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert a single post was returned. + $this->assertCount(1, $result->posts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_posts( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert posts and pagination exist. + $this->assertDataExists($result, 'posts'); + $this->assertPaginationExists($result); + + // Assert a single post was returned. + $this->assertCount(1, $result->posts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that get_post() returns the expected data. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPost() + { + $result = $this->api->get_post($_ENV['CONVERTKIT_API_POST_ID']); + $result = get_object_vars($result->post); + $this->assertEquals($result['id'], $_ENV['CONVERTKIT_API_POST_ID']); + } + + /** + * Test that get_post() throws a ClientException when an invalid + * post ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostWithInvalidPostID() + { + $this->assertApiError(function () { + return $this->api->get_post(12345); + }); + } + + /** + * Test that get_broadcasts() returns the expected data + * when a valid sent_after date is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsWithSentAfter() + { + $date = new DateTime('now'); + $date->modify('-4 years'); + $result = $this->api->get_broadcasts( + sent_after: $date, + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(4, $result->broadcasts); + } + + /** + * Test that get_broadcasts() returns no broadcasts + * when a sent_after date is specified that is after all broadcasts. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsWithSentAfterNow() + { + $date = new DateTime('now'); + $date->modify('-1 day'); + $result = $this->api->get_broadcasts( + sent_after: $date, + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert no broadcasts were returned. + $this->assertCount(0, $result->broadcasts); + } + + /** + * Test that get_broadcasts() returns the expected data + * when a valid sent_before date is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsWithSentBefore() + { + $date = new DateTime('now'); + $result = $this->api->get_broadcasts( + sent_before: new DateTime('now'), + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(12, $result->broadcasts); + } + + /** + * Test that get_broadcasts() returns the expected data + * when the slim parameter is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsSlim() + { + $result = $this->api->get_broadcasts( + slim: true + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Confirm content, public_url, email_address, email_template and subscriber_filter are excluded from the data. + $broadcast = get_object_vars($result->broadcasts[0]); + $this->assertArrayNotHasKey('content', $broadcast); + $this->assertArrayNotHasKey('public_url', $broadcast); + $this->assertArrayNotHasKey('email_address', $broadcast); + $this->assertArrayNotHasKey('email_template', $broadcast); + $this->assertArrayNotHasKey('subscriber_filter', $broadcast); + } + + /** + * Test that get_broadcasts() returns the expected data + * when the completed status is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsWithCompletedStatus() + { + $result = $this->api->get_broadcasts( + status: 'completed' + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + } + + /** + * Test that get_broadcasts() returns the expected data + * when the aborted status is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsWithAbortedStatus() + { + $result = $this->api->get_broadcasts( + status: 'aborted' + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert no broadcasts were returned. + $this->assertCount(0, $result->broadcasts); + } + + /** + * Test that get_broadcasts() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetBroadcastsPagination() + { + $result = $this->api->get_broadcasts( + per_page: 1 + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single broadcast was returned. + $this->assertCount(1, $result->broadcasts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_broadcasts( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single broadcast was returned. + $this->assertCount(1, $result->broadcasts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_broadcasts( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single broadcast was returned. + $this->assertCount(1, $result->broadcasts); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that create_broadcast(), update_broadcast() and delete_broadcast() works + * when specifying valid published_at and send_at values. + * + * We do all tests in a single function, so we don't end up with unnecessary Broadcasts remaining + * on the ConvertKit account when running tests, which might impact + * other tests that expect (or do not expect) specific Broadcasts. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateAndUpdateDraftBroadcast() + { + // Create a broadcast first. + $result = $this->api->create_broadcast( + subject: 'Test Subject', + content: 'Test Content', + description: 'Test Broadcast from PHP SDK', + ); + $broadcastID = $result->broadcast->id; + + // Confirm the Broadcast saved. + $result = get_object_vars($result->broadcast); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Subject', $result['subject']); + $this->assertEquals('Test Content', $result['content']); + $this->assertEquals('Test Broadcast from PHP SDK', $result['description']); + $this->assertEquals(null, $result['published_at']); + $this->assertEquals(null, $result['send_at']); + + // Update the existing broadcast. + $result = $this->api->update_broadcast( + id: $broadcastID, + subject: 'New Test Subject', + content: 'New Test Content', + description: 'New Test Broadcast from PHP SDK' + ); + + // Confirm the changes saved. + $result = get_object_vars($result->broadcast); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('New Test Subject', $result['subject']); + $this->assertEquals('New Test Content', $result['content']); + $this->assertEquals('New Test Broadcast from PHP SDK', $result['description']); + $this->assertEquals(null, $result['published_at']); + $this->assertEquals(null, $result['send_at']); + + // Delete Broadcast. + $this->api->delete_broadcast($broadcastID); + $this->assertLastResponseStatusCode(204); + } + + /** + * Test that create_broadcast() works when specifying valid published_at and send_at values. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreatePublicBroadcastWithValidDates() + { + // Create DateTime object. + $publishedAt = new DateTime('now'); + $publishedAt->modify('+7 days'); + $sendAt = new DateTime('now'); + $sendAt->modify('+14 days'); + + // Create broadcast first. + $result = $this->api->create_broadcast( + subject: 'Test Subject', + content: 'Test Content', + description: 'Test Broadcast from PHP SDK', + public: true, + published_at: $publishedAt, + send_at: $sendAt + ); + $broadcastID = $result->broadcast->id; + + // Set broadcast_id to ensure broadcast is deleted after test. + $this->broadcast_ids[] = $broadcastID; + + // Confirm the Broadcast saved. + $result = get_object_vars($result->broadcast); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Subject', $result['subject']); + $this->assertEquals('Test Content', $result['content']); + $this->assertEquals('Test Broadcast from PHP SDK', $result['description']); + $this->assertEquals( + $publishedAt->format('Y-m-d') . 'T' . $publishedAt->format('H:i:s') . 'Z', + $result['published_at'] + ); + $this->assertEquals( + $sendAt->format('Y-m-d') . 'T' . $sendAt->format('H:i:s') . 'Z', + $result['send_at'] + ); + } + + /** + * Test that get_broadcast() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetBroadcast() + { + $result = $this->api->get_broadcast($_ENV['CONVERTKIT_API_BROADCAST_ID']); + $result = get_object_vars($result->broadcast); + $this->assertEquals($result['id'], $_ENV['CONVERTKIT_API_BROADCAST_ID']); + } + + /** + * Test that get_broadcast() throws a ClientException when an invalid + * broadcast ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetBroadcastWithInvalidBroadcastID() + { + $this->assertApiError(function () { + return $this->api->get_broadcast(12345); + }); + } + + /** + * Test that get_broadcast_stats() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetBroadcastStats() + { + $result = $this->api->get_broadcast_stats($_ENV['CONVERTKIT_API_BROADCAST_ID']); + $result = get_object_vars($result->broadcast); + $this->assertArrayHasKey('id', $result); + $this->assertArrayHasKey('stats', $result); + $this->assertEquals($result['stats']->recipients, 1); + $this->assertEquals($result['stats']->open_rate, 0); + $this->assertEquals($result['stats']->click_rate, 0); + $this->assertEquals($result['stats']->unsubscribes, 0); + $this->assertEquals($result['stats']->total_clicks, 0); + } + + /** + * Test that get_broadcast_stats() throws a ClientException when an invalid + * broadcast ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetBroadcastStatsWithInvalidBroadcastID() + { + $this->assertApiError(function () { + return $this->api->get_broadcast_stats(12345); + }); + } + + /** + * Test that get_broadcast_link_clicks() returns the expected data. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetBroadcastLinkClicks() + { + // Get broadcast link clicks. + $result = $this->api->get_broadcast_link_clicks( + $_ENV['CONVERTKIT_API_BROADCAST_ID'], + per_page: 1 + ); + + // Assert clicks and pagination exist. + $this->assertDataExists($result->broadcast, 'clicks'); + $this->assertPaginationExists($result); + + // Assert a single click was returned. + $this->assertCount(1, $result->broadcast->clicks); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + } + + /** + * Test that get_broadcast_link_clicks() throws a ClientException when an invalid + * broadcast ID is specified. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetBroadcastLinkClicksWithInvalidBroadcastID() + { + $this->assertApiError(function () { + return $this->api->get_broadcast_link_clicks(12345); + }); + } + + /** + * Test that get_broadcasts_stats() returns the expected data. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetBroadcastsStats() + { + // Get broadcasts stats. + $result = $this->api->get_broadcasts_stats( + per_page: 1 + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single broadcast was returned. + $this->assertCount(1, $result->broadcasts); + + // Store the Broadcast ID to check it's different from the next broadcast. + $id = $result->broadcasts[0]->id; + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_broadcasts_stats( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single broadcast was returned. + $this->assertCount(1, $result->broadcasts); + + // Assert the broadcast ID is different from the previous broadcast. + $this->assertNotEquals($id, $result->broadcasts[0]->id); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_broadcasts_stats( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert a single webhook was returned. + $this->assertCount(1, $result->broadcasts); + + // Assert the broadcast ID matches the first broadcast. + $this->assertEquals($id, $result->broadcasts[0]->id); + } + + /** + * Test that get_broadcasts_stats() returns the expected data + * when a valid sent_after date is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsStatsWithSentAfter() + { + $date = new DateTime('now'); + $date->modify('-4 years'); + $result = $this->api->get_broadcasts_stats( + sent_after: $date, + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(4, $result->broadcasts); + } + + /** + * Test that get_broadcasts_stats() returns no broadcasts + * when a sent_after date is specified that is after all broadcasts. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsStatsWithSentAfterNow() + { + $date = new DateTime('now'); + $date->modify('-1 day'); + $result = $this->api->get_broadcasts_stats( + sent_after: $date, + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert no broadcasts were returned. + $this->assertCount(0, $result->broadcasts); + } + + /** + * Test that get_broadcasts_stats() returns the expected data + * when a valid sent_before date is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsStatsWithSentBefore() + { + $date = new DateTime('now'); + $result = $this->api->get_broadcasts_stats( + sent_before: new DateTime('now'), + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(12, $result->broadcasts); + } + + /** + * Test that get_broadcasts_stats() returns the expected data + * when the completed status is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsStatsWithCompletedStatus() + { + $result = $this->api->get_broadcasts_stats( + status: 'completed' + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(12, $result->broadcasts); + } + + /** + * Test that get_broadcasts_stats() returns the expected data + * when the aborted status is specified. + * + * @since 2.5 + * + * @return void + */ + public function testGetBroadcastsStatsWithAbortedStatus() + { + $result = $this->api->get_broadcasts_stats( + status: 'aborted' + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert the expected number of broadcasts were returned. + $this->assertCount(0, $result->broadcasts); + } + + /** + * Test that get_broadcasts_stats() returns the expected data + * when the total count is included. + * + * @since 2.2.1 + * + * @return void + */ + public function testGetBroadcastsStatsWithTotalCount() + { + $result = $this->api->get_broadcasts_stats( + include_total_count: true + ); + + // Assert broadcasts and pagination exist. + $this->assertDataExists($result, 'broadcasts'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that update_broadcast() throws a ClientException when an invalid + * broadcast ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateBroadcastWithInvalidBroadcastID() + { + $this->assertApiError(function () { + return $this->api->update_broadcast(12345); + }); + } + + /** + * Test that delete_broadcast() throws a ClientException when an invalid + * broadcast ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testDeleteBroadcastWithInvalidBroadcastID() + { + $this->assertApiError(function () { + return $this->api->delete_broadcast(12345); + }); + } + + /** + * Test that get_webhooks() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetWebhooksPagination() + { + // Create webhooks first. + $results = [ + $this->api->create_webhook( + url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), + event: 'subscriber.subscriber_activate', + ), + $this->api->create_webhook( + url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), + event: 'subscriber.subscriber_activate', + ), + ]; + + // Set webhook_ids to ensure webhooks are deleted after test. + $this->webhook_ids = [ + $results[0]->webhook->id, + $results[1]->webhook->id, + ]; + + // Get webhooks. + $result = $this->api->get_webhooks( + per_page: 1 + ); + + // Assert webhooks and pagination exist. + $this->assertDataExists($result, 'webhooks'); + $this->assertPaginationExists($result); + + // Assert a single webhook was returned. + $this->assertCount(1, $result->webhooks); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_webhooks( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert webhooks and pagination exist. + $this->assertDataExists($result, 'webhooks'); + $this->assertPaginationExists($result); + + // Assert a single webhook was returned. + $this->assertCount(1, $result->webhooks); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertFalse($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_webhooks( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert webhooks and pagination exist. + $this->assertDataExists($result, 'webhooks'); + $this->assertPaginationExists($result); + + // Assert a single webhook was returned. + $this->assertCount(1, $result->webhooks); + } + + /** + * Test that create_webhook(), get_webhooks() and delete_webhook() works. + * + * We do both, so we don't end up with unnecessary webhooks remaining + * on the ConvertKit account when running tests. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateGetAndDeleteWebhook() + { + // Create a webhook first. + $result = $this->api->create_webhook( + url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), + event: 'subscriber.subscriber_activate', + ); + $id = $result->webhook->id; + + // Get webhooks. + $result = $this->api->get_webhooks(); + + // Assert webhooks and pagination exist. + $this->assertDataExists($result, 'webhooks'); + $this->assertPaginationExists($result); + + // Get webhooks including total count. + $result = $this->api->get_webhooks( + include_total_count: true + ); + + // Assert webhooks and pagination exist. + $this->assertDataExists($result, 'webhooks'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + + // Delete the webhook. + $result = $this->api->delete_webhook($id); + } + + /** + * Test that create_webhook() works with an event parameter. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateWebhookWithEventParameter() + { + // Create a webhook. + $url = 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'); + $result = $this->api->create_webhook( + url: $url, + event: 'custom_field.field_value_updated', + parameter: $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'] + ); + + // Confirm webhook created with correct data. + $this->assertArrayHasKey('webhook', get_object_vars($result)); + $this->assertArrayHasKey('id', get_object_vars($result->webhook)); + $this->assertArrayHasKey('account_id', get_object_vars($result->webhook)); + $this->assertArrayHasKey('event', get_object_vars($result->webhook)); + $this->assertArrayHasKey('target_url', get_object_vars($result->webhook)); + $this->assertEquals($result->webhook->target_url, $url); + $this->assertEquals($result->webhook->event->name, 'field_value_updated'); + $this->assertEquals($result->webhook->event->custom_field_id, $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID']); + + // Delete the webhook. + $result = $this->api->delete_webhook($result->webhook->id); + } + + /** + * Test that create_webhook() throws an InvalidArgumentException when an invalid + * event is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateWebhookWithInvalidEvent() + { + $this->assertApiError(function () { + return $this->api->create_webhook( + url: 'https://webhook.site/' . str_shuffle('wfervdrtgsdewrafvwefds'), + event: 'invalid.event' + ); + }); + } + + /** + * Test that delete_webhook() throws a ClientException when an invalid + * ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testDeleteWebhookWithInvalidID() + { + $this->assertApiError(function () { + return $this->api->delete_webhook(12345); + }); + } + + /** + * Test that get_custom_fields() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetCustomFields() + { + $result = $this->api->get_custom_fields(); + + // Assert custom fields and pagination exist. + $this->assertDataExists($result, 'custom_fields'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_custom_fields() returns the expected data + * when the total count is included. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetCustomFieldsWithTotalCount() + { + $result = $this->api->get_custom_fields( + include_total_count: true + ); + + // Assert custom fields and pagination exist. + $this->assertDataExists($result, 'custom_fields'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_custom_fields() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetCustomFieldsPagination() + { + $result = $this->api->get_custom_fields( + per_page: 1 + ); + + // Assert custom fields and pagination exist. + $this->assertDataExists($result, 'custom_fields'); + $this->assertPaginationExists($result); + + // Assert a single custom field was returned. + $this->assertCount(1, $result->custom_fields); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_custom_fields( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert custom fields and pagination exist. + $this->assertDataExists($result, 'custom_fields'); + $this->assertPaginationExists($result); + + // Assert a single custom field was returned. + $this->assertCount(1, $result->custom_fields); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_custom_fields( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert custom fields and pagination exist. + $this->assertDataExists($result, 'custom_fields'); + $this->assertPaginationExists($result); + + // Assert a single custom field was returned. + $this->assertCount(1, $result->custom_fields); + } + + /** + * Test that create_custom_field() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateCustomField() + { + $label = 'Custom Field ' . mt_rand(); + $result = $this->api->create_custom_field($label); + + // Set custom_field_ids to ensure custom fields are deleted after test. + $this->custom_field_ids[] = $result->custom_field->id; + + $result = get_object_vars($result->custom_field); + $this->assertArrayHasKey('id', $result); + $this->assertArrayHasKey('name', $result); + $this->assertArrayHasKey('key', $result); + $this->assertArrayHasKey('label', $result); + $this->assertEquals($result['label'], $label); + } + + /** + * Test that create_custom_field() throws a ClientException when a blank + * label is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateCustomFieldWithBlankLabel() + { + $this->assertApiError(function () { + return $this->api->create_custom_field(''); + }); + } + + /** + * Test that create_custom_fields() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreateCustomFields() + { + $labels = [ + 'Custom Field ' . mt_rand(), + 'Custom Field ' . mt_rand(), + ]; + $result = $this->api->create_custom_fields($labels); + + // Set custom_field_ids to ensure custom fields are deleted after test. + foreach ($result->custom_fields as $index => $customField) { + $this->custom_field_ids[] = $customField->id; + } + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each custom field that was created. + $this->assertIsArray($result->custom_fields); + } + + /** + * Test that update_subscriber_custom_field_values() works. + * + * @since 2.4.0 + * + * @return void + */ + public function testUpdateSubscriberCustomFieldValues() + { + // Create subscribers. + $subscribers = [ + [ + 'email_address' => str_replace('@kit.com', '-1@kit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@kit.com', '-2@kit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // Set subscriber_id to ensure subscriber is unsubscribed after test. + foreach ($result->subscribers as $i => $subscriber) { + $this->subscriber_ids[] = $subscriber->id; + } + + // Bulk update subscriber custom field values. + $result = $this->api->update_subscriber_custom_field_values( + [ + [ + 'subscriber_id' => $this->subscriber_ids[0], + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '100', + ], + [ + 'subscriber_id' => $this->subscriber_ids[1], + 'subscriber_custom_field_id' => (int) $_ENV['CONVERTKIT_API_CUSTOM_FIELD_ID'], + 'value' => '200', + ], + ] + ); + + // Assert no failures. + $this->assertCount(0, $result->failures); + + // Confirm result is an array comprising of each custom field value that was updated. + $this->assertIsArray($result->custom_field_values); + $this->assertCount(2, $result->custom_field_values); + } + + /** + * Test that update_custom_field() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateCustomField() + { + // Create custom field. + $label = 'Custom Field ' . mt_rand(); + $result = $this->api->create_custom_field($label); + $id = $result->custom_field->id; + + // Set custom_field_ids to ensure custom fields are deleted after test. + $this->custom_field_ids[] = $result->custom_field->id; + + // Change label. + $newLabel = 'Custom Field ' . mt_rand(); + $this->api->update_custom_field($id, $newLabel); + + // Confirm label changed. + $customFields = $this->api->get_custom_fields(); + foreach ($customFields->custom_fields as $customField) { + if ($customField->id === $id) { + $this->assertEquals($customField->label, $newLabel); + } + } + } + + /** + * Test that update_custom_field() throws a ClientException when an + * invalid custom field ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testUpdateCustomFieldWithInvalidID() + { + $this->assertApiError(function () { + return $this->api->update_custom_field(12345, 'Something'); + }); + } + + /** + * Test that delete_custom_field() works. + * + * @since 1.0.0 + * + * @return void + */ + public function testDeleteCustomField() + { + // Create custom field. + $label = 'Custom Field ' . mt_rand(); + $result = $this->api->create_custom_field($label); + $id = $result->custom_field->id; + + // Delete custom field as tests passed. + $this->api->delete_custom_field($id); + + // Confirm custom field no longer exists. + $customFields = $this->api->get_custom_fields(); + foreach ($customFields->custom_fields as $customField) { + $this->assertNotEquals($customField->id, $id); + } + } + + /** + * Test that delete_custom_field() throws a ClientException when an + * invalid custom field ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testDeleteCustomFieldWithInvalidID() + { + $this->assertApiError(function () { + return $this->api->delete_custom_field(12345); + }); + } + + /** + * Test that get_purchases() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetPurchases() + { + $result = $this->api->get_purchases(); + + // Assert purchases and pagination exist. + $this->assertDataExists($result, 'purchases'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_purchases() returns the expected data + * when the total count is included. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetPurchasesWithTotalCount() + { + $result = $this->api->get_purchases( + include_total_count: true + ); + + // Assert purchases and pagination exist. + $this->assertDataExists($result, 'purchases'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_purchases() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetPurchasesPagination() + { + $result = $this->api->get_purchases( + per_page: 1 + ); + + // Assert purchases and pagination exist. + $this->assertDataExists($result, 'purchases'); + $this->assertPaginationExists($result); + + // Assert a single purchase was returned. + $this->assertCount(1, $result->purchases); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_purchases( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert purchases and pagination exist. + $this->assertDataExists($result, 'purchases'); + $this->assertPaginationExists($result); + + // Assert a single purchase was returned. + $this->assertCount(1, $result->purchases); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_purchases( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert purchases and pagination exist. + $this->assertDataExists($result, 'purchases'); + $this->assertPaginationExists($result); + + // Assert a single purchase was returned. + $this->assertCount(1, $result->purchases); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that get_purchases() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetPurchase() + { + // Get ID of first purchase. + $purchases = $this->api->get_purchases( + per_page: 1 + ); + $id = $purchases->purchases[0]->id; + + // Get purchase. + $result = $this->api->get_purchase($id); + $this->assertInstanceOf('stdClass', $result); + $this->assertEquals($purchases->purchases[0]->id, $id); + } + + /** + * Test that get_purchases() throws a ClientException when an invalid + * purchase ID is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetPurchaseWithInvalidID() + { + $this->assertApiError(function () { + return $this->api->get_purchase(12345); + }); + } + + /** + * Test that create_purchase() returns the expected data. + * + * @since 1.0.0 + * + * @return void + */ + public function testCreatePurchase() + { + $purchase = $this->api->create_purchase( + // Required fields. + email_address: $this->generateEmailAddress(), + transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), + currency: 'usd', + products: [ + [ + 'name' => 'Floppy Disk (512k)', + 'sku' => '7890-ijkl', + 'pid' => 9999, + 'lid' => 7777, + 'quantity' => 2, + 'unit_price' => 5.00, + ], + [ + 'name' => 'Telephone Cord (data)', + 'sku' => 'mnop-1234', + 'pid' => 5555, + 'lid' => 7778, + 'quantity' => 1, + 'unit_price' => 10.00, + ], + ], + // Optional fields. + first_name: 'Tim', + status: 'paid', + subtotal: 20.00, + tax: 2.00, + shipping: 2.00, + discount: 3.00, + total: 21.00, + transaction_time: new DateTime('now'), + ); + + $this->assertInstanceOf('stdClass', $purchase); + $this->assertArrayHasKey('transaction_id', get_object_vars($purchase->purchase)); + } + + /** + * Test that create_purchase() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreatePurchaseWithInvalidEmailAddress() + { + $this->assertApiError(function () { + return $this->api->create_purchase( + email_address: 'not-an-email-address', + transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), + currency: 'usd', + products: [ + [ + 'name' => 'Floppy Disk (512k)', + 'sku' => '7890-ijkl', + 'pid' => 9999, + 'lid' => 7777, + 'quantity' => 2, + 'unit_price' => 5.00, + ], + ], + ); + }); + } + + /** + * Test that create_purchase() throws a ClientException when a blank + * transaction ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreatePurchaseWithBlankTransactionID() + { + $this->assertApiError(function () { + return $this->api->create_purchase( + email_address: $this->generateEmailAddress(), + transaction_id: '', + currency: 'usd', + products: [ + [ + 'name' => 'Floppy Disk (512k)', + 'sku' => '7890-ijkl', + 'pid' => 9999, + 'lid' => 7777, + 'quantity' => 2, + 'unit_price' => 5.00, + ], + ], + ); + }); + } + + /** + * Test that create_purchase() throws a ClientException when no products + * are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreatePurchaseWithNoProducts() + { + $this->assertApiError(function () { + return $this->api->create_purchase( + email_address: $this->generateEmailAddress(), + transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), + currency: 'usd', + products: [], + ); + }); + } + + /** + * Test that get_segments() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSegments() + { + $result = $this->api->get_segments(); + + // Assert segments and pagination exist. + $this->assertDataExists($result, 'segments'); + $this->assertPaginationExists($result); + } + + /** + * Test that get_segments() returns the expected data + * when the total count is included. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSegmentsWithTotalCount() + { + $result = $this->api->get_segments( + include_total_count: true + ); + + // Assert segments and pagination exist. + $this->assertDataExists($result, 'segments'); + $this->assertPaginationExists($result); + + // Assert total count is included. + $this->assertArrayHasKey('total_count', get_object_vars($result->pagination)); + $this->assertGreaterThan(0, $result->pagination->total_count); + } + + /** + * Test that get_segments() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSegmentsPagination() + { + $result = $this->api->get_segments( + per_page: 1 + ); + + // Assert segments and pagination exist. + $this->assertDataExists($result, 'segments'); + $this->assertPaginationExists($result); + + // Assert a single segment was returned. + $this->assertCount(1, $result->segments); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch next page. + $result = $this->api->get_segments( + per_page: 1, + after_cursor: $result->pagination->end_cursor + ); + + // Assert segments and pagination exist. + $this->assertDataExists($result, 'segments'); + $this->assertPaginationExists($result); + + // Assert a single segment was returned. + $this->assertCount(1, $result->segments); + + // Assert has_previous_page and has_next_page are correct. + $this->assertTrue($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + + // Use pagination to fetch previous page. + $result = $this->api->get_segments( + per_page: 1, + before_cursor: $result->pagination->start_cursor + ); + + // Assert segments and pagination exist. + $this->assertDataExists($result, 'segments'); + $this->assertPaginationExists($result); + + // Assert a single segment was returned. + $this->assertCount(1, $result->segments); + + // Assert has_previous_page and has_next_page are correct. + $this->assertFalse($result->pagination->has_previous_page); + $this->assertTrue($result->pagination->has_next_page); + } + + /** + * Test that fetching a legacy form's markup works. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetResourceLegacyForm() + { + $markup = $this->api->get_resource($_ENV['CONVERTKIT_API_LEGACY_FORM_URL']); + + // Assert that the markup is HTML. + $this->assertTrue($this->isHtml($markup)); + + // Confirm that encoding works correctly. + $this->assertStringContainsString('Vantar þinn ungling sjálfstraust í stærðfræði?', $markup); + } + + /** + * Test that fetching a landing page's markup works. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetResourceLandingPage() + { + $markup = $this->api->get_resource($_ENV['CONVERTKIT_API_LANDING_PAGE_URL']); + + // Assert that the markup is HTML. + $this->assertTrue($this->isHtml($markup)); + + // Confirm that encoding works correctly. + $this->assertStringContainsString('Vantar þinn ungling sjálfstraust í stærðfræði?', $markup); + } + + /** + * Test that fetching a legacy landing page's markup works. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetResourceLegacyLandingPage() + { + $markup = $this->api->get_resource($_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_URL']); + + // Assert that the markup is HTML. + $this->assertTrue($this->isHtml($markup)); + + // Confirm that encoding works correctly. + $this->assertStringContainsString('Legacy Landing Page', $markup); + } + + /** + * Test that get_resource() throws an InvalidArgumentException when an invalid + * URL is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetResourceInvalidURL() + { + $this->assertApiError(function () { + return $this->api->get_resource('not-a-url'); + }); + } + + /** + * Test that get_resource() throws a ClientException when an inaccessible + * URL is specified. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetResourceInaccessibleURL() + { + $this->assertApiError(function () { + return $this->api->get_resource('https://kit.com/a/url/that/does/not/exist'); + }); + } + + /** + * Generates a unique email address for use in a test, comprising of a prefix, + * date + time and PHP version number. + * + * This ensures that if tests are run in parallel, the same email address + * isn't used for two tests across parallel testing runs. + * + * @since 1.0.0 + * + * @param string $domain Domain (default: kit.com). + * + * @return string + */ + public function generateEmailAddress($domain = 'kit.com') + { + return 'php-sdk-' . date('Y-m-d-H-i-s') . '-php-' . PHP_VERSION_ID . '@' . $domain; + } + + /** + * Checks if string is html. + * + * @since 1.0.0 + * + * @param string $string Possible HTML. + * @return bool + */ + public function isHtml($string) + { + return preg_match("/<[^<]+>/", $string, $m) != 0; + } + + /** + * Helper method to assert the given key exists as an array in the API response. + * + * @since 2.0.0 + * + * @param object $result API Result. + * @param string $key Key. + */ + public function assertDataExists($result, $key) + { + $result = get_object_vars($result); + $this->assertArrayHasKey($key, $result); + $this->assertIsArray($result[$key]); + } + + /** + * Helper method to assert pagination object exists in response. + * + * @since 2.0.0 + * + * @param object $result API Result. + */ + public function assertPaginationExists($result) + { + $result = get_object_vars($result); + $this->assertArrayHasKey('pagination', $result); + $pagination = get_object_vars($result['pagination']); + $this->assertArrayHasKey('has_previous_page', $pagination); + $this->assertArrayHasKey('has_next_page', $pagination); + $this->assertArrayHasKey('start_cursor', $pagination); + $this->assertArrayHasKey('end_cursor', $pagination); + $this->assertArrayHasKey('per_page', $pagination); + } +} From dc9358cc708a6c5b4ceb26091789b0c7f57e4d80 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 15:19:07 +0800 Subject: [PATCH 06/15] Add ObjectResponseProxy Helper --- tests/Integration/APITest.php | 75 +++++++-------- tests/Integration/TestsTrait.php | 19 ++-- tests/Support/Helper/WPUnit/API.php | 50 ---------- .../Helper/WPUnit/ObjectResponseProxy.php | 92 +++++++++++++++++++ 4 files changed, 144 insertions(+), 92 deletions(-) delete mode 100644 tests/Support/Helper/WPUnit/API.php create mode 100644 tests/Support/Helper/WPUnit/ObjectResponseProxy.php diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 162a8a6..d825519 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -92,24 +92,61 @@ public function setUp(): void require_once 'src/class-convertkit-log.php'; // Initialize the classes we want to test. - $this->api = new \ConvertKit_API_V4( + $rawApi = new \ConvertKit_API_V4( client_id: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], redirect_uri: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], access_token: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], refresh_token: $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'] ); - $this->api_no_data = new \ConvertKit_API_V4( + $rawApiNoData = new \ConvertKit_API_V4( client_id: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], redirect_uri: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], access_token: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA'], refresh_token: $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA'] ); + // For tests from TestsTrait, use the ObjectResponseProxy to convert array responses to stdClass. + if ($this->currentTestIsFromTestsTrait()) { + require_once __DIR__ . '/../Support/Helper/WPUnit/ObjectResponseProxy.php'; + $this->api = new \Helper\WPUnit\ObjectResponseProxy($rawApi); + $this->api_no_data = new \Helper\WPUnit\ObjectResponseProxy($rawApiNoData); + } else { + $this->api = $rawApi; + $this->api_no_data = $rawApiNoData; + } + // Wait a second to avoid hitting a 429 rate limit. sleep(1); } + /** + * Returns true when the currently-executing test method was defined on + * the shared TestsTrait rather than on this class. + * + * PHP reflection preserves the source file of trait-provided methods + * even after the trait has been flattened into the composing class, so + * we can distinguish the two just by looking at the declaring file. + * + * @since 2.6.0 + * + * @return bool + */ + private function currentTestIsFromTestsTrait(): bool + { + try { + // PHPUnit 10+ uses name(); older uses getName(false). Prefer the new API. + $testName = method_exists($this, 'name') + ? $this->name() + : $this->getName(false); + $method = new \ReflectionMethod($this, $testName); + $file = $method->getFileName(); + return $file !== false && basename($file) === 'TestsTrait.php'; + } catch (\Throwable $e) { + return false; + } + } + /** * Performs actions after each test. * @@ -865,23 +902,6 @@ public function testGetLegacyLandingPagesWithTotalCount() $this->assertArrayHasKey('total_count', $result['pagination']); $this->assertGreaterThan(0, $result['pagination']['total_count']); } - /** - * Test that add_subscriber_to_form_by_email() returns a WP_Error when an invalid - * form is specified. - * - * @since 1.0.0 - * - * @return void - */ - public function testAddSubscriberToFormByEmailWithInvalidformID() - { - $result = $this->api->add_subscriber_to_form_by_email( - form_id: 12345, - email_address: $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } /** * Test that add_subscriber_to_form() returns a WP_Error when a legacy * form ID is specified. @@ -900,23 +920,6 @@ public function testAddSubscriberToFormWithLegacyFormID() $this->assertEquals($result->get_error_code(), $this->errorCode); } - /** - * Test that add_subscriber_to_form() returns a WP_Error when an invalid - * email address is specified. - * - * @since 2.0.0 - * - * @return void - */ - public function testAddSubscriberToformWithInvalidSubscriberID() - { - $result = $this->api->add_subscriber_to_form( - form_id: (int) $_ENV['CONVERTKIT_API_FORM_ID'], - subscriber_id: 12345 - ); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } /** * Test that add_subscriber_to_legacy_form() returns the expected data. diff --git a/tests/Integration/TestsTrait.php b/tests/Integration/TestsTrait.php index 15c1657..db78e5a 100644 --- a/tests/Integration/TestsTrait.php +++ b/tests/Integration/TestsTrait.php @@ -6954,14 +6954,18 @@ public function isHtml($string) /** * Helper method to assert the given key exists as an array in the API response. * + * Accepts either a stdClass object (PHP SDK, Guzzle-decoded) or an + * associative array (WP Libs, wp_remote_retrieve_body -> json_decode true), + * so the same trait file works verbatim in both repos. + * * @since 2.0.0 * - * @param object $result API Result. - * @param string $key Key. + * @param object|array $result API Result. + * @param string $key Key. */ public function assertDataExists($result, $key) { - $result = get_object_vars($result); + $result = is_object($result) ? get_object_vars($result) : $result; $this->assertArrayHasKey($key, $result); $this->assertIsArray($result[$key]); } @@ -6969,15 +6973,18 @@ public function assertDataExists($result, $key) /** * Helper method to assert pagination object exists in response. * + * Accepts either a stdClass object (PHP SDK) or an associative array + * (WP Libs), so the same trait file works verbatim in both repos. + * * @since 2.0.0 * - * @param object $result API Result. + * @param object|array $result API Result. */ public function assertPaginationExists($result) { - $result = get_object_vars($result); + $result = is_object($result) ? get_object_vars($result) : $result; $this->assertArrayHasKey('pagination', $result); - $pagination = get_object_vars($result['pagination']); + $pagination = is_object($result['pagination']) ? get_object_vars($result['pagination']) : $result['pagination']; $this->assertArrayHasKey('has_previous_page', $pagination); $this->assertArrayHasKey('has_next_page', $pagination); $this->assertArrayHasKey('start_cursor', $pagination); diff --git a/tests/Support/Helper/WPUnit/API.php b/tests/Support/Helper/WPUnit/API.php deleted file mode 100644 index 732d736..0000000 --- a/tests/Support/Helper/WPUnit/API.php +++ /dev/null @@ -1,50 +0,0 @@ -{yourFunctionName}. - */ -class API extends \Codeception\Module -{ - /** - * Sends a request to the ConvertKit API, typically used to read an endpoint to confirm - * that data in an Acceptance Test was added/edited/deleted successfully. - * - * @param string $endpoint Endpoint. - * @param string $method Method (GET|POST|PUT). - * @param array $params Endpoint Parameters. - * @return array - */ - public function apiRequest($endpoint, $method = 'GET', $params = array()) - { - // Build query parameters. - $params = array_merge( - $params, - [ - 'api_key' => $_ENV['CONVERTKIT_API_KEY'], - 'api_secret' => $_ENV['CONVERTKIT_API_SECRET'], - ] - ); - - // Send request. - try { - $client = new \GuzzleHttp\Client(); - $result = $client->request( - $method, - 'https://api.kit.com/v3/' . $endpoint . '?' . http_build_query($params), - [ - 'headers' => [ - 'Accept-Encoding' => 'gzip', - 'timeout' => 5, - ], - ] - ); - - // Return JSON decoded response. - return json_decode($result->getBody()->getContents(), true); - } catch (\GuzzleHttp\Exception\ClientException $e) { - return []; - } - } -} diff --git a/tests/Support/Helper/WPUnit/ObjectResponseProxy.php b/tests/Support/Helper/WPUnit/ObjectResponseProxy.php new file mode 100644 index 0000000..8882df9 --- /dev/null +++ b/tests/Support/Helper/WPUnit/ObjectResponseProxy.php @@ -0,0 +1,92 @@ +forms, $result->pagination->end_cursor, etc.). + * + * This proxy exists solely to bridge that gap in test context — it lets the + * trait run unmodified against WP Libs. Plugin code that consumes WP Libs + * in production is unaffected; only the test suite ever sees the proxy. + * + * `WP_Error` and other non-array returns pass through untouched. + * + * @since 2.6.0 + */ +class ObjectResponseProxy +{ + /** + * The underlying `ConvertKit_API_V4` instance. + * + * @var ConvertKit_API_V4 + */ + private $api; + + /** + * Constructor. + * + * @param ConvertKit_API_V4 $api API Instance. + */ + public function __construct($api) + { + $this->api = $api; + } + + /** + * Forward every method call to the wrapped API. If the return value is + * an array, convert it recursively to a nested `stdClass` so the trait's + * object-style assertions work verbatim. + * + * @param string $name Method name. + * @param array $args Positional arguments. + * @return mixed + */ + public function __call($name, $args) + { + $result = $this->api->$name(...$args); + + if (is_array($result)) { + // Recursive array -> nested stdClass via a round-trip through JSON. + // Cheap, dependency-free, and preserves nested pagination objects. + return json_decode(wp_json_encode($result)); + } + + return $result; + } + + /** + * Forward property reads (e.g. debug flags, tokens) to the wrapped API. + * + * @param string $name Property name. + * @return mixed + */ + public function __get($name) + { + return $this->api->$name; + } + + /** + * Forward property writes to the wrapped API. + * + * @param string $name Property name. + * @param mixed $value Value. + * @return void + */ + public function __set($name, $value) + { + $this->api->$name = $value; + } + + /** + * Expose the wrapped raw API. Useful for tests that occasionally need + * the array-returning behaviour. + * + * @return object + */ + public function raw() + { + return $this->api; + } +} From 3c090c09e88358a2434f7814ab648ceca3070e0a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 15:27:37 +0800 Subject: [PATCH 07/15] Add new .env vars --- .env.dist.testing | 3 +++ .env.example | 3 +++ .github/workflows/tests.yml | 2 +- src/class-convertkit-api-v4.php | 15 +++++---------- .../Support/Helper/WPUnit/ObjectResponseProxy.php | 13 +++---------- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.env.dist.testing b/.env.dist.testing index d4e141b..a65f19d 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -51,4 +51,7 @@ CONVERTKIT_API_TAG_NAME_2="gravityforms-tag-1" CONVERTKIT_API_TAG_ID_2="2907192" CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" +CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" +CONVERTKIT_API_POST_ID="3175837" +CONVERTKIT_API_SNIPPET_ID="136038" CONVERTKIT_API_RECOMMENDATIONS_JS="https://cheerful-architect-3237.kit.com/WnaDZ370gtgOq750dwOl-recommendations.js" diff --git a/.env.example b/.env.example index faf05a5..6b9412a 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,9 @@ CONVERTKIT_API_TAG_NAME_2="gravityforms-tag-1" CONVERTKIT_API_TAG_ID_2="2907192" CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" +CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" +CONVERTKIT_API_POST_ID="3175837" +CONVERTKIT_API_SNIPPET_ID="136038" CONVERTKIT_API_SIGNED_SUBSCRIBER_ID= CONVERTKIT_API_SUBSCRIBER_TOKEN= CONVERTKIT_API_RECOMMENDATIONS_JS="https://cheerful-architect-3237.kit.com/WnaDZ370gtgOq750dwOl-recommendations.js" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f7d157..af3d9a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,7 +51,7 @@ jobs: max-parallel: 1 matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1', '8.2' ] + php-versions: [ '8.0' ] #[ '7.4', '8.0', '8.1', '8.2' ] # Steps to install, configure and run tests steps: diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 5d55c87..bab1e5b 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -83,11 +83,8 @@ class ConvertKit_API_V4 { * The HTTP status code of the last API response. * * Set by request() after every call. Read via get_last_response_code(). - * Provides parity with the PHP SDK's getResponseInterface()->getStatusCode(), - * so tests that assert a specific status code (e.g. 204 for deletes) can be - * copy-pasted between the SDK and WP Libs without change. * - * @since 2.0.5 + * @since 2.6.0 * * @var int */ @@ -242,11 +239,7 @@ private function generate_and_store_code_verifier() { /** * Returns the HTTP status code of the last API response. * - * Mirrors the PHP SDK's `getResponseInterface()->getStatusCode()` so tests - * that assert a status code (for example 204 on delete) can be copy-pasted - * between the SDK and WP Libs unchanged. - * - * @since 2.0.5 + * @since 2.6.0 * * @return int HTTP status code of the last API response (0 if none). */ @@ -1539,9 +1532,11 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // Fetch HTTP response code and body. $http_response_code = wp_remote_retrieve_response_code( $result ); - $this->last_response_code = (int) $http_response_code; $body = wp_remote_retrieve_body( $result ); + // Store the HTTP response code. + $this->last_response_code = (int) $http_response_code; + // If the body is null i.e. a 204 No Content, don't attempt to JSON decode it. $response = ( ! empty( $body ) ? json_decode( $body, true ) : null ); diff --git a/tests/Support/Helper/WPUnit/ObjectResponseProxy.php b/tests/Support/Helper/WPUnit/ObjectResponseProxy.php index 8882df9..cdea12f 100644 --- a/tests/Support/Helper/WPUnit/ObjectResponseProxy.php +++ b/tests/Support/Helper/WPUnit/ObjectResponseProxy.php @@ -2,16 +2,9 @@ namespace Helper\WPUnit; /** - * WP Libs' `request()` returns associative arrays (WordPress convention); - * the PHP SDK returns `stdClass` objects (Guzzle convention). The shared - * `TestsTrait.php` is copied verbatim from the SDK and therefore uses object - * access throughout ($result->forms, $result->pagination->end_cursor, etc.). - * - * This proxy exists solely to bridge that gap in test context — it lets the - * trait run unmodified against WP Libs. Plugin code that consumes WP Libs - * in production is unaffected; only the test suite ever sees the proxy. - * - * `WP_Error` and other non-array returns pass through untouched. + * Wraps `ConvertKit_API_V4` and converts every array response to a nested `stdClass` + * to mirror the PHP SDK's responses, ensuring TestsTrait tests can be used across + * both WordPress Libraries and the PHP SDK. * * @since 2.6.0 */ From 909c24fa258d74bcfc4dcdda1cce82a0dabb4ec4 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 15:45:01 +0800 Subject: [PATCH 08/15] Update .env --- .env.dist.testing | 3 +++ .env.example | 3 +++ tests/Integration/TestsTrait.php | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.dist.testing b/.env.dist.testing index a65f19d..30cb02e 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -35,7 +35,9 @@ TEST_SITE_HTTP_USER_AGENT_MOBILE=HeadlessChromeMobile CONVERTKIT_API_BROADCAST_ID="8697158" CONVERTKIT_API_CUSTOM_FIELD_ID="258240" CONVERTKIT_API_FORM_ID="2765139" +CONVERTKIT_API_FORM_ID_2="2780977" CONVERTKIT_API_LEGACY_FORM_ID="470099" +CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099" CONVERTKIT_API_LANDING_PAGE_ID="2765196" CONVERTKIT_API_LANDING_PAGE_URL="https://cheerful-architect-3237.kit.com/99f1db6843" CONVERTKIT_API_LANDING_PAGE_CHARACTER_ENCODING_ID="2849151" @@ -45,6 +47,7 @@ CONVERTKIT_API_LEGACY_LANDING_PAGE_URL="https://app.kit.com/landing_pages/470103 CONVERTKIT_API_POST_ID="3175837" CONVERTKIT_API_PRODUCT_ID="36377" CONVERTKIT_API_SEQUENCE_ID="1030824" +CONVERTKIT_API_SEQUENCE_EMAIL_ID="4533458" CONVERTKIT_API_TAG_NAME="wordpress" CONVERTKIT_API_TAG_ID="2744672" CONVERTKIT_API_TAG_NAME_2="gravityforms-tag-1" diff --git a/.env.example b/.env.example index 6b9412a..0298190 100644 --- a/.env.example +++ b/.env.example @@ -43,7 +43,9 @@ CONVERTKIT_OAUTH_REDIRECT_URI= CONVERTKIT_API_BROADCAST_ID="8697158" CONVERTKIT_API_CUSTOM_FIELD_ID="258240" CONVERTKIT_API_FORM_ID="2765139" +CONVERTKIT_API_FORM_ID_2="2780977" CONVERTKIT_API_LEGACY_FORM_ID="470099" +CONVERTKIT_API_LEGACY_FORM_URL="https://app.convertkit.com/landing_pages/470099" CONVERTKIT_API_LANDING_PAGE_ID="2765196" CONVERTKIT_API_LANDING_PAGE_URL="https://cheerful-architect-3237.kit.com/99f1db6843" CONVERTKIT_API_LANDING_PAGE_CHARACTER_ENCODING_ID="2849151" @@ -53,6 +55,7 @@ CONVERTKIT_API_LEGACY_LANDING_PAGE_URL="https://app.kit.com/landing_pages/470103 CONVERTKIT_API_POST_ID="3175837" CONVERTKIT_API_PRODUCT_ID="36377" CONVERTKIT_API_SEQUENCE_ID="1030824" +CONVERTKIT_API_SEQUENCE_EMAIL_ID="4533458" CONVERTKIT_API_TAG_NAME="wordpress" CONVERTKIT_API_TAG_ID="2744672" CONVERTKIT_API_TAG_NAME_2="gravityforms-tag-1" diff --git a/tests/Integration/TestsTrait.php b/tests/Integration/TestsTrait.php index db78e5a..4bd996d 100644 --- a/tests/Integration/TestsTrait.php +++ b/tests/Integration/TestsTrait.php @@ -6863,7 +6863,7 @@ public function testGetResourceLegacyForm() */ public function testGetResourceLandingPage() { - $markup = $this->api->get_resource($_ENV['CONVERTKIT_API_LANDING_PAGE_URL']); + $markup = $this->api->get_resource($_ENV['CONVERTKIT_API_LANDING_PAGE_CHARACTER_ENCODING_URL']); // Assert that the markup is HTML. $this->assertTrue($this->isHtml($markup)); From 7af38e4be6def42a8fcd69be77e366b37d147fd8 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 15:59:58 +0800 Subject: [PATCH 09/15] Reinstate get_posts_* tests that are WordPress specific --- tests/Integration/APITest.php | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index d825519..da0d092 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -1440,6 +1440,73 @@ public function testSequenceSubscribeWithInvalidEmailAddress() $this->assertInstanceOf(\WP_Error::class, $result); $this->assertEquals($result->get_error_code(), $this->errorCode); } + + /** + * Test that the `get_posts()` function returns expected data. + * + * @since 1.0.0 + */ + public function testGetPosts() + { + $result = $this->api->get_posts(); + + // Test array was returned. + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + + // Test expected response keys exist. + $this->assertArrayHasKey('total_posts', $result); + $this->assertArrayHasKey('page', $result); + $this->assertArrayHasKey('total_pages', $result); + $this->assertArrayHasKey('posts', $result); + + // Test first post within posts array. + $this->assertArrayHasKey('id', reset($result['posts'])); + $this->assertArrayHasKey('title', reset($result['posts'])); + $this->assertArrayHasKey('url', reset($result['posts'])); + $this->assertArrayHasKey('published_at', reset($result['posts'])); + $this->assertArrayHasKey('is_paid', reset($result['posts'])); + } + + /** + * Test that get_posts() returns the expected data + * when the post content is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsWithIncludeContent() + { + $this->markTestSkipped('Kit WordPress Libraries uses the wordpress/posts endpoint, which does not support the include_content parameter.'); + } + + /** + * Test that get_posts() returns the expected data + * when the total count is included. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsWithTotalCount() + { + $this->markTestSkipped('Kit WordPress Libraries uses the wordpress/posts endpoint, which does not support the total_count parameter.'); + } + + /** + * Test that get_posts() returns the expected data + * when pagination parameters and per_page limits are specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetPostsPagination() + { + $this->markTestSkipped('Kit WordPress Libraries uses the wordpress/posts endpoint, which does not support the pagination parameter.'); + } + /** * Test that the `get_posts()` function returns a blank array when no data * exists on the ConvertKit account. From 4dd02f19bbb1e43e942dbbf38fbcb09f8c6294dc Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 16:01:49 +0800 Subject: [PATCH 10/15] Coding standards --- src/class-convertkit-api-v4.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index bab1e5b..bbd736b 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1531,8 +1531,8 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i } // Fetch HTTP response code and body. - $http_response_code = wp_remote_retrieve_response_code( $result ); - $body = wp_remote_retrieve_body( $result ); + $http_response_code = wp_remote_retrieve_response_code( $result ); + $body = wp_remote_retrieve_body( $result ); // Store the HTTP response code. $this->last_response_code = (int) $http_response_code; From 54555ba1b5587cbc228d38ef5d3c72d530459e1b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 16:15:58 +0800 Subject: [PATCH 11/15] Remove dupliate CONVERTKIT_API_POST_ID from .env --- .env.dist.testing | 1 - .env.example | 1 - 2 files changed, 2 deletions(-) diff --git a/.env.dist.testing b/.env.dist.testing index 30cb02e..8d7fb74 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -55,6 +55,5 @@ CONVERTKIT_API_TAG_ID_2="2907192" CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" -CONVERTKIT_API_POST_ID="3175837" CONVERTKIT_API_SNIPPET_ID="136038" CONVERTKIT_API_RECOMMENDATIONS_JS="https://cheerful-architect-3237.kit.com/WnaDZ370gtgOq750dwOl-recommendations.js" diff --git a/.env.example b/.env.example index 0298190..e8bd896 100644 --- a/.env.example +++ b/.env.example @@ -63,7 +63,6 @@ CONVERTKIT_API_TAG_ID_2="2907192" CONVERTKIT_API_SUBSCRIBER_EMAIL="optin@n7studios.com" CONVERTKIT_API_SUBSCRIBER_ID="1579118532" CONVERTKIT_API_EMAIL_TEMPLATE_ID="5215567" -CONVERTKIT_API_POST_ID="3175837" CONVERTKIT_API_SNIPPET_ID="136038" CONVERTKIT_API_SIGNED_SUBSCRIBER_ID= CONVERTKIT_API_SUBSCRIBER_TOKEN= From 3c48fcf0e452d44c59e1ed5d1e8531309ca803b8 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 16:18:41 +0800 Subject: [PATCH 12/15] Override `testGetPost` to use WordPress Libraries version --- tests/Integration/APITest.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index da0d092..bc6c76e 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -1670,7 +1670,7 @@ public function testGetAllPostsWithInvalidPostsPerRequestParameter() * * @since 1.3.8 */ - public function testGetPostByID() + public function testGetPost() { $result = $this->api->get_post($_ENV['CONVERTKIT_API_POST_ID']); $this->assertNotInstanceOf(\WP_Error::class, $result); @@ -1687,19 +1687,6 @@ public function testGetPostByID() $this->assertArrayHasKey('content', $result); } - /** - * Test that the `get_post()` function returns a WP_Error when an invalid - * Post ID is specified. - * - * @since 1.3.8 - */ - public function testGetPostByInvalidID() - { - $result = $this->api->get_post(12345); - $this->assertInstanceOf(\WP_Error::class, $result); - $this->assertEquals($result->get_error_code(), $this->errorCode); - } - /** * Test that the `get_products()` function returns expected data. * From 406a408a5fdfdc84ebe11a3725f00be4bb53d5da Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 16:57:52 +0800 Subject: [PATCH 13/15] Add `get_resource` method to match PHP SDK --- src/class-convertkit-api-v4.php | 17 +++++++++++++++++ tests/Integration/TestsTrait.php | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index bbd736b..73623b7 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1386,6 +1386,23 @@ public function get_html( $url, $body_only = true ) { } + /** + * PHP SDK method to get HTML for the given URL, which will be either a: + * - Legacy Form + * - Legacy Landing Page + * - Landing Page + * + * This isn't specifically an API function, but for now it's best suited here. + * + * @param string $url URL of Form or Landing Page. + * @return WP_Error|string + */ + public function get_resource( $url ) { + + return $this->get_html( $url, true ); + + } + /** * Sets the type attribute for script elements to 'text/javascript', * where Cloudflare prepends a random string to the type attribute. diff --git a/tests/Integration/TestsTrait.php b/tests/Integration/TestsTrait.php index 4bd996d..4becfdd 100644 --- a/tests/Integration/TestsTrait.php +++ b/tests/Integration/TestsTrait.php @@ -6577,7 +6577,7 @@ public function testGetPurchasesPagination() } /** - * Test that get_purchases() returns the expected data. + * Test that get_purchase() returns the expected data. * * @since 1.0.0 * From 2f5dd4c26d9b227dbebbfa6ecd69f75fd634ac10 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Aug 2026 17:34:14 +0800 Subject: [PATCH 14/15] get_html(): Treat 4xx and 5xx errors as a WP_Error --- src/class-convertkit-api-v4.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 73623b7..a5dfc1b 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1339,6 +1339,19 @@ public function get_html( $url, $body_only = true ) { ); } + // Treat any 4xx or 5xx status as an error. + if ( $http_response_code >= 400 ) { + return new WP_Error( + 'convertkit_api_error', + sprintf( + /* translators: %1$s: URL, %2$d: HTTP status code */ + __( 'ConvertKit: Request to %1$s returned HTTP %2$d.', 'convertkit' ), + $url, + (int) $http_response_code + ) + ); + } + // If the HTML is missing the tag, it's likely to be a legacy form. // Wrap it in , and tags now, so we can inject the UTF-8 Content-Type meta tag. if ( strpos( $body, ' Date: Tue, 11 Aug 2026 18:51:09 +0800 Subject: [PATCH 15/15] Reinstate PHP versions for tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index af3d9a1..7f7d157 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,7 +51,7 @@ jobs: max-parallel: 1 matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.0' ] #[ '7.4', '8.0', '8.1', '8.2' ] + php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1', '8.2' ] # Steps to install, configure and run tests steps: