Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion hellotext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/Api/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
Expand Down Expand Up @@ -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';
}

}
36 changes: 36 additions & 0 deletions tests/Mocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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',
];
}
}
137 changes: 137 additions & 0 deletions tests/Unit/Api/ClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

use Hellotext\Api\Client;
use Hellotext\Constants;

beforeEach(function () {
if (!defined('ABSPATH')) {
define('ABSPATH', '/tmp/wordpress/');
}

global $HELLOTEXT_API_URL;
$HELLOTEXT_API_URL = 'https://api.hellotext.com';
});

test('Client has correct API version suffix by default', function () {
expect(Client::$sufix)->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');
});