diff --git a/src/ConvertKit_API.php b/src/ConvertKit_API.php index a2a0b0b..fa06d23 100644 --- a/src/ConvertKit_API.php +++ b/src/ConvertKit_API.php @@ -933,6 +933,155 @@ public function get_resources(string $resource) return $_resource; } + /** + * Get subscribers. + * + * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). + * @param string $email_address Search susbcribers by email address. This is an exact match search. + * @param \DateTime $created_after Filter subscribers who have been created after this date. + * @param \DateTime $created_before Filter subscribers who have been created before this date. + * @param \DateTime $updated_after Filter subscribers who have been updated after this date. + * @param \DateTime $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 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.0.0 + * + * @see https://developers.convertkit.com/v4.html#list-subscribers + * + * @return false|mixed + */ + public function get_subscribers( + string $subscriber_state = 'active', + string $email_address = '', + \DateTime $created_after = null, + \DateTime $created_before = null, + \DateTime $updated_after = null, + \DateTime $updated_before = null, + string $sort_field = 'id', + string $sort_order = 'desc', + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + // Build parameters. + $options = []; + + if (!empty($subscriber_state)) { + $options['status'] = $subscriber_state; + } + if (!empty($email_address)) { + $options['email_address'] = $email_address; + } + if (!is_null($created_after)) { + $options['created_after'] = $created_after->format('Y-m-d'); + } + if (!is_null($created_before)) { + $options['created_before'] = $created_before->format('Y-m-d'); + } + if (!is_null($updated_after)) { + $options['updated_after'] = $updated_after->format('Y-m-d'); + } + if (!is_null($updated_before)) { + $options['updated_before'] = $updated_before->format('Y-m-d'); + } + if (!empty($sort_field)) { + $options['sort_field'] = $sort_field; + } + if (!empty($sort_order)) { + $options['sort_order'] = $sort_order; + } + + // Build pagination parameters. + $options = $this->build_pagination_params( + params: $options, + after_cursor: $after_cursor, + before_cursor: $before_cursor, + per_page: $per_page + ); + + // Send request. + return $this->get( + endpoint: 'subscribers', + args: $options + ); + } + + /** + * Create a subscriber. + * + * Behaves as an upsert. If a subscriber with the provided email address does not exist, + * it creates one with the specified first name and state. If a subscriber with the provided + * email address already exists, it updates the first name. + * + * @param string $email_address Email Address. + * @param string $first_name First Name. + * @param string $subscriber_state Subscriber State (active|bounced|cancelled|complained|inactive). + * @param array $fields Custom Fields. + * + * @since 2.0.0 + * + * @see https://developers.convertkit.com/v4.html#create-a-subscriber + * + * @return mixed + */ + public function create_subscriber( + string $email_address, + string $first_name = '', + string $subscriber_state = '', + array $fields = [] + ) { + // Build parameters. + $options = ['email_address' => $email_address]; + + if (!empty($first_name)) { + $options['first_name'] = $first_name; + } + if (!empty($subscriber_state)) { + $options['state'] = $subscriber_state; + } + if (count($fields)) { + $options['fields'] = $fields; + } + + // Send request. + return $this->post( + endpoint: 'subscribers', + args: $options + ); + } + + /** + * Create multiple subscribers. + * + * @param array> $subscribers Subscribers. + * @param string $callback_url URL to notify for large batch size when async processing complete. + * + * @since 2.0.0 + * + * @see https://developers.convertkit.com/v4.html#bulk-create-subscribers + * + * @return mixed + */ + public function create_subscribers(array $subscribers, string $callback_url = '') + { + // Build parameters. + $options = ['subscribers' => $subscribers]; + + if (!empty($callback_url)) { + $options['callback_url'] = $callback_url; + } + + // Send request. + return $this->post( + endpoint: 'bulk/subscribers', + args: $options + ); + } + /** * Get the ConvertKit subscriber ID associated with email address if it exists. * Return false if subscriber not found. @@ -941,27 +1090,18 @@ public function get_resources(string $resource) * * @throws \InvalidArgumentException If the email address is not a valid email format. * - * @see https://developers.convertkit.com/#list-subscribers + * @see https://developers.convertkit.com/v4.html#get-a-subscriber * * @return false|integer */ public function get_subscriber_id(string $email_address) { - if (!filter_var($email_address, FILTER_VALIDATE_EMAIL)) { - throw new \InvalidArgumentException('Email address is not a valid email format.'); - } - $subscribers = $this->get( 'subscribers', ['email_address' => $email_address] ); - if (!$subscribers) { - $this->create_log('No subscribers'); - return false; - } - - if ($subscribers->total_subscribers === 0) { + if (!count($subscribers->subscribers)) { $this->create_log('No subscribers'); return false; } @@ -975,7 +1115,7 @@ public function get_subscriber_id(string $email_address) * * @param integer $subscriber_id Subscriber ID. * - * @see https://developers.convertkit.com/#view-a-single-subscriber + * @see https://developers.convertkit.com/v4.html#get-a-subscriber * * @return false|integer */ @@ -992,7 +1132,7 @@ public function get_subscriber(int $subscriber_id) * @param string $email_address New Email Address. * @param array $fields Updated Custom Fields. * - * @see https://developers.convertkit.com/#update-subscriber + * @see https://developers.convertkit.com/v4.html#update-a-subscriber * * @return false|mixed */ @@ -1023,56 +1163,64 @@ public function update_subscriber( } /** - * Unsubscribe an email address from all forms and sequences. + * Unsubscribe an email address. * * @param string $email Email Address. * - * @see https://developers.convertkit.com/#unsubscribe-subscriber + * @see https://developers.convertkit.com/v4.html#unsubscribe-subscriber * * @return false|object */ public function unsubscribe(string $email) { - return $this->put( - 'unsubscribe', - ['email' => $email] + return $this->post( + sprintf( + 'subscribers/%s/unsubscribe', + $this->get_subscriber_id($email) + ) ); } /** - * Remove subscription from a form + * Unsubscribe the given subscriber ID. * - * @param array $options Array of user data (email). + * @param integer $subscriber_id Subscriber ID. * - * @see https://developers.convertkit.com/#unsubscribe-subscriber + * @see https://developers.convertkit.com/v4.html#unsubscribe-subscriber * * @return false|object */ - public function form_unsubscribe(array $options) + public function unsubscribe_by_id(int $subscriber_id) { - // This function is deprecated in 1.0, as we prefer functions with structured arguments. - // This function name is also misleading, as it doesn't just unsubscribe the email - // address from forms. - trigger_error( - 'form_unsubscribe() is deprecated in 1.0. Use unsubscribe($email) instead.', - E_USER_NOTICE - ); - - return $this->put('unsubscribe', $options); + return $this->post(sprintf('subscribers/%s/unsubscribe', $subscriber_id)); } /** * Get a list of the tags for a subscriber. * * @param integer $subscriber_id Subscriber ID. + * @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.convertkit.com/#list-tags-for-a-subscriber + * @see https://developers.convertkit.com/v4.html#list-tags-for-a-subscriber * * @return false|array */ - public function get_subscriber_tags(int $subscriber_id) - { - return $this->get(sprintf('subscribers/%s/tags', $subscriber_id)); + public function get_subscriber_tags( + int $subscriber_id, + string $after_cursor = '', + string $before_cursor = '', + int $per_page = 100 + ) { + return $this->get( + endpoint: sprintf('subscribers/%s/tags', $subscriber_id), + args: $this->build_pagination_params( + after_cursor: $after_cursor, + before_cursor: $before_cursor, + per_page: $per_page + ) + ); } /** diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index ca2e5c1..8b2b2ff 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -2078,6 +2078,580 @@ public function testAddSubscriberToFormByIDWithInvalidSubscriberID() ); } + /** + * 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 + * 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 bounced. + * + * @since 1.0.0 + * + * @return void + */ + public function testGetSubscribersWithBouncedSubscriberState() + { + $result = $this->api->get_subscribers( + subscriber_state: 'bounced' + ); + + // Assert subscribers and pagination exist. + $this->assertDataExists($result, 'subscribers'); + $this->assertPaginationExists($result); + + // Check the correct subscribers were returned. + $this->assertEquals($result->subscribers[0]->state, 'bounced'); + } + + /** + * 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('2024-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('2024-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 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); + + // 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); + } + + /** + * Test that get_subscribers() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetSubscribersWithInvalidEmailAddress() + { + $this->expectException(ClientException::class); + $result = $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->expectException(ClientException::class); + $result = $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->expectException(ClientException::class); + $result = $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->expectException(ClientException::class); + $result = $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->expectException(ClientException::class); + $result = $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 + ); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); + } + + /** + * 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 + ); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->first_name, $firstName); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); + } + + /** + * 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 + ); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->state, $subscriberState); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); + } + + /** + * 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 + ] + ); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + $this->assertEquals($result->subscriber->fields->last_name, $lastName); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); + } + + /** + * Test that create_subscriber() throws a ClientException when an invalid + * email address is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscriberWithInvalidEmailAddress() + { + $this->expectException(ClientException::class); + $result = $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() + { + $this->expectException(ClientException::class); + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress, + subscriber_state: 'not-a-valid-state' + ); + } + + /** + * 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' + ] + ); + + // Assert subscriber exists with correct data. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); + } + + /** + * Test that create_subscribers() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscribers() + { + $subscribers = [ + [ + 'email_address' => str_replace('@convertkit.com', '-1@convertkit.com', $this->generateEmailAddress()), + ], + [ + 'email_address' => str_replace('@convertkit.com', '-2@convertkit.com', $this->generateEmailAddress()), + ], + ]; + $result = $this->api->create_subscribers($subscribers); + + // 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']); + + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($subscriber->id); + } + } + + /** + * Test that create_subscribers() throws a ClientException when no data is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testCreateSubscribersWithBlankData() + { + $this->expectException(ClientException::class); + $result = $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 get_subscriber_id() returns the expected data. * @@ -2087,8 +2661,6 @@ public function testAddSubscriberToFormByIDWithInvalidSubscriberID() */ public function testGetSubscriberID() { - $this->markTestIncomplete(); - $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']); @@ -2104,9 +2676,7 @@ public function testGetSubscriberID() */ public function testGetSubscriberIDWithInvalidEmailAddress() { - $this->markTestIncomplete(); - - $this->expectException(InvalidArgumentException::class); + $this->expectException(ClientException::class); $result = $this->api->get_subscriber_id('not-an-email-address'); } @@ -2120,8 +2690,6 @@ public function testGetSubscriberIDWithInvalidEmailAddress() */ public function testGetSubscriberIDWithNotSubscribedEmailAddress() { - $this->markTestIncomplete(); - $result = $this->api->get_subscriber_id('not-a-subscriber@test.com'); $this->assertFalse($result); } @@ -2135,16 +2703,11 @@ public function testGetSubscriberIDWithNotSubscribedEmailAddress() */ public function testGetSubscriber() { - $this->markTestIncomplete(); + $result = $this->api->get_subscriber((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $subscriber = $this->api->get_subscriber((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $this->assertInstanceOf('stdClass', $subscriber); - $this->assertArrayHasKey('subscriber', get_object_vars($subscriber)); - $this->assertArrayHasKey('id', get_object_vars($subscriber->subscriber)); - $this->assertEquals( - get_object_vars($subscriber->subscriber)['id'], - (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']); } /** @@ -2157,8 +2720,6 @@ public function testGetSubscriber() */ public function testGetSubscriberWithInvalidSubscriberID() { - $this->markTestIncomplete(); - $this->expectException(ClientException::class); $subscriber = $this->api->get_subscriber(12345); } @@ -2172,13 +2733,11 @@ public function testGetSubscriberWithInvalidSubscriberID() */ public function testUpdateSubscriberWithNoChanges() { - $this->markTestIncomplete(); - $result = $this->api->update_subscriber($_ENV['CONVERTKIT_API_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'], $_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']); } /** @@ -2190,33 +2749,31 @@ public function testUpdateSubscriberWithNoChanges() */ public function testUpdateSubscriberFirstName() { - $this->markTestIncomplete(); - // Add a subscriber. - $email = $this->generateEmailAddress(); - $result = $this->api->add_subscriber_to_sequence( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email: $email + $firstName = 'FirstName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress ); + // Assert subscriber created with no first name. + $this->assertNull($result->subscriber->first_name); + // Get subscriber ID. - $subscriberID = $result->subscription->subscriber->id; + $subscriberID = $result->subscriber->id; // Update subscriber's first name. $result = $this->api->update_subscriber( subscriber_id: $subscriberID, - first_name: 'First Name' + first_name: $firstName ); - // Confirm the change is reflected in the subscriber. - $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'], $subscriberID); - $this->assertEquals(get_object_vars($result->subscriber)['first_name'], 'First Name'); + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->first_name, $firstName); - // Unsubscribe. - $this->api->unsubscribe($email); + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); } /** @@ -2228,17 +2785,17 @@ public function testUpdateSubscriberFirstName() */ public function testUpdateSubscriberEmailAddress() { - $this->markTestIncomplete(); - // Add a subscriber. - $email = $this->generateEmailAddress(); - $result = $this->api->add_subscriber_to_sequence( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email: $email + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress ); + // Assert subscriber created. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + // Get subscriber ID. - $subscriberID = $result->subscription->subscriber->id; + $subscriberID = $result->subscriber->id; // Update subscriber's email address. $newEmail = $this->generateEmailAddress(); @@ -2247,15 +2804,12 @@ public function testUpdateSubscriberEmailAddress() email_address: $newEmail ); - // Confirm the change is reflected in the subscriber. - $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'], $subscriberID); - $this->assertEquals(get_object_vars($result->subscriber)['email_address'], $newEmail); + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->email_address, $newEmail); - // Unsubscribe. - $this->api->unsubscribe($newEmail); + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); } /** @@ -2267,35 +2821,33 @@ public function testUpdateSubscriberEmailAddress() */ public function testUpdateSubscriberCustomFields() { - $this->markTestIncomplete(); - // Add a subscriber. - $email = $this->generateEmailAddress(); - $result = $this->api->add_subscriber_to_sequence( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email: $email + $lastName = 'LastName'; + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress ); + // Assert subscriber created. + $this->assertEquals($result->subscriber->email_address, $emailAddress); + // Get subscriber ID. - $subscriberID = $result->subscription->subscriber->id; + $subscriberID = $result->subscriber->id; - // Update subscriber's email address. + // Update subscriber's custom fields. $result = $this->api->update_subscriber( subscriber_id: $subscriberID, fields: [ - 'last_name' => 'Last Name', + 'last_name' => $lastName, ] ); - // Confirm the change is reflected in the subscriber. - $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'], $subscriberID); - $this->assertEquals($result->subscriber->fields->last_name, 'Last Name'); + // Assert changes were made. + $this->assertEquals($result->subscriber->id, $subscriberID); + $this->assertEquals($result->subscriber->fields->last_name, $lastName); - // Unsubscribe. - $this->api->unsubscribe($email); + // Unsubscribe to cleanup test. + $this->api->unsubscribe_by_id($result->subscriber->id); } /** @@ -2308,8 +2860,6 @@ public function testUpdateSubscriberCustomFields() */ public function testUpdateSubscriberWithInvalidSubscriberID() { - $this->markTestIncomplete(); - $this->expectException(ClientException::class); $subscriber = $this->api->update_subscriber(12345); } @@ -2323,23 +2873,14 @@ public function testUpdateSubscriberWithInvalidSubscriberID() */ public function testUnsubscribe() { - $this->markTestIncomplete(); - // Add a subscriber. - $email = $this->generateEmailAddress(); - $result = $this->api->add_subscriber_to_sequence( - sequence_id: $_ENV['CONVERTKIT_API_SEQUENCE_ID'], - email: $email + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress ); // Unsubscribe. - $result = $this->api->unsubscribe($email); - - // Confirm the change is reflected in the subscriber. - $this->assertInstanceOf('stdClass', $result); - $this->assertArrayHasKey('subscriber', get_object_vars($result)); - $this->assertEquals($result->subscriber->email_address, $email); - $this->assertEquals($result->subscriber->state, 'cancelled'); + $this->assertNull($this->api->unsubscribe($emailAddress)); } /** @@ -2352,8 +2893,6 @@ public function testUnsubscribe() */ public function testUnsubscribeWithNotSubscribedEmailAddress() { - $this->markTestIncomplete(); - $this->expectException(ClientException::class); $subscriber = $this->api->unsubscribe('not-subscribed@convertkit.com'); } @@ -2368,12 +2907,43 @@ public function testUnsubscribeWithNotSubscribedEmailAddress() */ public function testUnsubscribeWithInvalidEmailAddress() { - $this->markTestIncomplete(); - $this->expectException(ClientException::class); $subscriber = $this->api->unsubscribe('invalid-email'); } + /** + * Test that unsubscribe() works with a valid subscriber ID. + * + * @since 2.0.0 + * + * @return void + */ + public function testUnsubscribeByID() + { + // Add a subscriber. + $emailAddress = $this->generateEmailAddress(); + $result = $this->api->create_subscriber( + email_address: $emailAddress + ); + + // Unsubscribe. + $this->assertNull($this->api->unsubscribe_by_id($result->subscriber->id)); + } + + /** + * Test that unsubscribe() throws a ClientException when an invalid + * subscriber ID is specified. + * + * @since 2.0.0 + * + * @return void + */ + public function testUnsubscribeByIDWithInvalidSubscriberID() + { + $this->expectException(ClientException::class); + $subscriber = $this->api->unsubscribe_by_id(12345); + } + /** * Test that get_subscriber_tags() returns the expected data. * @@ -2383,11 +2953,11 @@ public function testUnsubscribeWithInvalidEmailAddress() */ public function testGetSubscriberTags() { - $this->markTestIncomplete(); + $result = $this->api->get_subscriber_tags((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $subscriber = $this->api->get_subscriber_tags((int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID']); - $this->assertInstanceOf('stdClass', $subscriber); - $this->assertArrayHasKey('tags', get_object_vars($subscriber)); + // Assert tags and pagination exist. + $this->assertDataExists($result, 'tags'); + $this->assertPaginationExists($result); } /** @@ -2400,12 +2970,74 @@ public function testGetSubscriberTags() */ public function testGetSubscriberTagsWithInvalidSubscriberID() { - $this->markTestIncomplete(); - $this->expectException(ClientException::class); $subscriber = $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); + + // 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 destroy_broadcast() works * when specifying valid published_at and send_at values.