diff --git a/changelog.txt b/changelog.txt index 613c7c6..199d49e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ ** Hellotext for WooCommerce Changelog ** +2026-01-19 - version 1.3.1 + +* Add X-Plugin-Version header to all API requests for version tracking and analytics. + + 2026-01-18 - version 1.3.0 * Add comprehensive type hints to all source files for better IDE support and type safety. diff --git a/hellotext.php b/hellotext.php index 4fb9c48..642383a 100644 --- a/hellotext.php +++ b/hellotext.php @@ -8,7 +8,7 @@ * Plugin Name: Hellotext * Plugin URI: https://github.com/hellotext/hellotext-wordpress * Description: Integrates Hellotext tracking to WooCommerce. - * Version: 1.3.0 + * Version: 1.3.1 * Author: Hellotext * Author URI: https://www.hellotext.com * License: GPL v2 diff --git a/src/Api/Client.php b/src/Api/Client.php index e0db10b..883b8f0 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -53,6 +53,7 @@ public static function request(string $method = 'GET', string $path = '/', array 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $access_token, + 'X-Plugin-Version' => self::get_plugin_version(), ], 'sslverify' => true, ]; @@ -178,4 +179,20 @@ private static function get_api_url(): string { return $HELLOTEXT_API_URL . self::$sufix; } + /** + * Get plugin version. + * + * @return string + */ + private static function get_plugin_version(): string { + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugin_file = dirname(__DIR__, 2) . '/hellotext.php'; + $plugin_data = get_plugin_data($plugin_file, false, false); + + return $plugin_data['Version'] ?? 'unknown'; + } + } diff --git a/tests/Mocks.php b/tests/Mocks.php index f677503..f045b0d 100644 --- a/tests/Mocks.php +++ b/tests/Mocks.php @@ -175,6 +175,11 @@ public function get_id () { // Mock WordPress HTTP API functions if (!function_exists('wp_remote_request')) { function wp_remote_request ($url, $args = array()) { + // Allow per-test override + if (isset($GLOBALS['test_wp_remote_request'])) { + return $GLOBALS['test_wp_remote_request']($url, $args); + } + return array( 'response' => array('code' => 200), 'body' => json_encode(array('success' => true)), @@ -211,3 +216,34 @@ function wp_remote_retrieve_body ($response) { return $response['body'] ?? ''; } } + +if (!function_exists('get_option')) { + function get_option ($option, $default = false) { + // Allow per-test override + if (isset($GLOBALS['test_options'][$option])) { + return $GLOBALS['test_options'][$option]; + } + + // Default mock values + if ($option === 'hellotext_access_token') { + return 'test_token_123'; + } + + return $default; + } +} + +if (!function_exists('get_plugin_data')) { + function get_plugin_data ($plugin_file, $markup = true, $translate = true) { + // Allow per-test override + if (isset($GLOBALS['test_plugin_data'])) { + return $GLOBALS['test_plugin_data']; + } + + return [ + 'Name' => 'Hellotext', + 'Version' => '1.3.0', + 'Author' => 'Hellotext', + ]; + } +} diff --git a/tests/Unit/Api/ClientTest.php b/tests/Unit/Api/ClientTest.php new file mode 100644 index 0000000..8dbe7e1 --- /dev/null +++ b/tests/Unit/Api/ClientTest.php @@ -0,0 +1,137 @@ +toBe('/v1'); +}); + +test('can set custom suffix', function () { + Client::with_sufix('/v2'); + expect(Client::$sufix)->toBe('/v2'); + + // Reset + Client::with_sufix('/v1'); +}); + +test('custom suffix adds leading slash if missing', function () { + Client::with_sufix('v3'); + expect(Client::$sufix)->toBe('/v3'); + + // Reset + Client::with_sufix('/v1'); +}); + +test('empty suffix works', function () { + Client::with_sufix(''); + expect(Client::$sufix)->toBe(''); + + // Reset + Client::with_sufix('/v1'); +}); + +test('Client::get() returns expected structure', function () { + $response = Client::get('/test'); + + expect($response)->toBeArray(); + expect($response)->toHaveKeys(['request', 'status', 'body', 'error']); +}); + +test('Client::post() returns expected structure', function () { + $response = Client::post('/test', ['data' => 'value']); + + expect($response)->toBeArray(); + expect($response)->toHaveKeys(['request', 'status', 'body', 'error']); +}); + +test('Client::put() returns expected structure', function () { + $response = Client::put('/test/1', ['data' => 'value']); + + expect($response)->toBeArray(); + expect($response)->toHaveKeys(['request', 'status', 'body', 'error']); +}); + +test('Client::patch() returns expected structure', function () { + $response = Client::patch('/test/1', ['data' => 'value']); + + expect($response)->toBeArray(); + expect($response)->toHaveKeys(['request', 'status', 'body', 'error']); +}); + +test('Client::delete() returns expected structure', function () { + $response = Client::delete('/test/1'); + + expect($response)->toBeArray(); + expect($response)->toHaveKeys(['request', 'status', 'body', 'error']); +}); + +test('request includes method in response', function () { + $response = Client::get('/test'); + + expect($response['request']['method'])->toBe('GET'); +}); + +test('request includes path in response', function () { + $response = Client::get('/test/endpoint'); + + expect($response['request']['path'])->toContain('/test/endpoint'); +}); + +test('request includes data in response', function () { + $data = ['key' => 'value']; + $response = Client::post('/test', $data); + + expect($response['request']['data'])->toBe($data); +}); + +test('with_sufix returns Client instance', function () { + $client = Client::with_sufix('/custom'); + + expect($client)->toBeInstanceOf(Client::class); +}); + +test('all HTTP methods accept optional data parameter', function () { + // These should not throw errors + expect(fn() => Client::get('/test', null))->not->toThrow(Error::class); + expect(fn() => Client::post('/test', null))->not->toThrow(Error::class); + expect(fn() => Client::put('/test', null))->not->toThrow(Error::class); + expect(fn() => Client::patch('/test', null))->not->toThrow(Error::class); + expect(fn() => Client::delete('/test', null))->not->toThrow(Error::class); +}); + +test('handles empty path gracefully', function () { + $response = Client::get(''); + + expect($response)->toBeArray(); + expect($response)->toHaveKey('status'); +}); + +test('request method is case-insensitive', function () { + $response = Client::request('get', '/test'); + expect($response['request']['method'])->toBe('get'); +}); + +test('static methods can be called', function () { + expect(method_exists(Client::class, 'get'))->toBeTrue(); + expect(method_exists(Client::class, 'post'))->toBeTrue(); + expect(method_exists(Client::class, 'put'))->toBeTrue(); + expect(method_exists(Client::class, 'patch'))->toBeTrue(); + expect(method_exists(Client::class, 'delete'))->toBeTrue(); + expect(method_exists(Client::class, 'request'))->toBeTrue(); + expect(method_exists(Client::class, 'with_sufix'))->toBeTrue(); +}); + +test('Client has API_VERSION constant reference', function () { + expect(defined('Hellotext\Constants::API_VERSION'))->toBeTrue(); + expect(Constants::API_VERSION)->toBe('v1'); +});