diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 879bd4e..ea904eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,6 +51,7 @@ jobs: CONVERTKIT_OAUTH_REFRESH_TOKEN=${{ secrets.CONVERTKIT_OAUTH_REFRESH_TOKEN }} CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA=${{ secrets.CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA }} CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA=${{ secrets.CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA }} + CONVERTKIT_API_KEY=${{ secrets.CONVERTKIT_API_KEY }} CONVERTKIT_OAUTH_CLIENT_ID=${{ secrets.CONVERTKIT_OAUTH_CLIENT_ID }} CONVERTKIT_OAUTH_CLIENT_SECRET=${{ secrets.CONVERTKIT_OAUTH_CLIENT_SECRET }} CONVERTKIT_OAUTH_REDIRECT_URI=${{ secrets.CONVERTKIT_OAUTH_REDIRECT_URI }} diff --git a/README.md b/README.md index b2bd177..b8f1ef5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ConvertKit SDK PHP +# Kit SDK PHP -The ConvertKit PHP SDK provides convinient access to the ConvertKit API from applications written in the PHP language. +The Kit PHP SDK provides convinient access to the Kit API from applications written in the PHP language. It includes a pre-defined set of methods for interacting with the API. @@ -10,6 +10,7 @@ It includes a pre-defined set of methods for interacting with the API. |-------------|-------------|--------------------|--------------| | 1.x | v3 | API Key and Secret | 7.4+ | | 2.x | v4 | OAuth | 8.0+ | +| 2.2+ | v4 | API Key | 8.0+ | Refer to [this guide](MIGRATION.md) for changes when upgrading to the v2 SDK. @@ -41,9 +42,9 @@ If you use Composer, these dependencies should be handled automatically. ### 2.x (v4 API, OAuth, PHP 8.0+) -First, register your OAuth application in the `OAuth Applications` section at https://app.convertkit.com/account_settings/advanced_settings. +First, register your OAuth application in the `OAuth Applications` section at https://app.kit.com/account_settings/advanced_settings. -Using the supplied Client ID and secret, redirect the user to ConvertKit to grant your application access to their ConvertKit account. +Using the supplied Client ID and secret, redirect the user to Kit to grant your application access to their Kit account. ```php // Require the autoloader (if you're using a PHP framework, this may already be done for you). @@ -59,7 +60,7 @@ $api = new \ConvertKit_API\ConvertKit_API( header('Location: '.$api->get_oauth_url('')); ``` -Once the user grants your application access to their ConvertKit account, they'll be redirected to your Redirect URI with an authorization code. For example: +Once the user grants your application access to their Kit account, they'll be redirected to your Redirect URI with an authorization code. For example: `your-redirect-uri?code=` @@ -118,26 +119,40 @@ $api = new \ConvertKit_API\ConvertKit_API( API requests may then be performed: ```php -$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@convertkit.com'); +$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); ``` To determine whether a new entity / relationship was created, or an existing entity / relationship updated, inspect the HTTP code of the last request: ```php -$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@convertkit.com'); +$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); $code = $api->getResponseInterface()->getStatusCode(); // 200 OK if e.g. a subscriber already added to the specified form, 201 Created if the subscriber added to the specified form for the first time. ``` The PSR-7 response can be fetched and further inspected, if required - for example, to check if a header exists: ```php -$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@convertkit.com'); +$result = $api->add_subscriber_to_form(12345, 'joe.bloggs@kit.com'); $api->getResponseInterface()->hasHeader('Content-Length'); // Check if the last API request included a `Content-Length` header ``` +### 2.2+ (v4 API, API Key, PHP 8.0+) + +Get your Kit API Key and API Secret [here](https://app.kit.com/account_settings/developer_settings) and set it somewhere in your application. + +```php +// Require the autoloader (if you're using a PHP framework, this may already be done for you). +require_once 'vendor/autoload.php'; + +// Initialize the API class. +$api = new \ConvertKit_API\ConvertKit_API( + apiKey: '' +); +``` + ### 1.x (v3 API, API Key and Secret, PHP 7.4+) -Get your ConvertKit API Key and API Secret [here](https://app.convertkit.com/account/edit) and set it somewhere in your application. +Get your Kit API Key and API Secret [here](https://app.kit.com/account_settings/developer_settings) and set it somewhere in your application. ```php // Require the autoloader (if you're using a PHP framework, this may already be done for you). @@ -149,7 +164,7 @@ $api = new \ConvertKit_API\ConvertKit_API('', ' tests + tests/ConvertKitAPITest.php diff --git a/src/ConvertKit_API.php b/src/ConvertKit_API.php index 6fcd9d3..9a8e1b9 100644 --- a/src/ConvertKit_API.php +++ b/src/ConvertKit_API.php @@ -62,19 +62,22 @@ class ConvertKit_API * @param string $clientID OAuth Client ID. * @param string $clientSecret OAuth Client Secret. * @param string $accessToken OAuth Access Token. + * @param string $apiKey API Key. * @param boolean $debug Log requests to debugger. * @param string $debugLogFileLocation Path and filename of debug file to write to. */ public function __construct( - string $clientID, - string $clientSecret, + string $clientID = '', + string $clientSecret = '', string $accessToken = '', + string $apiKey = '', bool $debug = false, string $debugLogFileLocation = '' ) { $this->client_id = $clientID; $this->client_secret = $clientSecret; $this->access_token = $accessToken; + $this->api_key = $apiKey; $this->debug = $debug; // Set the Guzzle client. @@ -122,22 +125,35 @@ private function create_log(string $message) return; } - // Mask the Client ID, Client Secret and Access Token. - $message = str_replace( - $this->client_id, - str_repeat('*', (strlen($this->client_id) - 4)) . substr($this->client_id, - 4), - $message - ); - $message = str_replace( - $this->client_secret, - str_repeat('*', (strlen($this->client_secret) - 4)) . substr($this->client_secret, - 4), - $message - ); - $message = str_replace( - $this->access_token, - str_repeat('*', (strlen($this->access_token) - 4)) . substr($this->access_token, - 4), - $message - ); + // Mask the Client ID, Client Secret, Access Token, and API Key. + if ($this->client_id) { + $message = str_replace( + $this->client_id, + str_repeat('*', (strlen($this->client_id) - 4)) . substr($this->client_id, - 4), + $message + ); + } + if ($this->client_secret) { + $message = str_replace( + $this->client_secret, + str_repeat('*', (strlen($this->client_secret) - 4)) . substr($this->client_secret, - 4), + $message + ); + } + if ($this->access_token) { + $message = str_replace( + $this->access_token, + str_repeat('*', (strlen($this->access_token) - 4)) . substr($this->access_token, - 4), + $message + ); + } + if ($this->api_key) { + $message = str_replace( + $this->api_key, + str_repeat('*', (strlen($this->api_key) - 4)) . substr($this->api_key, - 4), + $message + ); + } // Mask email addresses that may be contained within the message. $message = preg_replace_callback( @@ -427,7 +443,12 @@ public function get_request_headers(string $type = 'application/json', bool $aut } // Add authorization header and return. - $headers['Authorization'] = 'Bearer ' . $this->access_token; + if ($this->api_key) { + $headers['X-Kit-Api-Key'] = $this->api_key; + } else if ($this->access_token) { + $headers['Authorization'] = 'Bearer ' . $this->access_token; + } + return $headers; } diff --git a/src/ConvertKit_API_Traits.php b/src/ConvertKit_API_Traits.php index 7d72d43..71f2175 100644 --- a/src/ConvertKit_API_Traits.php +++ b/src/ConvertKit_API_Traits.php @@ -33,6 +33,13 @@ trait ConvertKit_API_Traits */ protected $access_token = ''; + /** + * API Key + * + * @var string + */ + protected $api_key = ''; + /** * OAuth Authorization URL * diff --git a/tests/ConvertKitAPIKeyTest.php b/tests/ConvertKitAPIKeyTest.php new file mode 100644 index 0000000..6558399 --- /dev/null +++ b/tests/ConvertKitAPIKeyTest.php @@ -0,0 +1,759 @@ +load(); + + // Set location where API class will create/write the log file. + $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug.log'; + + // Delete any existing debug log file. + $this->deleteLogFile(); + + // Setup API instances. + $this->api = new ConvertKit_API( + apiKey: $_ENV['CONVERTKIT_API_KEY'] + ); + + // Wait a second to avoid hitting a 429 rate limit. + sleep(1); + } + + /** + * Test that debug logging works when enabled and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugEnabled() + { + // Setup API with debugging enabled. + $api = new ConvertKit_API( + apiKey: $_ENV['CONVERTKIT_API_KEY'], + debug: true + ); + + $result = $api->get_account(); + + // Confirm that the log includes expected data. + $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); + $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); + } + + /** + * Test that debug logging works when enabled, a custom debug log file and path is specified + * and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugEnabledWithCustomLogFile() + { + // Define custom log file location. + $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug-custom.log'; + + // Setup API with debugging enabled. + $api = new ConvertKit_API( + apiKey: $_ENV['CONVERTKIT_API_KEY'], + debug: true, + debugLogFileLocation: $this->logFile + ); + $result = $api->get_account(); + + // Confirm log file exists. + $this->assertFileExists($this->logFile); + + // Confirm that the log includes expected data. + $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); + $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); + } + + /** + * Test that debug logging works when enabled and an API call is made, with email addresses and credentials + * masked in the log file. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugCredentialsAndEmailsAreMasked() + { + // Setup API with debugging enabled. + $api = new ConvertKit_API( + apiKey: $_ENV['CONVERTKIT_API_KEY'], + debug: true + ); + + // Create log entries with API Key and Email Address, as if an API method + // were to log this sensitive data. + $this->callPrivateMethod($api, 'create_log', ['API Key: ' . $_ENV['CONVERTKIT_API_KEY']]); + $this->callPrivateMethod($api, 'create_log', ['Email: ' . $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']]); + + // Confirm that the log includes the masked API Key and Email Address. + $this->assertStringContainsString( + str_repeat( + '*', + (strlen($_ENV['CONVERTKIT_API_KEY']) - 4) + ) . substr($_ENV['CONVERTKIT_API_KEY'], -4), + $this->getLogFileContents() + ); + $this->assertStringContainsString( + 'o****@n********.c**', + $this->getLogFileContents() + ); + + // Confirm that the log does not include the unmasked API Key or Email Address. + $this->assertStringNotContainsString($_ENV['CONVERTKIT_API_KEY'], $this->getLogFileContents()); + $this->assertStringNotContainsString($_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], $this->getLogFileContents()); + } + + /** + * Test that debug logging is not performed when disabled and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugDisabled() + { + $result = $this->api->get_account(); + $this->assertEmpty($this->getLogFileContents()); + } + + /** + * Test that calling request_headers() returns the expected array of headers + * + * @since 2.2.0 + * + * @return void + */ + public function testRequestHeadersMethod() + { + $headers = $this->api->get_request_headers(); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayHasKey('X-Kit-Api-Key', $headers); + $this->assertEquals($headers['Accept'], 'application/json'); + $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + $this->assertEquals($headers['X-Kit-Api-Key'], $_ENV['CONVERTKIT_API_KEY']); + } + + /** + * Test that calling request_headers() with a different `type` parameter + * returns the expected array of headers + * + * @since 2.2.0 + * + * @return void + */ + public function testRequestHeadersMethodWithType() + { + $headers = $this->api->get_request_headers( + type: 'text/html' + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayHasKey('X-Kit-Api-Key', $headers); + $this->assertEquals($headers['Accept'], 'text/html'); + $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + $this->assertEquals($headers['X-Kit-Api-Key'], $_ENV['CONVERTKIT_API_KEY']); + } + + /** + * Test that calling request_headers() with the `auth` parameter set to false + * returns the expected array of headers + * + * @since 2.2.0 + * + * @return void + */ + public function testRequestHeadersMethodWithAuthDisabled() + { + $headers = $this->api->get_request_headers( + auth: false + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayNotHasKey('X-Kit-Api-Key', $headers); + $this->assertEquals($headers['Accept'], 'application/json'); + $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + } + + /** + * Test that calling request_headers() with a different `type` parameter + * and the `auth` parameter set to false returns the expected array of headers + * + * @since 2.2.0 + * + * @return void + */ + public function testRequestHeadersMethodWithTypeAndAuthDisabled() + { + $headers = $this->api->get_request_headers( + type: 'text/html', + auth: false + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayNotHasKey('X-Kit-Api-Key', $headers); + $this->assertEquals($headers['Accept'], 'text/html'); + $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + } + + /** + * Test that create_tags() throws a ClientException when attempting + * to create tags, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateTags() + { + $this->expectException(ClientException::class); + $result = $this->api->create_tags([ + 'Tag Test ' . mt_rand(), + 'Tag Test ' . mt_rand(), + ]); + } + + /** + * Test that create_tags() throws a ClientException when attempting + * to create blank tags, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateTagsBlank() + { + $this->expectException(ClientException::class); + $result = $this->api->create_tags([ + '', + '', + ]); + } + + /** + * Test that create_tags() throws a ClientException when creating + * tags that already exists, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateTagsThatExist() + { + $this->expectException(ClientException::class); + $result = $this->api->create_tags( + [ + $_ENV['CONVERTKIT_API_TAG_NAME'], + $_ENV['CONVERTKIT_API_TAG_NAME_2'], + ] + ); + } + + /** + * Test that add_subscribers_to_forms() throws a ClientException when + * attempting to add subscribers to forms, as this is only supported + * using OAuth. + * + * @since 2.2.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; + + $this->expectException(ClientException::class); + + // 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, + ], + ] + ); + } + + /** + * Test that add_subscribers_to_forms() returns a ClientException + * when a referrer URL is specified, as this is only supported + * using OAuth. + * + * @since 2.2.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; + + $this->expectException(ClientException::class); + + // 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/', + ], + ] + ); + } + + /** + * Test that add_subscribers_to_forms() returns a ClientException + * when a referrer URL with UTM parameters is specified, as this is only + * supported using OAuth. + * + * @since 2.2.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; + + $this->expectException(ClientException::class); + + // 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, + ], + ] + ); + } + + /** + * Test that add_subscribers_to_forms() returns a ClientException + * when invalid Form IDs are specified, as this is only supported + * using OAuth. + * + * @since 2.2.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; + + $this->expectException(ClientException::class); + + // 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, + ], + ] + ); + } + + /** + * Test that add_subscribers_to_forms() returns a ClientException + * when invalid Subscriber IDs are specified, as this is only supported + * + * @since 2.2.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; + + $this->expectException(ClientException::class); + + // 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, + ], + ] + ); + } + + /** + * Test that create_subscribers() returns a ClientException + * when attempting to create subscribers, as this is only supported + * using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateSubscribers() + { + $this->expectException(ClientException::class); + $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); + } + + /** + * Test that create_subscribers() throws a ClientException when no data is specified. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateSubscribersWithBlankData() + { + $this->expectException(ClientException::class); + $result = $this->api->create_subscribers([ + [], + ]); + } + + /** + * Test that create_subscribers() throws a ClientException when invalid email addresses + * are specified, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateSubscribersWithInvalidEmailAddresses() + { + $this->expectException(ClientException::class); + $subscribers = [ + [ + 'email_address' => 'not-an-email-address', + ], + [ + 'email_address' => 'not-an-email-address-again', + ], + ]; + $result = $this->api->create_subscribers($subscribers); + } + + /** + * Test that create_custom_fields() throws a ClientException + * as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreateCustomFields() + { + $this->expectException(ClientException::class); + $labels = [ + 'Custom Field ' . mt_rand(), + 'Custom Field ' . mt_rand(), + ]; + $result = $this->api->create_custom_fields($labels); + } + + /** + * Test that get_purchases() throws a ClientException + * as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testGetPurchases() + { + $this->expectException(ClientException::class); + $result = $this->api->get_purchases(); + } + + /** + * Test that get_purchases() throws a ClientException + * when the total count is included, as this is only + * supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testGetPurchasesWithTotalCount() + { + $this->expectException(ClientException::class); + $result = $this->api->get_purchases( + include_total_count: true + ); + } + + /** + * Test that get_purchases() throws a ClientException + * when pagination parameters and per_page limits are specified, + * as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testGetPurchasesPagination() + { + $this->expectException(ClientException::class); + $result = $this->api->get_purchases( + per_page: 1 + ); + } + + /** + * Test that get_purchases() throws a ClientException + * when a purchase ID is specified, as this is only + * supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testGetPurchase() + { + $this->expectException(ClientException::class); + $result = $this->api->get_purchase(12345); + } + + /** + * Test that get_purchases() throws a ClientException when an invalid + * purchase ID is specified, as this is only supported + * using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testGetPurchaseWithInvalidID() + { + $this->expectException(ClientException::class); + $this->api->get_purchase(12345); + } + + /** + * Test that create_purchase() throws a ClientException + * as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreatePurchase() + { + $this->expectException(ClientException::class); + $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'), + ); + } + + /** + * Test that create_purchase() throws a ClientException when an invalid + * email address is specified, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreatePurchaseWithInvalidEmailAddress() + { + $this->expectException(ClientException::class); + $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, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreatePurchaseWithBlankTransactionID() + { + $this->expectException(ClientException::class); + $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, as this is only supported using OAuth. + * + * @since 2.2.0 + * + * @return void + */ + public function testCreatePurchaseWithNoProducts() + { + $this->expectException(ClientException::class); + $this->api->create_purchase( + email_address: $this->generateEmailAddress(), + transaction_id: str_shuffle('wfervdrtgsdewrafvwefds'), + currency: 'usd', + products: [], + ); + } +} diff --git a/tests/ConvertKitAPIOAuthTest.php b/tests/ConvertKitAPIOAuthTest.php new file mode 100644 index 0000000..829d0a7 --- /dev/null +++ b/tests/ConvertKitAPIOAuthTest.php @@ -0,0 +1,457 @@ +load(); + + // Set location where API class will create/write the log file. + $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug.log'; + + // Delete any existing debug log file. + $this->deleteLogFile(); + + // Setup API. + $this->api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] + ); + + // Wait a second to avoid hitting a 429 rate limit. + sleep(1); + } + + /** + * Test that debug logging works when enabled and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugEnabled() + { + // Setup API with debugging enabled. + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + debug: true + ); + $result = $api->get_account(); + + // Confirm that the log includes expected data. + $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); + $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); + } + + /** + * Test that debug logging works when enabled, a custom debug log file and path is specified + * and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugEnabledWithCustomLogFile() + { + // Define custom log file location. + $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug-custom.log'; + + // Setup API with debugging enabled. + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + debug: true, + debugLogFileLocation: $this->logFile + ); + $result = $api->get_account(); + + // Confirm log file exists. + $this->assertFileExists($this->logFile); + + // Confirm that the log includes expected data. + $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); + $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); + } + + /** + * Test that debug logging works when enabled and an API call is made, with email addresses and credentials + * masked in the log file. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugCredentialsAndEmailsAreMasked() + { + // Setup API with debugging enabled. + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + debug: true + ); + + // Create log entries with Client ID, Client Secret, Access Token and Email Address, as if an API method + // were to log this sensitive data. + $this->callPrivateMethod($api, 'create_log', ['Client ID: ' . $_ENV['CONVERTKIT_OAUTH_CLIENT_ID']]); + $this->callPrivateMethod($api, 'create_log', ['Client Secret: ' . $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET']]); + $this->callPrivateMethod($api, 'create_log', ['Access Token: ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']]); + $this->callPrivateMethod($api, 'create_log', ['Email: ' . $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']]); + + // Confirm that the log includes the masked Client ID, Secret, Access Token and Email Address. + $this->assertStringContainsString( + str_repeat( + '*', + (strlen($_ENV['CONVERTKIT_OAUTH_CLIENT_ID']) - 4) + ) . substr($_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], -4), + $this->getLogFileContents() + ); + $this->assertStringContainsString( + str_repeat( + '*', + (strlen($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET']) - 4) + ) . substr($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], -4), + $this->getLogFileContents() + ); + $this->assertStringContainsString( + str_repeat( + '*', + (strlen($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']) - 4) + ) . substr($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], -4), + $this->getLogFileContents() + ); + $this->assertStringContainsString( + 'o****@n********.c**', + $this->getLogFileContents() + ); + + // Confirm that the log does not include the unmasked Client ID, Secret, Access Token or Email Address. + $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], $this->getLogFileContents()); + $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], $this->getLogFileContents()); + $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], $this->getLogFileContents()); + $this->assertStringNotContainsString($_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], $this->getLogFileContents()); + } + + /** + * Test that debug logging is not performed when disabled and an API call is made. + * + * @since 2.2.0 + * + * @return void + */ + public function testDebugDisabled() + { + $result = $this->api->get_account(); + $this->assertEmpty($this->getLogFileContents()); + } + + /** + * Test that calling request_headers() returns the expected array of headers + * + * @since 2.0.0 + * + * @return void + */ + public function testRequestHeadersMethod() + { + $headers = $this->api->get_request_headers(); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayHasKey('Authorization', $headers); + $this->assertEquals($headers['Accept'], 'application/json'); + $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + $this->assertEquals($headers['Authorization'], 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']); + } + + /** + * Test that calling request_headers() with a different `type` parameter + * returns the expected array of headers + * + * @since 2.0.0 + * + * @return void + */ + public function testRequestHeadersMethodWithType() + { + $headers = $this->api->get_request_headers( + type: 'text/html' + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayHasKey('Authorization', $headers); + $this->assertEquals($headers['Accept'], 'text/html'); + $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + $this->assertEquals($headers['Authorization'], 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']); + } + + /** + * Test that calling request_headers() with the `auth` parameter set to false + * returns the expected array of headers + * + * @since 2.0.0 + * + * @return void + */ + public function testRequestHeadersMethodWithAuthDisabled() + { + $headers = $this->api->get_request_headers( + auth: false + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayNotHasKey('Authorization', $headers); + $this->assertEquals($headers['Accept'], 'application/json'); + $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + } + + /** + * Test that calling request_headers() with a different `type` parameter + * and the `auth` parameter set to false returns the expected array of headers + * + * @since 2.0.0 + * + * @return void + */ + public function testRequestHeadersMethodWithTypeAndAuthDisabled() + { + $headers = $this->api->get_request_headers( + type: 'text/html', + auth: false + ); + $this->assertArrayHasKey('Accept', $headers); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertArrayHasKey('User-Agent', $headers); + $this->assertArrayNotHasKey('Authorization', $headers); + $this->assertEquals($headers['Accept'], 'text/html'); + $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); + $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); + } + + /** + * Test that get_oauth_url() returns the correct URL to begin the OAuth process. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetOAuthURL() + { + // Confirm the OAuth URL returned is correct. + $this->assertEquals( + $this->api->get_oauth_url($_ENV['CONVERTKIT_OAUTH_REDIRECT_URI']), + 'https://app.convertkit.com/oauth/authorize?' . http_build_query([ + 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + 'redirect_uri' => $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + 'response_type' => 'code', + ]) + ); + } + + /** + * Test that get_access_token() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetAccessToken() + { + // Initialize API. + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] + ); + + // Define response parameters. + $params = [ + 'access_token' => 'example-access-token', + 'refresh_token' => 'example-refresh-token', + 'token_type' => 'Bearer', + 'created_at' => strtotime('now'), + 'expires_in' => strtotime('+3 days'), + 'scope' => 'public', + ]; + + // Add mock handler for this API request. + $api = $this->mockResponse( + api: $api, + responseBody: $params, + ); + + // Send request. + $result = $api->get_access_token( + authCode: 'auth-code', + redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + ); + + // Inspect response. + $result = get_object_vars($result); + $this->assertIsArray($result); + $this->assertArrayHasKey('access_token', $result); + $this->assertArrayHasKey('refresh_token', $result); + $this->assertArrayHasKey('token_type', $result); + $this->assertArrayHasKey('created_at', $result); + $this->assertArrayHasKey('expires_in', $result); + $this->assertArrayHasKey('scope', $result); + $this->assertEquals($result['access_token'], $params['access_token']); + $this->assertEquals($result['refresh_token'], $params['refresh_token']); + $this->assertEquals($result['created_at'], $params['created_at']); + $this->assertEquals($result['expires_in'], $params['expires_in']); + } + + /** + * Test that a ClientException is thrown when an invalid auth code is supplied + * when fetching an access token. + * + * @since 2.0.0 + * + * @return void + */ + public function testGetAccessTokenWithInvalidAuthCode() + { + $this->expectException(ClientException::class); + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] + ); + $result = $api->get_access_token( + authCode: 'not-a-real-auth-code', + redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + ); + } + + /** + * Test that refresh_token() returns the expected data. + * + * @since 2.0.0 + * + * @return void + */ + public function testRefreshToken() + { + // Initialize API. + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] + ); + + // Define response parameters. + $params = [ + 'access_token' => 'new-example-access-token', + 'refresh_token' => 'new-example-refresh-token', + 'token_type' => 'Bearer', + 'created_at' => strtotime('now'), + 'expires_in' => strtotime('+3 days'), + 'scope' => 'public', + ]; + + // Add mock handler for this API request. + $api = $this->mockResponse( + api: $api, + responseBody: $params, + ); + + // Send request. + $result = $api->refresh_token( + refreshToken: 'refresh-token', + redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + ); + + // Inspect response. + $result = get_object_vars($result); + $this->assertIsArray($result); + $this->assertArrayHasKey('access_token', $result); + $this->assertArrayHasKey('refresh_token', $result); + $this->assertArrayHasKey('token_type', $result); + $this->assertArrayHasKey('created_at', $result); + $this->assertArrayHasKey('expires_in', $result); + $this->assertArrayHasKey('scope', $result); + $this->assertEquals($result['access_token'], $params['access_token']); + $this->assertEquals($result['refresh_token'], $params['refresh_token']); + $this->assertEquals($result['created_at'], $params['created_at']); + $this->assertEquals($result['expires_in'], $params['expires_in']); + } + + /** + * Test that a ServerException is thrown when an invalid refresh token is supplied + * when refreshing an access token. + * + * @since 2.0.0 + * + * @return void + */ + public function testRefreshTokenWithInvalidToken() + { + $this->expectException(ServerException::class); + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] + ); + $result = $api->refresh_token( + refreshToken: 'not-a-real-refresh-token', + redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], + ); + } + + /** + * Test that a ClientException is thrown when an invalid access token is supplied. + * + * @since 1.0.0 + * + * @return void + */ + public function testInvalidAPICredentials() + { + $this->expectException(ClientException::class); + $api = new ConvertKit_API( + clientID: 'fakeClientID', + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] + ); + $result = $api->get_account(); + + $this->expectException(ClientException::class); + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: 'fakeClientSecret', + accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] + ); + $result = $api->get_account(); + + $this->expectException(ClientException::class); + $api = new ConvertKit_API( + clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], + accessToken: 'fakeAccessToken' + ); + $result = $api->get_account(); + } +} diff --git a/tests/ConvertKitAPITest.php b/tests/ConvertKitAPITest.php index b425d13..786d85e 100644 --- a/tests/ConvertKitAPITest.php +++ b/tests/ConvertKitAPITest.php @@ -69,37 +69,6 @@ class ConvertKitAPITest extends TestCase */ protected $broadcast_ids = []; - /** - * Load .env configuration into $_ENV superglobal, and initialize the API - * class before each test. - * - * @since 1.0.0 - * - * @return void - */ - protected function setUp(): void - { - // Load environment credentials from root folder. - $dotenv = Dotenv::createImmutable(dirname(dirname(__FILE__))); - $dotenv->load(); - - // Set location where API class will create/write the log file. - $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug.log'; - - // Delete any existing debug log file. - $this->deleteLogFile(); - - // Setup API. - $this->api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] - ); - - // Wait a second to avoid hitting a 429 rate limit. - sleep(1); - } - /** * Cleanup data from the ConvertKit account on a test pass/fail, such as unsubscribing, deleting custom fields etc * @@ -195,417 +164,6 @@ public function testClientInterfaceInjection() $this->assertEquals(200, $this->api->getResponseInterface()->getStatusCode()); } - /** - * Test that debug logging works when enabled and an API call is made. - * - * @since 1.2.0 - * - * @return void - */ - public function testDebugEnabled() - { - // Setup API with debugging enabled. - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], - debug: true - ); - $result = $api->get_account(); - - // Confirm that the log includes expected data. - $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); - $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); - } - - /** - * Test that debug logging works when enabled, a custom debug log file and path is specified - * and an API call is made. - * - * @since 1.3.0 - * - * @return void - */ - public function testDebugEnabledWithCustomLogFile() - { - // Define custom log file location. - $this->logFile = dirname(dirname(__FILE__)) . '/src/logs/debug-custom.log'; - - // Setup API with debugging enabled. - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], - debug: true, - debugLogFileLocation: $this->logFile - ); - $result = $api->get_account(); - - // Confirm log file exists. - $this->assertFileExists($this->logFile); - - // Confirm that the log includes expected data. - $this->assertStringContainsString('ck-debug.INFO: GET account', $this->getLogFileContents()); - $this->assertStringContainsString('ck-debug.INFO: Finish request successfully', $this->getLogFileContents()); - } - - /** - * Test that debug logging works when enabled and an API call is made, with email addresses and credentials - * masked in the log file. - * - * @since 2.0.0 - * - * @return void - */ - public function testDebugCredentialsAndEmailsAreMasked() - { - // Setup API with debugging enabled. - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], - debug: true - ); - - // Create log entries with Client ID, Client Secret, Access Token and Email Address, as if an API method - // were to log this sensitive data. - $this->callPrivateMethod($api, 'create_log', ['Client ID: ' . $_ENV['CONVERTKIT_OAUTH_CLIENT_ID']]); - $this->callPrivateMethod($api, 'create_log', ['Client Secret: ' . $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET']]); - $this->callPrivateMethod($api, 'create_log', ['Access Token: ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']]); - $this->callPrivateMethod($api, 'create_log', ['Email: ' . $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL']]); - - // Confirm that the log includes the masked Client ID, Secret, Access Token and Email Address. - $this->assertStringContainsString( - str_repeat( - '*', - (strlen($_ENV['CONVERTKIT_OAUTH_CLIENT_ID']) - 4) - ) . substr($_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], -4), - $this->getLogFileContents() - ); - $this->assertStringContainsString( - str_repeat( - '*', - (strlen($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET']) - 4) - ) . substr($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], -4), - $this->getLogFileContents() - ); - $this->assertStringContainsString( - str_repeat( - '*', - (strlen($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']) - 4) - ) . substr($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], -4), - $this->getLogFileContents() - ); - $this->assertStringContainsString( - 'o****@n********.c**', - $this->getLogFileContents() - ); - - // Confirm that the log does not include the unmasked Client ID, Secret, Access Token or Email Address. - $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], $this->getLogFileContents()); - $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], $this->getLogFileContents()); - $this->assertStringNotContainsString($_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], $this->getLogFileContents()); - $this->assertStringNotContainsString($_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], $this->getLogFileContents()); - } - - /** - * Test that debug logging is not performed when disabled and an API call is made. - * - * @since 1.2.0 - * - * @return void - */ - public function testDebugDisabled() - { - $result = $this->api->get_account(); - $this->assertEmpty($this->getLogFileContents()); - } - - /** - * Test that calling request_headers() returns the expected array of headers - * - * @since 2.0.0 - * - * @return void - */ - public function testRequestHeadersMethod() - { - $headers = $this->api->get_request_headers(); - $this->assertArrayHasKey('Accept', $headers); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertArrayHasKey('User-Agent', $headers); - $this->assertArrayHasKey('Authorization', $headers); - $this->assertEquals($headers['Accept'], 'application/json'); - $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); - $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); - $this->assertEquals($headers['Authorization'], 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']); - } - - /** - * Test that calling request_headers() with a different `type` parameter - * returns the expected array of headers - * - * @since 2.0.0 - * - * @return void - */ - public function testRequestHeadersMethodWithType() - { - $headers = $this->api->get_request_headers( - type: 'text/html' - ); - $this->assertArrayHasKey('Accept', $headers); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertArrayHasKey('User-Agent', $headers); - $this->assertArrayHasKey('Authorization', $headers); - $this->assertEquals($headers['Accept'], 'text/html'); - $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); - $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); - $this->assertEquals($headers['Authorization'], 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN']); - } - - /** - * Test that calling request_headers() with the `auth` parameter set to false - * returns the expected array of headers - * - * @since 2.0.0 - * - * @return void - */ - public function testRequestHeadersMethodWithAuthDisabled() - { - $headers = $this->api->get_request_headers( - auth: false - ); - $this->assertArrayHasKey('Accept', $headers); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertArrayHasKey('User-Agent', $headers); - $this->assertArrayNotHasKey('Authorization', $headers); - $this->assertEquals($headers['Accept'], 'application/json'); - $this->assertEquals($headers['Content-Type'], 'application/json; charset=utf-8'); - $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); - } - - /** - * Test that calling request_headers() with a different `type` parameter - * and the `auth` parameter set to false returns the expected array of headers - * - * @since 2.0.0 - * - * @return void - */ - public function testRequestHeadersMethodWithTypeAndAuthDisabled() - { - $headers = $this->api->get_request_headers( - type: 'text/html', - auth: false - ); - $this->assertArrayHasKey('Accept', $headers); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertArrayHasKey('User-Agent', $headers); - $this->assertArrayNotHasKey('Authorization', $headers); - $this->assertEquals($headers['Accept'], 'text/html'); - $this->assertEquals($headers['Content-Type'], 'text/html; charset=utf-8'); - $this->assertEquals($headers['User-Agent'], 'ConvertKitPHPSDK/' . $this->api::VERSION . ';PHP/' . phpversion()); - } - - /** - * Test that get_oauth_url() returns the correct URL to begin the OAuth process. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetOAuthURL() - { - // Confirm the OAuth URL returned is correct. - $this->assertEquals( - $this->api->get_oauth_url($_ENV['CONVERTKIT_OAUTH_REDIRECT_URI']), - 'https://app.convertkit.com/oauth/authorize?' . http_build_query([ - 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - 'redirect_uri' => $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], - 'response_type' => 'code', - ]) - ); - } - - /** - * Test that get_access_token() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetAccessToken() - { - // Initialize API. - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] - ); - - // Define response parameters. - $params = [ - 'access_token' => 'example-access-token', - 'refresh_token' => 'example-refresh-token', - 'token_type' => 'Bearer', - 'created_at' => strtotime('now'), - 'expires_in' => strtotime('+3 days'), - 'scope' => 'public', - ]; - - // Add mock handler for this API request. - $api = $this->mockResponse( - api: $api, - responseBody: $params, - ); - - // Send request. - $result = $api->get_access_token( - authCode: 'auth-code', - redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], - ); - - // Inspect response. - $result = get_object_vars($result); - $this->assertIsArray($result); - $this->assertArrayHasKey('access_token', $result); - $this->assertArrayHasKey('refresh_token', $result); - $this->assertArrayHasKey('token_type', $result); - $this->assertArrayHasKey('created_at', $result); - $this->assertArrayHasKey('expires_in', $result); - $this->assertArrayHasKey('scope', $result); - $this->assertEquals($result['access_token'], $params['access_token']); - $this->assertEquals($result['refresh_token'], $params['refresh_token']); - $this->assertEquals($result['created_at'], $params['created_at']); - $this->assertEquals($result['expires_in'], $params['expires_in']); - } - - /** - * Test that a ClientException is thrown when an invalid auth code is supplied - * when fetching an access token. - * - * @since 2.0.0 - * - * @return void - */ - public function testGetAccessTokenWithInvalidAuthCode() - { - $this->expectException(ClientException::class); - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] - ); - $result = $api->get_access_token( - authCode: 'not-a-real-auth-code', - redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], - ); - } - - /** - * Test that refresh_token() returns the expected data. - * - * @since 2.0.0 - * - * @return void - */ - public function testRefreshToken() - { - // Initialize API. - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] - ); - - // Define response parameters. - $params = [ - 'access_token' => 'new-example-access-token', - 'refresh_token' => 'new-example-refresh-token', - 'token_type' => 'Bearer', - 'created_at' => strtotime('now'), - 'expires_in' => strtotime('+3 days'), - 'scope' => 'public', - ]; - - // Add mock handler for this API request. - $api = $this->mockResponse( - api: $api, - responseBody: $params, - ); - - // Send request. - $result = $api->refresh_token( - refreshToken: 'refresh-token', - redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], - ); - - // Inspect response. - $result = get_object_vars($result); - $this->assertIsArray($result); - $this->assertArrayHasKey('access_token', $result); - $this->assertArrayHasKey('refresh_token', $result); - $this->assertArrayHasKey('token_type', $result); - $this->assertArrayHasKey('created_at', $result); - $this->assertArrayHasKey('expires_in', $result); - $this->assertArrayHasKey('scope', $result); - $this->assertEquals($result['access_token'], $params['access_token']); - $this->assertEquals($result['refresh_token'], $params['refresh_token']); - $this->assertEquals($result['created_at'], $params['created_at']); - $this->assertEquals($result['expires_in'], $params['expires_in']); - } - - /** - * Test that a ServerException is thrown when an invalid refresh token is supplied - * when refreshing an access token. - * - * @since 2.0.0 - * - * @return void - */ - public function testRefreshTokenWithInvalidToken() - { - $this->expectException(ServerException::class); - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'] - ); - $result = $api->refresh_token( - refreshToken: 'not-a-real-refresh-token', - redirectURI: $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], - ); - } - - /** - * Test that a ClientException is thrown when an invalid access token is supplied. - * - * @since 1.0.0 - * - * @return void - */ - public function testInvalidAPICredentials() - { - $this->expectException(ClientException::class); - $api = new ConvertKit_API( - clientID: 'fakeClientID', - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] - ); - $result = $api->get_account(); - - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: 'fakeClientSecret', - accessToken: $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'] - ); - $result = $api->get_account(); - - $api = new ConvertKit_API( - clientID: $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], - clientSecret: $_ENV['CONVERTKIT_OAUTH_CLIENT_SECRET'], - accessToken: 'fakeAccessToken' - ); - $result = $api->get_account(); - } /** * Test that get_account() returns the expected data. @@ -5523,7 +5081,7 @@ public function testGetResourceInaccessibleURL() * * @return void */ - private function deleteLogFile() + public function deleteLogFile() { if (file_exists($this->logFile)) { unlink($this->logFile); @@ -5537,7 +5095,7 @@ private function deleteLogFile() * * @return string */ - private function getLogFileContents() + public function getLogFileContents() { // Return blank string if no log file. if (!file_exists($this->logFile)) { @@ -5557,7 +5115,7 @@ private function getLogFileContents() * @param string $name Method Name. * @param array $args Method Arguments. */ - private function callPrivateMethod($obj, $name, array $args) + public function callPrivateMethod($obj, $name, array $args) { $class = new \ReflectionClass($obj); $method = $class->getMethod($name); @@ -5578,7 +5136,7 @@ private function callPrivateMethod($obj, $name, array $args) * * @return string */ - private function generateEmailAddress($domain = 'kit.com') + public function generateEmailAddress($domain = 'kit.com') { return 'php-sdk-' . date('Y-m-d-H-i-s') . '-php-' . PHP_VERSION_ID . '@' . $domain; } @@ -5591,7 +5149,7 @@ private function generateEmailAddress($domain = 'kit.com') * @param $string Possible HTML. * @return bool */ - private function isHtml($string) + public function isHtml($string) { return preg_match("/<[^<]+>/", $string, $m) != 0; } @@ -5605,7 +5163,7 @@ private function isHtml($string) * @param null|array $responseBody Response to return when API call is made. * @param int $httpCode HTTP Code to return when API call is made. */ - private function mockResponse(ConvertKit_API $api, $responseBody = null, int $httpCode = 200) + public function mockResponse(ConvertKit_API $api, $responseBody = null, int $httpCode = 200) { // Setup API with a mock Guzzle client, returning the data // as if we successfully swapped an auth code for an access token. @@ -5635,7 +5193,7 @@ private function mockResponse(ConvertKit_API $api, $responseBody = null, int $ht * * @param array $result API Result. */ - private function assertDataExists($result, $key) + public function assertDataExists($result, $key) { $result = get_object_vars($result); $this->assertArrayHasKey($key, $result); @@ -5649,7 +5207,7 @@ private function assertDataExists($result, $key) * * @param array $result API Result. */ - private function assertPaginationExists($result) + public function assertPaginationExists($result) { $result = get_object_vars($result); $this->assertArrayHasKey('pagination', $result);