From 386e78e67387bda3d4a81adc20816221817334dd Mon Sep 17 00:00:00 2001 From: Chris Minett <1084019+chrisminett@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:56:40 +0100 Subject: [PATCH 1/4] Deprecate `getConfig()` method Update unit tests to validate the values used to construct the HTTP client. --- CHANGELOG.md | 2 ++ src/Client.php | 30 +++++++++++++++--- src/Helper.php | 2 +- src/Service.php | 2 +- tests/ClientTest.php | 75 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4f3e5..06851aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Changed +- Deprecate `getConfig()` method. Packages can maintain their config internally. ## [5.1.1] - 2021-08-10 ### Fixed diff --git a/src/Client.php b/src/Client.php index b041f32..237c002 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,6 +5,7 @@ namespace Maxemail\Api; use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\ClientInterface as GuzzleClientInterface; use GuzzleHttp\HandlerStack; use Psr\Log\LoggerInterface; @@ -91,7 +92,7 @@ class Client implements \Psr\Log\LoggerAwareInterface private $logger; /** - * @var GuzzleClient + * @var GuzzleClientInterface */ private $httpClient; @@ -100,6 +101,11 @@ class Client implements \Psr\Log\LoggerAwareInterface */ private $debugLoggingEnabled = false; + /** + * @var \Closure(array):GuzzleClientInterface + */ + private $httpClientFactory; + /** * @param array $config { * @var string $username Required @@ -157,7 +163,7 @@ private function getInstance(string $serviceName): Service return $this->services[$serviceName]; } - private function getClient(): GuzzleClient + private function getClient(): GuzzleClientInterface { if ($this->httpClient === null) { $stack = HandlerStack::create(); @@ -166,7 +172,8 @@ private function getClient(): GuzzleClient if ($this->debugLoggingEnabled) { Middleware::addLogging($stack, $this->getLogger()); } - $this->httpClient = new GuzzleClient([ + + $clientConfig = [ 'base_uri' => $this->uri . 'api/json/', 'auth' => [ $this->username, @@ -178,7 +185,13 @@ private function getClient(): GuzzleClient 'Accept' => 'application/json', ], 'handler' => $stack, - ]); + ]; + + if (!isset($this->httpClientFactory)) { + $this->httpClient = new GuzzleClient($clientConfig); + } else { + $this->httpClient = ($this->httpClientFactory)($clientConfig); + } } return $this->httpClient; @@ -187,6 +200,7 @@ private function getClient(): GuzzleClient /** * Get API connection config * + * @deprecated v5.2 No replacement; packages can maintain their own config; to be removed in v7. * @return array { * @var string $uri * @var string $username @@ -224,4 +238,12 @@ public function getLogger(): LoggerInterface return $this->logger; } + + /** + * @internal This method is not part of the BC promise. Used for DI for unit tests only. + */ + public function setHttpClientFactory(\Closure $httpClientFactory): void + { + $this->httpClientFactory = $httpClientFactory; + } } diff --git a/src/Helper.php b/src/Helper.php index 5c940ba..262f534 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -4,7 +4,7 @@ namespace Maxemail\Api; -use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\ClientInterface as GuzzleClient; use Psr\Log\LogLevel; /** diff --git a/src/Service.php b/src/Service.php index e56beeb..13b0f30 100644 --- a/src/Service.php +++ b/src/Service.php @@ -4,7 +4,7 @@ namespace Maxemail\Api; -use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\ClientInterface as GuzzleClient; /** * Maxemail API Client diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 47d0256..f00982b 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -4,6 +4,7 @@ namespace Maxemail\Api; +use GuzzleHttp\ClientInterface as GuzzleClient; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +27,30 @@ public function testConfigValid() { $api = new Client($this->testConfig); - $this->assertSame($this->testConfig, $api->getConfig()); + $factory = function (array $actual): GuzzleClient { + $expectedUri = $this->testConfig['uri'] . 'api/json/'; + static::assertSame($expectedUri, $actual['base_uri']); + + $expectedAuth = [ + $this->testConfig['username'], + $this->testConfig['password'], + ]; + static::assertSame($expectedAuth, $actual['auth']); + + $expectedHeaders = [ + 'User-Agent' => 'MxmApiClient/' . Client::VERSION . ' PHP/' . PHP_VERSION, + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Accept' => 'application/json', + ]; + static::assertSame($expectedHeaders, $actual['headers']); + + return $this->createMock(GuzzleClient::class); + }; + + $api->setHttpClientFactory($factory); + + // Get a service, to trigger the HTTP Client factory + $api->folder; } public function testConfigSupportDeprecatedUserPass() @@ -38,8 +62,20 @@ public function testConfigSupportDeprecatedUserPass() $api = new Client($config); - $this->assertSame($config['user'], $api->getConfig()['username']); - $this->assertSame($config['pass'], $api->getConfig()['password']); + $factory = function (array $actual) use ($config): GuzzleClient { + $expectedAuth = [ + $config['user'], + $config['pass'], + ]; + static::assertSame($expectedAuth, $actual['auth']); + + return $this->createMock(GuzzleClient::class); + }; + + $api->setHttpClientFactory($factory); + + // Get a service, to trigger the HTTP Client factory + $api->folder; } public function testConfigDefaultHost() @@ -51,20 +87,40 @@ public function testConfigDefaultHost() $api = new Client($config); - $this->assertSame('https://mxm.xtremepush.com/', $api->getConfig()['uri']); + $factory = function (array $actual) use ($config): GuzzleClient { + $expectedUri = 'https://mxm.xtremepush.com/api/json/'; + static::assertSame($expectedUri, $actual['base_uri']); + + return $this->createMock(GuzzleClient::class); + }; + + $api->setHttpClientFactory($factory); + + // Get a service, to trigger the HTTP Client factory + $api->folder; } public function testConfigStripsUriPath() { $config = [ - 'uri' => 'http://maxemail.example.com/some/extra/path', + 'uri' => 'https://maxemail.example.com/some/extra/path', 'username' => 'api@user.com', 'password' => 'apipass', ]; $api = new Client($config); - $this->assertSame('http://maxemail.example.com/', $api->getConfig()['uri']); + $factory = function (array $actual) use ($config): GuzzleClient { + $expectedUri = 'https://maxemail.example.com/api/json/'; + static::assertSame($expectedUri, $actual['base_uri']); + + return $this->createMock(GuzzleClient::class); + }; + + $api->setHttpClientFactory($factory); + + // Get a service, to trigger the HTTP Client factory + $api->folder; } public function testConfigInvalidUri() @@ -119,6 +175,13 @@ public function testConfigMissingPassword(): void new Client($config); } + public function testGetConfig(): void + { + $api = new Client($this->testConfig); + + static::assertSame($this->testConfig, $api->getConfig()); + } + public function testSetGetLogger() { /** @var \Psr\Log\LoggerInterface|MockObject $logger */ From 74b07a7388ac97e6a16a706b883e19be49e2890d Mon Sep 17 00:00:00 2001 From: Chris Minett <1084019+chrisminett@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:58:22 +0100 Subject: [PATCH 2/4] Add support for API token authentication for Maxemail v145 --- CHANGELOG.md | 3 ++ README.md | 3 +- phpunit.xml.dist | 3 +- src/Client.php | 47 +++++++++++++++-------- tests/ClientTest.php | 80 ++++++++++++++++++++++++++++++---------- tests/FunctionalTest.php | 3 +- 6 files changed, 98 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06851aa..a0c9ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- Support for API token authentication. Username and password can still be used + as a fallback. ### Changed - Deprecate `getConfig()` method. Packages can maintain their config internally. diff --git a/README.md b/README.md index 1f40d73..f8d47c4 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,7 @@ $ composer require maxemail/api-php ```php // Instantiate Client: $config = [ - 'username' => 'api@user.com', - 'password' => 'apipass' + 'token' => 'apitoken', ]; $api = new \Maxemail\Api\Client($config); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e6230c1..8a738f1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,8 +12,7 @@ - - + diff --git a/src/Client.php b/src/Client.php index 237c002..5ae1fa6 100644 --- a/src/Client.php +++ b/src/Client.php @@ -66,6 +66,11 @@ class Client implements \Psr\Log\LoggerAwareInterface */ private $uri = 'https://mxm.xtremepush.com/'; + /** + * @var string + */ + private $token; + /** * @var string */ @@ -118,20 +123,25 @@ class Client implements \Psr\Log\LoggerAwareInterface */ public function __construct(array $config) { - // Support deprecated key names from v3 - if (!isset($config['username']) && isset($config['user'])) { - $config['username'] = $config['user']; - } - if (!isset($config['password']) && isset($config['pass'])) { - $config['password'] = $config['pass']; - } + // Must have API token + if (!isset($config['token'])) { + // Support deprecated key names from v3 + if (!isset($config['username']) && isset($config['user'])) { + $config['username'] = $config['user']; + } + if (!isset($config['password']) && isset($config['pass'])) { + $config['password'] = $config['pass']; + } - // Must have user/pass - if (!isset($config['username']) || !isset($config['password'])) { - throw new Exception\InvalidArgumentException('API config requires username & password'); + // Must have user/pass + if (!isset($config['username']) || !isset($config['password'])) { + throw new Exception\InvalidArgumentException('API config requires token OR username & password'); + } + $this->username = $config['username']; + $this->password = $config['password']; + } else { + $this->token = $config['token']; } - $this->username = $config['username']; - $this->password = $config['password']; if (isset($config['uri'])) { $parsed = parse_url($config['uri']); @@ -175,10 +185,6 @@ private function getClient(): GuzzleClientInterface $clientConfig = [ 'base_uri' => $this->uri . 'api/json/', - 'auth' => [ - $this->username, - $this->password, - ], 'headers' => [ 'User-Agent' => 'MxmApiClient/' . self::VERSION . ' PHP/' . PHP_VERSION, 'Content-Type' => 'application/x-www-form-urlencoded', @@ -187,6 +193,15 @@ private function getClient(): GuzzleClientInterface 'handler' => $stack, ]; + if (isset($this->token)) { + $clientConfig['headers']['Authorization'] = 'Bearer ' . $this->token; + } else { + $clientConfig['auth'] = [ + $this->username, + $this->password, + ]; + } + if (!isset($this->httpClientFactory)) { $this->httpClient = new GuzzleClient($clientConfig); } else { diff --git a/tests/ClientTest.php b/tests/ClientTest.php index f00982b..370a511 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -19,8 +19,7 @@ class ClientTest extends TestCase { private $testConfig = [ 'uri' => 'https://maxemail.example.com/', - 'username' => 'api@user.com', - 'password' => 'apipass', + 'token' => 'apitoken', ]; public function testConfigValid() @@ -31,16 +30,11 @@ public function testConfigValid() $expectedUri = $this->testConfig['uri'] . 'api/json/'; static::assertSame($expectedUri, $actual['base_uri']); - $expectedAuth = [ - $this->testConfig['username'], - $this->testConfig['password'], - ]; - static::assertSame($expectedAuth, $actual['auth']); - $expectedHeaders = [ 'User-Agent' => 'MxmApiClient/' . Client::VERSION . ' PHP/' . PHP_VERSION, 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $this->testConfig['token'], ]; static::assertSame($expectedHeaders, $actual['headers']); @@ -81,8 +75,7 @@ public function testConfigSupportDeprecatedUserPass() public function testConfigDefaultHost() { $config = [ - 'username' => 'api@user.com', - 'password' => 'apipass', + 'token' => 'apitoken', ]; $api = new Client($config); @@ -104,8 +97,7 @@ public function testConfigStripsUriPath() { $config = [ 'uri' => 'https://maxemail.example.com/some/extra/path', - 'username' => 'api@user.com', - 'password' => 'apipass', + 'token' => 'apitoken', ]; $api = new Client($config); @@ -130,8 +122,7 @@ public function testConfigInvalidUri() $config = [ 'uri' => '//', - 'username' => 'api@user.com', - 'password' => 'apipass', + 'token' => 'apitoken', ]; new Client($config); @@ -144,17 +135,49 @@ public function testConfigMissingUriProtocol() $config = [ 'uri' => 'maxemail.example.com', + 'token' => 'apitoken', + ]; + + new Client($config); + } + + public function testConfigLegacyAuthentication(): void + { + $config = [ 'username' => 'api@user.com', 'password' => 'apipass', ]; - new Client($config); + $api = new Client($config); + + $factory = function (array $actual) use ($config): GuzzleClient { + $expectedAuth = [ + $config['username'], + $config['password'], + ]; + static::assertSame($expectedAuth, $actual['auth']); + + return $this->createMock(GuzzleClient::class); + }; + + $api->setHttpClientFactory($factory); + + // Get a service, to trigger the HTTP Client factory + $api->folder; + } + + public function testConfigMissingToken(): void + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('API config requires token OR username & password'); + + new Client([]); } public function testConfigMissingUsername(): void { $this->expectException(Exception\InvalidArgumentException::class); - $this->expectExceptionMessage('API config requires username & password'); + $this->expectExceptionMessage('API config requires token OR username & password'); $config = [ 'password' => 'apipass', @@ -166,7 +189,7 @@ public function testConfigMissingUsername(): void public function testConfigMissingPassword(): void { $this->expectException(Exception\InvalidArgumentException::class); - $this->expectExceptionMessage('API config requires username & password'); + $this->expectExceptionMessage('API config requires token OR username & password'); $config = [ 'username' => 'api@user.com', @@ -175,11 +198,30 @@ public function testConfigMissingPassword(): void new Client($config); } - public function testGetConfig(): void + public function testGetConfigWithToken(): void { $api = new Client($this->testConfig); - static::assertSame($this->testConfig, $api->getConfig()); + $expected = [ + 'uri' => $this->testConfig['uri'], + 'username' => null, + 'password' => null, + ]; + + static::assertSame($expected, $api->getConfig()); + } + + public function testGetConfigWithLegacyAuthentication(): void + { + $config = [ + 'uri' => 'https://maxemail.example.com/', + 'username' => 'api@user.com', + 'password' => 'apipass', + ]; + + $api = new Client($config); + + static::assertSame($config, $api->getConfig()); } public function testSetGetLogger() diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 5df4ca7..21567bd 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -30,8 +30,7 @@ protected function setUp(): void $config = [ 'uri' => getenv('FUNC_API_URI'), - 'username' => getenv('FUNC_API_USERNAME'), - 'password' => getenv('FUNC_API_PASSWORD'), + 'token' => getenv('FUNC_API_TOKEN'), ]; $this->client = new Client($config); } From c3202e2db3c1c380af168fec9362e8f47bb9c204 Mon Sep 17 00:00:00 2001 From: Chris Minett <1084019+chrisminett@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:29:33 +0100 Subject: [PATCH 3/4] Update Client docblock --- src/Client.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 5ae1fa6..1ec8620 100644 --- a/src/Client.php +++ b/src/Client.php @@ -113,8 +113,9 @@ class Client implements \Psr\Log\LoggerAwareInterface /** * @param array $config { - * @var string $username Required - * @var string $password Required + * @var string $token Required, or username & password + * @var string $username Required, if no token + * @var string $password Required, if no token * @var string $uri Optional. Default https://mxm.xtremepush.com/ * @var string $user @deprecated See username * @var string $pass @deprecated See password From 6379d104d9d34e8a02acbf5239f8c74229b410ad Mon Sep 17 00:00:00 2001 From: Chris Minett <1084019+chrisminett@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:11:37 +0100 Subject: [PATCH 4/4] Tag for v5.2.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c9ec0..1b2cf46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] + +## [5.2.0] - 2025-06-25 ### Added - Support for API token authentication. Username and password can still be used as a fallback.