From e0269c643b0d2b170f5c9852882b7ef842f0c704 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 00:06:36 +0100 Subject: [PATCH 1/9] Add custom exceptions for error handling --- AGENTS.md | 52 +++++++- README.md | 51 +++++++- src/Exceptions/AuthenticationException.php | 19 +++ src/Exceptions/RateLimitException.php | 31 +++++ src/Exceptions/ValidationException.php | 19 +++ src/Exceptions/WatiApiException.php | 40 +++++++ src/Exceptions/WatiException.php | 9 ++ src/WatiClient.php | 79 ++++++++++-- tests/ExceptionsTest.php | 132 +++++++++++++++++++++ tests/WatiClientTest.php | 47 ++++++++ 10 files changed, 468 insertions(+), 11 deletions(-) create mode 100644 src/Exceptions/AuthenticationException.php create mode 100644 src/Exceptions/RateLimitException.php create mode 100644 src/Exceptions/ValidationException.php create mode 100644 src/Exceptions/WatiApiException.php create mode 100644 src/Exceptions/WatiException.php create mode 100644 tests/ExceptionsTest.php diff --git a/AGENTS.md b/AGENTS.md index cfea230..a6f3e24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,18 @@ $environment = new WatiEnvironment($endpoint, $bearerToken); $client = new WatiClient($environment); ``` +#### With Custom Options + +```php +$client = new WatiClient($environment, [ + 'timeout' => 60, // Request timeout in seconds (default: 30) + 'connect_timeout' => 15, // Connection timeout in seconds (default: 10) + 'verify' => true, // Verify SSL certificate (default: true) + 'proxy' => 'tcp://localhost:8080', // Proxy URL (default: null) + 'debug' => false, // Enable debug mode (default: false) +]); +``` + ### 3. Make API Requests Extend `WatiRequest` to create requests: @@ -65,7 +77,45 @@ composer types src/ ├── WatiClient.php # Main HTTP client ├── WatiEnvironment.php # Holds endpoint + token -└── WatiRequest.php # Base request class +├── WatiRequest.php # Base request class +└── Exceptions/ + ├── WatiException.php # Base exception + ├── WatiApiException.php # API error responses + ├── AuthenticationException.php # 401 errors + ├── RateLimitException.php # 429 errors + └── ValidationException.php # 400/422 errors +``` + +## Error Handling + +The client throws specific exceptions for different error scenarios: + +```php +use Wati\Http\Exceptions\AuthenticationException; +use Wati\Http\Exceptions\RateLimitException; +use Wati\Http\Exceptions\ValidationException; +use Wati\Http\Exceptions\WatiApiException; +use Wati\Http\Exceptions\WatiException; + +try { + $response = $client->send(new GetContactsRequest()); +} catch (AuthenticationException $e) { + // Invalid bearer token - check credentials + echo "Auth failed: " . $e->getMessage(); +} catch (RateLimitException $e) { + // Rate limited - wait and retry + $retryAfter = $e->getRetryAfter(); // seconds to wait +} catch (ValidationException $e) { + // Invalid request parameters + $errors = $e->getResponseData(); +} catch (WatiApiException $e) { + // Other API errors (4xx, 5xx) + $statusCode = $e->getStatusCode(); + $data = $e->getResponseData(); +} catch (WatiException $e) { + // Connection or other HTTP errors + echo "Request failed: " . $e->getMessage(); +} ``` ## Common Operations diff --git a/README.md b/README.md index 79fb968..d9e2223 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![Total Downloads](http://poser.pugx.org/phpjuice/wati-http-client/downloads)](https://packagist.org/packages/phpjuice/wati-http-client) [![License](http://poser.pugx.org/phpjuice/wati-http-client/license)](https://packagist.org/packages/phpjuice/wati-http-client) -A PHP HTTP Client for the [Wati.io](https://wati.io) WhatsApp API. Provides a simple, fluent API to interact with Wati's REST API. +A PHP HTTP Client for the [Wati.io](https://wati.io) WhatsApp API. Provides a simple, fluent API to interact with Wati's +REST API. ## Installation @@ -43,6 +44,20 @@ $environment = new WatiEnvironment($endpoint, $bearerToken); $client = new WatiClient($environment); ``` +#### With Custom Options + +```php + 60, // Request timeout in seconds (default: 30) + 'connect_timeout' => 15, // Connection timeout in seconds (default: 10) + 'verify' => true, // Verify SSL certificate (default: true) + 'proxy' => 'tcp://localhost:8080', // Proxy URL (default: null) + 'debug' => false, // Enable debug mode (default: false) +]); +``` + ## Usage ### Making Requests @@ -121,6 +136,40 @@ For full API documentation, visit [Wati API Docs](https://docs.wati.io/reference - **Templates**: Get and send message templates - **Campaigns**: Manage broadcasts +## Error Handling + +The client throws specific exceptions for different error scenarios: + +```php +send(new GetContactsRequest()); +} catch (AuthenticationException $e) { + // Invalid bearer token - check credentials + echo "Auth failed: " . $e->getMessage(); +} catch (RateLimitException $e) { + // Rate limited - wait and retry + $retryAfter = $e->getRetryAfter(); // seconds to wait +} catch (ValidationException $e) { + // Invalid request parameters + $errors = $e->getResponseData(); +} catch (WatiApiException $e) { + // Other API errors (4xx, 5xx) + $statusCode = $e->getStatusCode(); + $data = $e->getResponseData(); +} catch (WatiException $e) { + // Connection or other HTTP errors + echo "Request failed: " . $e->getMessage(); +} +``` + ## Changelog Please see the [CHANGELOG](changelog.md) for more information on what has changed recently. diff --git a/src/Exceptions/AuthenticationException.php b/src/Exceptions/AuthenticationException.php new file mode 100644 index 0000000..7bb092c --- /dev/null +++ b/src/Exceptions/AuthenticationException.php @@ -0,0 +1,19 @@ +getHeaderLine('Retry-After'); + $this->retryAfter = $retryAfter !== '' ? (int) $retryAfter : null; + } + } + + public function getRetryAfter(): ?int + { + return $this->retryAfter; + } +} diff --git a/src/Exceptions/ValidationException.php b/src/Exceptions/ValidationException.php new file mode 100644 index 0000000..d4a9c35 --- /dev/null +++ b/src/Exceptions/ValidationException.php @@ -0,0 +1,19 @@ +|null */ + protected ?array $responseData = null; + + public function __construct( + string $message, + protected int $statusCode, + ?ResponseInterface $response = null, + ?Throwable $previous = null + ) { + if ($response instanceof ResponseInterface) { + $body = $response->getBody()->getContents(); + $decoded = json_decode($body, true); + $this->responseData = is_array($decoded) ? $decoded : null; + } + + parent::__construct($message, $statusCode, $previous); + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + /** @return array|null */ + public function getResponseData(): ?array + { + return $this->responseData; + } +} diff --git a/src/Exceptions/WatiException.php b/src/Exceptions/WatiException.php new file mode 100644 index 0000000..4ac4e50 --- /dev/null +++ b/src/Exceptions/WatiException.php @@ -0,0 +1,9 @@ +client = new Client([ - 'base_uri' => $this->environment->baseUrl(), - 'timeout' => 30, - 'connect_timeout' => 10, - ]); + /** @var array */ + protected array $defaultOptions = [ + 'timeout' => 30, + 'connect_timeout' => 10, + 'verify' => true, + 'debug' => false, + 'proxy' => null, + ]; + + /** + * @param array $options + */ + public function __construct( + protected WatiEnvironment $environment, + array $options = [] + ) { + $config = array_merge($this->defaultOptions, $options); + $config['base_uri'] = $this->environment->baseUrl(); + + $this->client = new Client(array_filter($config, fn ($value): bool => $value !== null)); } - /** @throws GuzzleException */ + /** + * @throws WatiException + * @throws WatiApiException + */ public function send(RequestInterface $request): ResponseInterface { if (! $this->hasAuthHeader($request)) { @@ -32,7 +56,44 @@ public function send(RequestInterface $request): ResponseInterface $request = $this->injectUserAgentHeaders($request); $request = $this->injectSdkHeaders($request); - return $this->client->send($request); + try { + return $this->client->send($request); + } catch (ClientException $e) { + $response = $e->getResponse(); + $statusCode = $response->getStatusCode(); + + throw match ($statusCode) { + 401 => new AuthenticationException(response: $response, previous: $e), + 429 => new RateLimitException(response: $response, previous: $e), + 400, 422 => new ValidationException(response: $response, previous: $e), + default => new WatiApiException( + $e->getMessage(), + $statusCode, + $response, + $e + ), + }; + } catch (ServerException $e) { + $response = $e->getResponse(); + throw new WatiApiException( + 'Wati API server error: '.$e->getMessage(), + $response->getStatusCode(), + $response, + $e + ); + } catch (ConnectException $e) { + throw new WatiException( + 'Failed to connect to Wati API: '.$e->getMessage(), + 0, + $e + ); + } catch (GuzzleException $e) { + throw new WatiException( + 'HTTP request failed: '.$e->getMessage(), + 0, + $e + ); + } } public function hasAuthHeader(RequestInterface $request): bool diff --git a/tests/ExceptionsTest.php b/tests/ExceptionsTest.php new file mode 100644 index 0000000..9d954ec --- /dev/null +++ b/tests/ExceptionsTest.php @@ -0,0 +1,132 @@ + 'application/json'], '{"error": "Unauthorized"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); +})->throws(AuthenticationException::class); + +it('throws RateLimitException on 429', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(429, ['Content-Type' => 'application/json', 'Retry-After' => '60'], '{"error": "Rate limit exceeded"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); +})->throws(RateLimitException::class); + +it('throws ValidationException on 400', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(400, ['Content-Type' => 'application/json'], '{"error": "Bad request"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); +})->throws(ValidationException::class); + +it('throws ValidationException on 422', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(422, ['Content-Type' => 'application/json'], '{"error": "Unprocessable entity"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); +})->throws(ValidationException::class); + +it('throws WatiApiException on 404', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(404, ['Content-Type' => 'application/json'], '{"error": "Not found"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts/999') extends WatiRequest {}; + $client->send($request); +})->throws(WatiApiException::class); + +it('throws WatiApiException on 500', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(500, ['Content-Type' => 'application/json'], '{"error": "Internal server error"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); +})->throws(WatiApiException::class); + +it('includes response data in exception', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(401, ['Content-Type' => 'application/json'], '{"error": "Invalid token", "code": "AUTH_FAILED"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + + try { + $client->send($request); + } catch (WatiApiException $e) { + expect($e->getStatusCode())->toBe(401) + ->and($e->getResponseData())->toBe(['error' => 'Invalid token', 'code' => 'AUTH_FAILED']); + } +}); + +it('includes retry-after in RateLimitException', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $mockHandler = new MockHandler([ + new Response(429, ['Content-Type' => 'application/json', 'Retry-After' => '120'], '{"error": "Rate limit exceeded"}'), + ]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + + try { + $client->send($request); + } catch (RateLimitException $e) { + expect($e->getRetryAfter())->toBe(120); + } +}); diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index 355307e..f4b5ef0 100644 --- a/tests/WatiClientTest.php +++ b/tests/WatiClientTest.php @@ -83,3 +83,50 @@ function createMockClient(): array expect($response->getStatusCode())->toBe(200); }); + +it('accepts custom timeout option', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, ['timeout' => 60]); + + expect($client->getEnvironment())->toBe($env); +}); + +it('accepts custom connect_timeout option', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, ['connect_timeout' => 20]); + + expect($client->getEnvironment())->toBe($env); +}); + +it('accepts verify ssl option', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, ['verify' => false]); + + expect($client->getEnvironment())->toBe($env); +}); + +it('accepts proxy option', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, ['proxy' => 'tcp://localhost:8080']); + + expect($client->getEnvironment())->toBe($env); +}); + +it('accepts debug option', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, ['debug' => true]); + + expect($client->getEnvironment())->toBe($env); +}); + +it('accepts multiple options', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env, [ + 'timeout' => 60, + 'connect_timeout' => 20, + 'verify' => true, + 'debug' => false, + ]); + + expect($client->getEnvironment())->toBe($env); +}); From a36e95d18f697edf1bb04ce722f831bcb121d899 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 01:00:52 +0100 Subject: [PATCH 2/9] Add proper path handling --- .gitignore | 1 + AGENTS.md | 4 ++- README.md | 5 ++-- changelog.md | 17 +++++++++++- src/WatiClient.php | 20 ++++++++++++-- src/WatiEnvironment.php | 27 ++++++++++++++++-- tests/ExceptionsTest.php | 46 +++++++++++++++++++++++++++++++ tests/WatiClientTest.php | 52 +++++++++++++++++++++++++++++++++-- tests/WatiEnvironmentTest.php | 23 ++++++++++++++++ 9 files changed, 184 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 48b9403..a950b14 100755 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ vendor .php-cs-fixer.cache .phpunit.cache composer.lock +test-request.php diff --git a/AGENTS.md b/AGENTS.md index a6f3e24..6891669 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,9 @@ composer require phpjuice/wati-http-client use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; -$endpoint = 'https://your-instance.wati.io'; +// Get this URL from your Wati Dashboard (API Docs section) +// It includes your tenant ID: https://live-mt-server.wati.io/{tenantId} +$endpoint = 'https://live-mt-server.wati.io/372813'; $bearerToken = 'your-bearer-token'; $environment = new WatiEnvironment($endpoint, $bearerToken); diff --git a/README.md b/README.md index d9e2223..4143df2 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ composer require "phpjuice/wati-http-client" use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; -// Your API endpoint and bearer token from the Wati dashboard -$endpoint = "https://your-instance.wati.io"; +// Get this URL from your Wati Dashboard (API Docs section) +// It includes your tenant ID: https://live-mt-server.wati.io/{tenantId} +$endpoint = "https://live-mt-server.wati.io/372813"; $bearerToken = "your-bearer-token"; // Create environment diff --git a/changelog.md b/changelog.md index 7c38f83..8dd5759 100644 --- a/changelog.md +++ b/changelog.md @@ -4,4 +4,19 @@ All notable changes to `phpjuice/wati-http-client` will be documented in this fi ## 1.0.0 - 2026-02-20 -- Initial release of Wati HTTP Client (PHP 8.3+ support) +- Initial release as Wati HTTP Client +- Migrated from PayPal HTTP Client to Wati HTTP Client +- Simplified authentication using Bearer token (no OAuth flow) +- `WatiClient` - Main client with auto-injection of Authorization header +- `WatiEnvironment` - Holds API endpoint URL and bearer token +- `WatiRequest` - Base request class extending Guzzle PSR-7 Request +- Custom exceptions for error handling: + - `WatiException` - Base exception + - `WatiApiException` - API error responses with status code and response data + - `AuthenticationException` - 401 errors + - `RateLimitException` - 429 errors with retry-after support + - `ValidationException` - 400/422 errors +- Configurable HTTP options (timeout, connect_timeout, verify, proxy, debug) +- Proper tenant ID handling in URLs with trailing slash preservation +- Request path normalization for correct URI resolution with base URLs containing paths +- PHP 8.3+ support diff --git a/src/WatiClient.php b/src/WatiClient.php index 327a87b..ca5cddf 100644 --- a/src/WatiClient.php +++ b/src/WatiClient.php @@ -49,6 +49,8 @@ public function __construct( */ public function send(RequestInterface $request): ResponseInterface { + $request = $this->normalizeRequestPath($request); + if (! $this->hasAuthHeader($request)) { $request = $request->withHeader('Authorization', $this->environment->authorizationString()); } @@ -96,6 +98,19 @@ public function send(RequestInterface $request): ResponseInterface } } + protected function normalizeRequestPath(RequestInterface $request): RequestInterface + { + $uri = $request->getUri(); + $path = $uri->getPath(); + + if (str_starts_with($path, '/')) { + $uri = $uri->withPath(substr($path, 1)); + $request = $request->withUri($uri); + } + + return $request; + } + public function hasAuthHeader(RequestInterface $request): bool { return array_key_exists('Authorization', $request->getHeaders()); @@ -109,9 +124,8 @@ protected function injectUserAgentHeaders(RequestInterface $request): RequestInt protected function injectSdkHeaders(RequestInterface $request): RequestInterface { return $request - ->withHeader('sdk_name', 'Wati PHP SDK') - ->withHeader('sdk_version', '1.0.0') - ->withHeader('sdk_tech_stack', 'PHP '.PHP_VERSION); + ->withHeader('SDK_Name', 'Wati PHP SDK') + ->withHeader('SDK_Version', '1.0.0'); } public function setClient(Client $client): self diff --git a/src/WatiEnvironment.php b/src/WatiEnvironment.php index 2d4aa04..80294db 100644 --- a/src/WatiEnvironment.php +++ b/src/WatiEnvironment.php @@ -8,9 +8,32 @@ class WatiEnvironment { protected string $endpoint; - public function __construct(string $endpoint, protected string $bearerToken) + protected string $bearerToken; + + /** + * Create a new Wati environment. + * + * @param string $endpoint API endpoint URL from Wati dashboard (includes tenant ID). + * Example: https://live-mt-server.wati.io/372813 + * @param string $bearerToken The bearer token for authentication. + */ + public function __construct(string $endpoint, string $bearerToken) { - $this->endpoint = rtrim($endpoint, '/'); + // Normalize bearer token: strip "Bearer " prefix if present + if (str_starts_with(strtolower($bearerToken), 'bearer ')) { + $bearerToken = substr($bearerToken, strlen('bearer ')); + } + $this->bearerToken = $bearerToken; + + $parsed = parse_url($endpoint); + + // Check if the URL contains a path (tenant ID) + $hasPath = isset($parsed['path']) && $parsed['path'] !== '/'; + + // Ensure URLs with paths (tenant IDs) end with a trailing slash for proper URI resolution. + // This ensures relative paths are appended correctly: + // base: https://server/tenant/ + path: api/v1/contacts -> https://server/tenant/api/v1/contacts + $this->endpoint = $hasPath ? rtrim($endpoint, '/').'/' : rtrim($endpoint, '/'); } public function baseUrl(): string diff --git a/tests/ExceptionsTest.php b/tests/ExceptionsTest.php index 9d954ec..065b6ee 100644 --- a/tests/ExceptionsTest.php +++ b/tests/ExceptionsTest.php @@ -4,14 +4,19 @@ namespace Tests\Http; +use Exception; use GuzzleHttp\Client; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use Wati\Http\Exceptions\AuthenticationException; use Wati\Http\Exceptions\RateLimitException; use Wati\Http\Exceptions\ValidationException; use Wati\Http\Exceptions\WatiApiException; +use Wati\Http\Exceptions\WatiException; use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; use Wati\Http\WatiRequest; @@ -130,3 +135,44 @@ expect($e->getRetryAfter())->toBe(120); } }); + +it('throws WatiException on connection failure', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $connectException = new ConnectException( + 'Connection refused', + new Request('GET', '/api/v1/contacts') + ); + + $mockHandler = new MockHandler([$connectException]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + + try { + $client->send($request); + } catch (WatiException $e) { + expect($e->getMessage())->toContain('Failed to connect to Wati API') + ->and($e->getPrevious())->toBeInstanceOf(ConnectException::class); + } +}); + +it('throws WatiException on generic guzzle error', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + + $guzzleException = new class('Too many redirects') extends Exception implements GuzzleException {}; + + $mockHandler = new MockHandler([$guzzleException]); + $client->setClient(new Client(['handler' => HandlerStack::create($mockHandler)])); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + + try { + $client->send($request); + } catch (WatiException $e) { + expect($e->getMessage())->toContain('HTTP request failed') + ->and($e->getPrevious())->toBeInstanceOf(GuzzleException::class); + } +}); diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index f4b5ef0..debae0d 100644 --- a/tests/WatiClientTest.php +++ b/tests/WatiClientTest.php @@ -4,10 +4,12 @@ namespace Tests\Http; +use Closure; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\RequestInterface; use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; use Wati\Http\WatiRequest; @@ -66,8 +68,8 @@ function createMockClient(): array $lastRequest = $mockHandler->getLastRequest(); assert($lastRequest !== null); expect($lastRequest->getHeaderLine('Authorization'))->toBe('Bearer test-token') - ->and($lastRequest->getHeaderLine('sdk_name'))->toBe('Wati PHP SDK') - ->and($lastRequest->getHeaderLine('sdk_version'))->toBe('1.0.0') + ->and($lastRequest->getHeaderLine('SDK_Name'))->toBe('Wati PHP SDK') + ->and($lastRequest->getHeaderLine('SDK_Version'))->toBe('1.0.0') ->and($lastRequest->getHeaderLine('User-Agent'))->toBe('WatiHttp-PHP HTTP/1.1'); }); @@ -130,3 +132,49 @@ function createMockClient(): array expect($client->getEnvironment())->toBe($env); }); + +it('normalizes request path by removing leading slash', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $client = new WatiClient($env); + /** @var Client $mockClient */ + /** @var MockHandler $mockHandler */ + [$mockClient, $mockHandler] = createMockClient(); + $client->setClient($mockClient); + + $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $client->send($request); + + $lastRequest = $mockHandler->getLastRequest(); + assert($lastRequest !== null); + expect($lastRequest->getUri()->getPath())->toBe('api/v1/contacts'); +}); + +it('preserves tenant id in url when base url contains path', function (): void { + $env = new WatiEnvironment('https://live-mt-server.wati.io/372813', 'test-token'); + $client = new WatiClient($env); + + // Create a client that captures the effective URL + $capturedUrl = null; + $response = json_encode(['status' => 'success']); + assert($response !== false); + + $mockHandler = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], $response), + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $handlerStack->push(function (callable $handler) use (&$capturedUrl): Closure { + return function (RequestInterface $request, array $options) use ($handler, &$capturedUrl) { + $capturedUrl = (string) $request->getUri(); + + return $handler($request, $options); + }; + }); + + $client->setClient(new Client(['handler' => $handlerStack, 'base_uri' => $env->baseUrl()])); + + $request = new class('GET', '/api/v1/getContacts') extends WatiRequest {}; + $client->send($request); + + expect($capturedUrl)->toBe('https://live-mt-server.wati.io/372813/api/v1/getContacts'); +}); diff --git a/tests/WatiEnvironmentTest.php b/tests/WatiEnvironmentTest.php index f21f3d6..7deb02a 100644 --- a/tests/WatiEnvironmentTest.php +++ b/tests/WatiEnvironmentTest.php @@ -21,3 +21,26 @@ $env = new WatiEnvironment('https://example.wati.io/', 'test-bearer-token'); expect($env->baseUrl())->toBe('https://example.wati.io'); }); + +it('adds trailing slash to endpoint with tenant id path', function (): void { + $env = new WatiEnvironment('https://live-mt-server.wati.io/372813', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://live-mt-server.wati.io/372813/'); +}); + +it('preserves trailing slash on endpoint with tenant id path', function (): void { + $env = new WatiEnvironment('https://live-mt-server.wati.io/372813/', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://live-mt-server.wati.io/372813/'); +}); + +it('strips Bearer prefix from token', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'Bearer my-token'); + expect($env->authorizationString())->toBe('Bearer my-token'); +}); +it('strips lowercase bearer prefix from token', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'bearer my-token'); + expect($env->authorizationString())->toBe('Bearer my-token'); +}); +it('accepts token without bearer prefix', function (): void { + $env = new WatiEnvironment('https://example.wati.io', 'my-token'); + expect($env->authorizationString())->toBe('Bearer my-token'); +}); From 38d7779c87ee55d863301f1a3cff266d119c1921 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 01:13:30 +0100 Subject: [PATCH 3/9] Fix inject headers --- src/WatiClient.php | 5 +++-- tests/WatiClientTest.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/WatiClient.php b/src/WatiClient.php index ca5cddf..009c1f6 100644 --- a/src/WatiClient.php +++ b/src/WatiClient.php @@ -124,8 +124,9 @@ protected function injectUserAgentHeaders(RequestInterface $request): RequestInt protected function injectSdkHeaders(RequestInterface $request): RequestInterface { return $request - ->withHeader('SDK_Name', 'Wati PHP SDK') - ->withHeader('SDK_Version', '1.0.0'); + ->withHeader('Wati-SDK-Name', 'wati-http-client') + ->withHeader('Wati-SDK-Version', '1.0.0') + ->withHeader('Wati-SDK-Language', 'PHP'); } public function setClient(Client $client): self diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index debae0d..ed1529d 100644 --- a/tests/WatiClientTest.php +++ b/tests/WatiClientTest.php @@ -68,8 +68,9 @@ function createMockClient(): array $lastRequest = $mockHandler->getLastRequest(); assert($lastRequest !== null); expect($lastRequest->getHeaderLine('Authorization'))->toBe('Bearer test-token') - ->and($lastRequest->getHeaderLine('SDK_Name'))->toBe('Wati PHP SDK') - ->and($lastRequest->getHeaderLine('SDK_Version'))->toBe('1.0.0') + ->and($lastRequest->getHeaderLine('Wati-SDK-Name'))->toBe('wati-http-client') + ->and($lastRequest->getHeaderLine('Wati-SDK-Version'))->toBe('1.0.0') + ->and($lastRequest->getHeaderLine('Wati-SDK-Language'))->toBe('PHP') ->and($lastRequest->getHeaderLine('User-Agent'))->toBe('WatiHttp-PHP HTTP/1.1'); }); From cbf2f33ec37632b0af8665147fa58ece2c507120 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 01:15:33 +0100 Subject: [PATCH 4/9] Update changelog --- changelog.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 8dd5759..d8f9217 100644 --- a/changelog.md +++ b/changelog.md @@ -2,21 +2,18 @@ All notable changes to `phpjuice/wati-http-client` will be documented in this file. -## 1.0.0 - 2026-02-20 +## 1.0.1 - 2026-02-20 -- Initial release as Wati HTTP Client -- Migrated from PayPal HTTP Client to Wati HTTP Client -- Simplified authentication using Bearer token (no OAuth flow) -- `WatiClient` - Main client with auto-injection of Authorization header -- `WatiEnvironment` - Holds API endpoint URL and bearer token -- `WatiRequest` - Base request class extending Guzzle PSR-7 Request - Custom exceptions for error handling: - - `WatiException` - Base exception - - `WatiApiException` - API error responses with status code and response data - - `AuthenticationException` - 401 errors - - `RateLimitException` - 429 errors with retry-after support - - `ValidationException` - 400/422 errors + - `WatiException` - Base exception + - `WatiApiException` - API error responses with status code and response data + - `AuthenticationException` - 401 errors + - `RateLimitException` - 429 errors with retry-after support + - `ValidationException` - 400/422 errors - Configurable HTTP options (timeout, connect_timeout, verify, proxy, debug) - Proper tenant ID handling in URLs with trailing slash preservation - Request path normalization for correct URI resolution with base URLs containing paths -- PHP 8.3+ support + +## 1.0.0 - 2026-02-20 + +- Initial release of Wati HTTP Client (PHP 8.3+ support) From 98a0eac0e50f7cb4444784626e2e8b9cd849f97d Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 01:18:57 +0100 Subject: [PATCH 5/9] Update readme --- AGENTS.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6891669..e39240b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; // Get this URL from your Wati Dashboard (API Docs section) -// It includes your tenant ID: https://live-mt-server.wati.io/{tenantId} +// It includes your tenant ID: https://your-instance.wati.io/{tenantId} $endpoint = 'https://live-mt-server.wati.io/372813'; $bearerToken = 'your-bearer-token'; diff --git a/README.md b/README.md index 4143df2..5e1116d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ use Wati\Http\WatiClient; use Wati\Http\WatiEnvironment; // Get this URL from your Wati Dashboard (API Docs section) -// It includes your tenant ID: https://live-mt-server.wati.io/{tenantId} +// It includes your tenant ID: https://your-instance.wati.io/{tenantId} $endpoint = "https://live-mt-server.wati.io/372813"; $bearerToken = "your-bearer-token"; From 6db9b6aabd5f105f18be01e3fccaca2b2e87b8b4 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 01:22:25 +0100 Subject: [PATCH 6/9] Change example urls --- AGENTS.md | 2 +- README.md | 2 +- src/WatiEnvironment.php | 2 +- tests/ExceptionsTest.php | 20 ++++++++++---------- tests/WatiClientTest.php | 26 +++++++++++++------------- tests/WatiEnvironmentTest.php | 24 ++++++++++++------------ 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e39240b..d1b0d4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ use Wati\Http\WatiEnvironment; // Get this URL from your Wati Dashboard (API Docs section) // It includes your tenant ID: https://your-instance.wati.io/{tenantId} -$endpoint = 'https://live-mt-server.wati.io/372813'; +$endpoint = 'https://your-instance.wati.io/123456'; $bearerToken = 'your-bearer-token'; $environment = new WatiEnvironment($endpoint, $bearerToken); diff --git a/README.md b/README.md index 5e1116d..809a976 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ use Wati\Http\WatiEnvironment; // Get this URL from your Wati Dashboard (API Docs section) // It includes your tenant ID: https://your-instance.wati.io/{tenantId} -$endpoint = "https://live-mt-server.wati.io/372813"; +$endpoint = "https://your-instance.wati.io/123456"; $bearerToken = "your-bearer-token"; // Create environment diff --git a/src/WatiEnvironment.php b/src/WatiEnvironment.php index 80294db..ab0d152 100644 --- a/src/WatiEnvironment.php +++ b/src/WatiEnvironment.php @@ -14,7 +14,7 @@ class WatiEnvironment * Create a new Wati environment. * * @param string $endpoint API endpoint URL from Wati dashboard (includes tenant ID). - * Example: https://live-mt-server.wati.io/372813 + * Example: https://your-instance.wati.io/123456 * @param string $bearerToken The bearer token for authentication. */ public function __construct(string $endpoint, string $bearerToken) diff --git a/tests/ExceptionsTest.php b/tests/ExceptionsTest.php index 065b6ee..103ad7c 100644 --- a/tests/ExceptionsTest.php +++ b/tests/ExceptionsTest.php @@ -22,7 +22,7 @@ use Wati\Http\WatiRequest; it('throws AuthenticationException on 401', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -35,7 +35,7 @@ })->throws(AuthenticationException::class); it('throws RateLimitException on 429', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -48,7 +48,7 @@ })->throws(RateLimitException::class); it('throws ValidationException on 400', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -61,7 +61,7 @@ })->throws(ValidationException::class); it('throws ValidationException on 422', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -74,7 +74,7 @@ })->throws(ValidationException::class); it('throws WatiApiException on 404', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -87,7 +87,7 @@ })->throws(WatiApiException::class); it('throws WatiApiException on 500', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -100,7 +100,7 @@ })->throws(WatiApiException::class); it('includes response data in exception', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -119,7 +119,7 @@ }); it('includes retry-after in RateLimitException', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $mockHandler = new MockHandler([ @@ -137,7 +137,7 @@ }); it('throws WatiException on connection failure', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $connectException = new ConnectException( @@ -159,7 +159,7 @@ }); it('throws WatiException on generic guzzle error', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $guzzleException = new class('Too many redirects') extends Exception implements GuzzleException {}; diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index ed1529d..bb00333 100644 --- a/tests/WatiClientTest.php +++ b/tests/WatiClientTest.php @@ -38,13 +38,13 @@ function createMockClient(): array } it('can create a client', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); expect($client->getEnvironment())->toBe($env); }); it('has authorization header', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; @@ -55,7 +55,7 @@ function createMockClient(): array }); it('injects sdk headers', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); /** @var Client $mockClient */ /** @var MockHandler $mockHandler */ @@ -75,7 +75,7 @@ function createMockClient(): array }); it('can execute a request', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); /** @var Client $mockClient */ [$mockClient] = createMockClient(); @@ -88,42 +88,42 @@ function createMockClient(): array }); it('accepts custom timeout option', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, ['timeout' => 60]); expect($client->getEnvironment())->toBe($env); }); it('accepts custom connect_timeout option', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, ['connect_timeout' => 20]); expect($client->getEnvironment())->toBe($env); }); it('accepts verify ssl option', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, ['verify' => false]); expect($client->getEnvironment())->toBe($env); }); it('accepts proxy option', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, ['proxy' => 'tcp://localhost:8080']); expect($client->getEnvironment())->toBe($env); }); it('accepts debug option', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, ['debug' => true]); expect($client->getEnvironment())->toBe($env); }); it('accepts multiple options', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env, [ 'timeout' => 60, 'connect_timeout' => 20, @@ -135,7 +135,7 @@ function createMockClient(): array }); it('normalizes request path by removing leading slash', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); /** @var Client $mockClient */ /** @var MockHandler $mockHandler */ @@ -151,7 +151,7 @@ function createMockClient(): array }); it('preserves tenant id in url when base url contains path', function (): void { - $env = new WatiEnvironment('https://live-mt-server.wati.io/372813', 'test-token'); + $env = new WatiEnvironment('https://your-instance.wati.io/123456', 'test-token'); $client = new WatiClient($env); // Create a client that captures the effective URL @@ -177,5 +177,5 @@ function createMockClient(): array $request = new class('GET', '/api/v1/getContacts') extends WatiRequest {}; $client->send($request); - expect($capturedUrl)->toBe('https://live-mt-server.wati.io/372813/api/v1/getContacts'); + expect($capturedUrl)->toBe('https://your-instance.wati.io/123456/api/v1/getContacts'); }); diff --git a/tests/WatiEnvironmentTest.php b/tests/WatiEnvironmentTest.php index 7deb02a..7af9150 100644 --- a/tests/WatiEnvironmentTest.php +++ b/tests/WatiEnvironmentTest.php @@ -7,40 +7,40 @@ use Wati\Http\WatiEnvironment; it('can create an environment', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-bearer-token'); - expect($env->baseUrl())->toBe('https://example.wati.io') + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://your-instance.wati.io') ->and($env->bearerToken())->toBe('test-bearer-token'); }); it('returns authorization string', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'test-bearer-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'test-bearer-token'); expect($env->authorizationString())->toBe('Bearer test-bearer-token'); }); it('trims trailing slash from endpoint', function (): void { - $env = new WatiEnvironment('https://example.wati.io/', 'test-bearer-token'); - expect($env->baseUrl())->toBe('https://example.wati.io'); + $env = new WatiEnvironment('https://your-instance.wati.io/', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://your-instance.wati.io'); }); it('adds trailing slash to endpoint with tenant id path', function (): void { - $env = new WatiEnvironment('https://live-mt-server.wati.io/372813', 'test-bearer-token'); - expect($env->baseUrl())->toBe('https://live-mt-server.wati.io/372813/'); + $env = new WatiEnvironment('https://your-instance.wati.io/123456', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://your-instance.wati.io/123456/'); }); it('preserves trailing slash on endpoint with tenant id path', function (): void { - $env = new WatiEnvironment('https://live-mt-server.wati.io/372813/', 'test-bearer-token'); - expect($env->baseUrl())->toBe('https://live-mt-server.wati.io/372813/'); + $env = new WatiEnvironment('https://your-instance.wati.io/123456/', 'test-bearer-token'); + expect($env->baseUrl())->toBe('https://your-instance.wati.io/123456/'); }); it('strips Bearer prefix from token', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'Bearer my-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'Bearer my-token'); expect($env->authorizationString())->toBe('Bearer my-token'); }); it('strips lowercase bearer prefix from token', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'bearer my-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'bearer my-token'); expect($env->authorizationString())->toBe('Bearer my-token'); }); it('accepts token without bearer prefix', function (): void { - $env = new WatiEnvironment('https://example.wati.io', 'my-token'); + $env = new WatiEnvironment('https://your-instance.wati.io', 'my-token'); expect($env->authorizationString())->toBe('Bearer my-token'); }); From e4dac2c7e160340d04ac0e560b37db19e2b84ded Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 17:09:42 +0100 Subject: [PATCH 7/9] Handle exceptions --- src/WatiClient.php | 8 +------- tests/ExceptionsTest.php | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/WatiClient.php b/src/WatiClient.php index 009c1f6..f22768d 100644 --- a/src/WatiClient.php +++ b/src/WatiClient.php @@ -83,18 +83,12 @@ public function send(RequestInterface $request): ResponseInterface $response, $e ); - } catch (ConnectException $e) { + } catch (ConnectException|GuzzleException $e) { throw new WatiException( 'Failed to connect to Wati API: '.$e->getMessage(), 0, $e ); - } catch (GuzzleException $e) { - throw new WatiException( - 'HTTP request failed: '.$e->getMessage(), - 0, - $e - ); } } diff --git a/tests/ExceptionsTest.php b/tests/ExceptionsTest.php index 103ad7c..8ddd656 100644 --- a/tests/ExceptionsTest.php +++ b/tests/ExceptionsTest.php @@ -172,7 +172,7 @@ try { $client->send($request); } catch (WatiException $e) { - expect($e->getMessage())->toContain('HTTP request failed') + expect($e->getMessage())->toContain('Failed to connect to Wati API: Too many redirects') ->and($e->getPrevious())->toBeInstanceOf(GuzzleException::class); } }); From 5427e5e4dcd7c27055f527212da3ea2134e51472 Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 17:19:20 +0100 Subject: [PATCH 8/9] Fx --- src/WatiClient.php | 4 ++-- tests/WatiClientTest.php | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/WatiClient.php b/src/WatiClient.php index f22768d..45419bc 100644 --- a/src/WatiClient.php +++ b/src/WatiClient.php @@ -83,7 +83,7 @@ public function send(RequestInterface $request): ResponseInterface $response, $e ); - } catch (ConnectException|GuzzleException $e) { + } catch (ConnectException|GuzzleException $e) { throw new WatiException( 'Failed to connect to Wati API: '.$e->getMessage(), 0, @@ -105,7 +105,7 @@ protected function normalizeRequestPath(RequestInterface $request): RequestInter return $request; } - public function hasAuthHeader(RequestInterface $request): bool + protected function hasAuthHeader(RequestInterface $request): bool { return array_key_exists('Authorization', $request->getHeaders()); } diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index bb00333..6657f0c 100644 --- a/tests/WatiClientTest.php +++ b/tests/WatiClientTest.php @@ -43,18 +43,27 @@ function createMockClient(): array expect($client->getEnvironment())->toBe($env); }); -it('has authorization header', function (): void { +it('injects sdk headers', function (): void { $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); + /** @var Client $mockClient */ + /** @var MockHandler $mockHandler */ + [$mockClient, $mockHandler] = createMockClient(); + $client->setClient($mockClient); $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; - expect($client->hasAuthHeader($request))->toBeFalse(); + $client->send($request); - $request = $request->withHeader('Authorization', 'Bearer test'); - expect($client->hasAuthHeader($request))->toBeTrue(); + $lastRequest = $mockHandler->getLastRequest(); + assert($lastRequest !== null); + expect($lastRequest->getHeaderLine('Authorization'))->toBe('Bearer test-token') + ->and($lastRequest->getHeaderLine('Wati-SDK-Name'))->toBe('wati-http-client') + ->and($lastRequest->getHeaderLine('Wati-SDK-Version'))->toBe('1.0.0') + ->and($lastRequest->getHeaderLine('Wati-SDK-Language'))->toBe('PHP') + ->and($lastRequest->getHeaderLine('User-Agent'))->toBe('WatiHttp-PHP HTTP/1.1'); }); -it('injects sdk headers', function (): void { +it('does not overwrite custom authorization header', function (): void { $env = new WatiEnvironment('https://your-instance.wati.io', 'test-token'); $client = new WatiClient($env); /** @var Client $mockClient */ @@ -63,15 +72,12 @@ function createMockClient(): array $client->setClient($mockClient); $request = new class('GET', '/api/v1/contacts') extends WatiRequest {}; + $request = $request->withHeader('Authorization', 'Custom-Token'); $client->send($request); $lastRequest = $mockHandler->getLastRequest(); assert($lastRequest !== null); - expect($lastRequest->getHeaderLine('Authorization'))->toBe('Bearer test-token') - ->and($lastRequest->getHeaderLine('Wati-SDK-Name'))->toBe('wati-http-client') - ->and($lastRequest->getHeaderLine('Wati-SDK-Version'))->toBe('1.0.0') - ->and($lastRequest->getHeaderLine('Wati-SDK-Language'))->toBe('PHP') - ->and($lastRequest->getHeaderLine('User-Agent'))->toBe('WatiHttp-PHP HTTP/1.1'); + expect($lastRequest->getHeaderLine('Authorization'))->toBe('Custom-Token'); }); it('can execute a request', function (): void { From cca1159612d8851e3e060ab39f5d91dacc56a91f Mon Sep 17 00:00:00 2001 From: Mohammed Elhaouari Date: Sat, 21 Feb 2026 17:32:05 +0100 Subject: [PATCH 9/9] Add test --- phpunit.xml | 1 + src/WatiClient.php | 12 ++++++++++-- src/WatiEnvironment.php | 8 +++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 1a9ed6f..1ec04a3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,7 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" + cacheDirectory=".phpunit.cache" > diff --git a/src/WatiClient.php b/src/WatiClient.php index 45419bc..5bad568 100644 --- a/src/WatiClient.php +++ b/src/WatiClient.php @@ -21,7 +21,15 @@ class WatiClient { protected Client $client; - /** @var array */ + /** + * @var array{ + * timeout: int, + * connect_timeout: int, + * verify: bool, + * debug: bool, + * proxy: null|string + * } + */ protected array $defaultOptions = [ 'timeout' => 30, 'connect_timeout' => 10, @@ -31,7 +39,7 @@ class WatiClient ]; /** - * @param array $options + * @param array $options */ public function __construct( protected WatiEnvironment $environment, diff --git a/src/WatiEnvironment.php b/src/WatiEnvironment.php index ab0d152..5cb4266 100644 --- a/src/WatiEnvironment.php +++ b/src/WatiEnvironment.php @@ -6,9 +6,11 @@ class WatiEnvironment { - protected string $endpoint; + const int BEARER_PREFIX_LENGTH = 7; // strlen('bearer ') - protected string $bearerToken; + protected readonly string $endpoint; + + protected readonly string $bearerToken; /** * Create a new Wati environment. @@ -21,7 +23,7 @@ public function __construct(string $endpoint, string $bearerToken) { // Normalize bearer token: strip "Bearer " prefix if present if (str_starts_with(strtolower($bearerToken), 'bearer ')) { - $bearerToken = substr($bearerToken, strlen('bearer ')); + $bearerToken = substr($bearerToken, self::BEARER_PREFIX_LENGTH); } $this->bearerToken = $bearerToken;