diff --git a/.env.dist.testing b/.env.dist.testing index f61980a..f69b10f 100644 --- a/.env.dist.testing +++ b/.env.dist.testing @@ -12,3 +12,6 @@ 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" diff --git a/.env.example b/.env.example index a995c61..a8ce412 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,6 @@ 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" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d389cee..1c322ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: # Defines PHP Versions matrix to run tests on strategy: fail-fast: false + max-parallel: 1 matrix: php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] diff --git a/phpcs.xml b/phpcs.xml index 3b059b2..9a763a5 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -72,7 +72,7 @@ - + diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 92c8abd..4ca2f85 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -415,6 +415,156 @@ 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. + * + * @see https://developers.kit.com/api-reference/sequences/get-a-sequence + * + * @return mixed|object + */ + public function get_sequence(int $id) + { + return $this->get(sprintf('sequences/%s', $id)); + } + + /** + * 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 * @@ -512,6 +662,343 @@ public function get_sequence_subscriptions( ); } + /** + * List sequence emails + * + * @param integer $sequence_id Sequence ID. + * @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, + bool $include_total_count = false, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + return $this->get( + sprintf('sequences/%s/emails', $sequence_id), + $this->build_total_count_and_pagination_params( + [], + $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|null $preview_text = null, + string|null $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. + * + * @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) + { + return $this->get(sprintf('sequences/%s/emails/%s', $sequence_id, $email_id)); + } + + /** + * 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|null $subject = null, + int|null $delay_value = null, + string|null $delay_unit = null, + string|null $preview_text = null, + string|null $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|null $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. * @@ -811,6 +1298,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. * @@ -822,6 +1360,7 @@ 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 $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. @@ -842,6 +1381,7 @@ public function get_subscribers( \DateTime|null $updated_before = null, string $sort_field = 'id', string $sort_order = 'desc', + array $include = [], bool $include_total_count = false, string $after_cursor = '', string $before_cursor = '', @@ -874,6 +1414,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( @@ -2067,8 +2610,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 */ diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index 8259adc..2620edb 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -1059,6 +1059,135 @@ public function testGetSequencesPagination() $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->assertEquals(204, $this->api->getResponseInterface()->getStatusCode()); + } + + /** + * 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() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testGetSequenceWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $this->api->get_sequence(12345); + } + + /** + * Test that update_sequence() throws a ClientException when an invalid + * sequence ID is specified. + * + * @since 2.5.0 + * + * @return void + */ + public function testUpdateSequenceWithInvalidSequenceID() + { + $this->expectException(ClientException::class); + $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->expectException(ClientException::class); + $this->api->delete_sequence(12345); + } + /** * Test that add_subscriber_to_sequence_by_email() returns the expected data. * @@ -1472,6 +1601,579 @@ public function testGetSequenceSubscriptionsWithInvalidPagination() ); } + /** + * 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 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->expectException(ClientException::class); + $result = $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: false, + 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(false, $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 + ); + + // 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: 1, + ); + + // 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(1, $result['position']); + + // Delete Sequence Email. + $this->api->delete_sequence_email((int) $_ENV['CONVERTKIT_API_SEQUENCE_ID'], $sequenceEmailID); + $this->assertEquals(204, $this->api->getResponseInterface()->getStatusCode()); + } + + /** + * 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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $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); + } + + /** + * Test that create_snippet() works. + * + * @since 2.5.0 + * + * @return void + */ + public function testCreateSnippet() + { + // Add mock handler for this API request, as the API doesn't provide + // a method to delete snippets to cleanup the test. + $this->api = $this->mockResponse( + api: $this->api, + responseBody: [ + 'snippet' => [ + 'id' => 12345, + 'name' => 'Test Snippet', + 'snippet_type' => 'inline', + 'content' => 'Test Content', + ], + ] + ); + + // Create a snippet. + $result = $this->api->create_snippet( + name: 'Test Snippet', + snippet_type: 'inline', + content: 'Test Content' + ); + $snippetID = $result->snippet->id; + + // Confirm the Snippet saved. + $result = get_object_vars($result->snippet); + $this->assertArrayHasKey('id', $result); + $this->assertEquals('Test Snippet', $result['name']); + $this->assertEquals('inline', $result['snippet_type']); + $this->assertEquals('Test Content', $result['content']); + } + + /** + * 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->expectException(ClientException::class); + $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->expectException(ClientException::class); + $this->api->update_snippet(12345); + } + /** * Test that get_tags() returns the expected data. * @@ -3315,6 +4017,28 @@ public function testGetSubscribersWithSortOrderParam() ); } + /** + * 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. @@ -4344,6 +5068,159 @@ public function testGetEmailTemplatesPagination() $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->expectException(ClientException::class); + $this->api->get_post(12345); + } + /** * Test that get_broadcasts() returns the expected data * when a valid sent_after date is specified.