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 cfea230..d1b0d4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,13 +16,27 @@ 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://your-instance.wati.io/{tenantId} +$endpoint = 'https://your-instance.wati.io/123456'; $bearerToken = 'your-bearer-token'; $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 +79,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..809a976 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 @@ -32,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://your-instance.wati.io/{tenantId} +$endpoint = "https://your-instance.wati.io/123456"; $bearerToken = "your-bearer-token"; // Create environment @@ -43,6 +45,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 +137,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/changelog.md b/changelog.md index 7c38f83..d8f9217 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,18 @@ All notable changes to `phpjuice/wati-http-client` will be documented in this file. +## 1.0.1 - 2026-02-20 + +- 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 + ## 1.0.0 - 2026-02-20 - Initial release of Wati HTTP Client (PHP 8.3+ support) 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/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{ + * timeout: int, + * connect_timeout: int, + * verify: bool, + * debug: bool, + * proxy: null|string + * } + */ + 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 { + $request = $this->normalizeRequestPath($request); + if (! $this->hasAuthHeader($request)) { $request = $request->withHeader('Authorization', $this->environment->authorizationString()); } @@ -32,10 +66,54 @@ 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|GuzzleException $e) { + throw new WatiException( + 'Failed to connect to Wati API: '.$e->getMessage(), + 0, + $e + ); + } + } + + 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 + protected function hasAuthHeader(RequestInterface $request): bool { return array_key_exists('Authorization', $request->getHeaders()); } @@ -48,9 +126,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('sdk_tech_stack', 'PHP '.PHP_VERSION); + ->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/src/WatiEnvironment.php b/src/WatiEnvironment.php index 2d4aa04..5cb4266 100644 --- a/src/WatiEnvironment.php +++ b/src/WatiEnvironment.php @@ -6,11 +6,36 @@ class WatiEnvironment { - protected string $endpoint; + const int BEARER_PREFIX_LENGTH = 7; // strlen('bearer ') - public function __construct(string $endpoint, protected string $bearerToken) + protected readonly string $endpoint; + + protected readonly string $bearerToken; + + /** + * Create a new Wati environment. + * + * @param string $endpoint API endpoint URL from Wati dashboard (includes tenant ID). + * Example: https://your-instance.wati.io/123456 + * @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, self::BEARER_PREFIX_LENGTH); + } + $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 new file mode 100644 index 0000000..8ddd656 --- /dev/null +++ b/tests/ExceptionsTest.php @@ -0,0 +1,178 @@ + '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://your-instance.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://your-instance.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://your-instance.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://your-instance.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://your-instance.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://your-instance.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://your-instance.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); + } +}); + +it('throws WatiException on connection failure', function (): void { + $env = new WatiEnvironment('https://your-instance.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://your-instance.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('Failed to connect to Wati API: Too many redirects') + ->and($e->getPrevious())->toBeInstanceOf(GuzzleException::class); + } +}); diff --git a/tests/WatiClientTest.php b/tests/WatiClientTest.php index 355307e..6657f0c 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; @@ -36,24 +38,33 @@ 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'); +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 { - $env = new WatiEnvironment('https://example.wati.io', 'test-token'); +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 */ /** @var MockHandler $mockHandler */ @@ -61,18 +72,16 @@ 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('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'); + expect($lastRequest->getHeaderLine('Authorization'))->toBe('Custom-Token'); }); 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(); @@ -83,3 +92,96 @@ function createMockClient(): array expect($response->getStatusCode())->toBe(200); }); + +it('accepts custom timeout option', function (): void { + $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://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://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://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://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://your-instance.wati.io', 'test-token'); + $client = new WatiClient($env, [ + 'timeout' => 60, + 'connect_timeout' => 20, + 'verify' => true, + 'debug' => false, + ]); + + expect($client->getEnvironment())->toBe($env); +}); + +it('normalizes request path by removing leading slash', 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 {}; + $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://your-instance.wati.io/123456', '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://your-instance.wati.io/123456/api/v1/getContacts'); +}); diff --git a/tests/WatiEnvironmentTest.php b/tests/WatiEnvironmentTest.php index f21f3d6..7af9150 100644 --- a/tests/WatiEnvironmentTest.php +++ b/tests/WatiEnvironmentTest.php @@ -7,17 +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://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://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://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://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://your-instance.wati.io', 'my-token'); + expect($env->authorizationString())->toBe('Bearer my-token'); });