From 8bd6d1954ff1167d5e566ed4b1d200dcc2dd8b6f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:26:49 +0100 Subject: [PATCH] Migrate HTTP layer to utopia-php/client Replace the hand-rolled cURL in Adapter::request()/requestMulti() with the utopia-php/client PSR-18 client, and have every adapter consume the PSR-7 response directly (getStatusCode/getBody/getHeaderLine) instead of the old custom result array. - request() builds a PSR-7 request via the request factory (json/form/ multipart chosen from Content-Type) and returns ResponseInterface. - requestMulti() sends sequentially over one HTTP/2, connection-reused client (APNs requires HTTP/2) and returns responses in request order; callers map results by position. - Mailgun attachments now use Psr7 multipart Part::file instead of curl_file_create; fixed a latent Vonage associative-header bug. - Test stubs (SESStub/ResendStub) return Utopia\Psr7\Response. utopia-php/client requires PHP 8.4, so bump composer (require + platform), the Dockerfile to php:8.4, and phpstan to ^2 (needed to parse the client's 8.4 syntax). Also fix a PHP 8.4 implicit-nullable deprecation in JWT. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 2 +- composer.json | 7 +- composer.lock | 176 +++++++++++++- src/Utopia/Messaging/Adapter.php | 219 ++++++------------ src/Utopia/Messaging/Adapter/Chat/Discord.php | 2 +- .../Messaging/Adapter/Email/Mailgun.php | 17 +- src/Utopia/Messaging/Adapter/Email/Resend.php | 26 +-- src/Utopia/Messaging/Adapter/Email/SES.php | 49 ++-- .../Messaging/Adapter/Email/Sendgrid.php | 11 +- src/Utopia/Messaging/Adapter/Push/APNS.php | 11 +- src/Utopia/Messaging/Adapter/Push/FCM.php | 17 +- .../Messaging/Adapter/SMS/Clickatell.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Fast2SMS.php | 4 +- src/Utopia/Messaging/Adapter/SMS/Infobip.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Inforu.php | 6 +- src/Utopia/Messaging/Adapter/SMS/Mock.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Msg91.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Plivo.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Seven.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Sinch.php | 2 +- src/Utopia/Messaging/Adapter/SMS/Telesign.php | 7 +- src/Utopia/Messaging/Adapter/SMS/Telnyx.php | 2 +- .../Messaging/Adapter/SMS/TextMagic.php | 7 +- src/Utopia/Messaging/Adapter/SMS/Twilio.php | 7 +- src/Utopia/Messaging/Adapter/SMS/Vonage.php | 12 +- src/Utopia/Messaging/Helpers/JWT.php | 2 +- tests/Messaging/Adapter/Chat/DiscordTest.php | 4 +- .../Adapter/Email/ResendRoutingTest.php | 16 +- .../Adapter/Email/SESRoutingTest.php | 22 +- 29 files changed, 361 insertions(+), 279 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5bc1e5e3..98f4903a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer install \ --no-scripts \ --prefer-dist -FROM php:8.3.11-cli-alpine3.20 +FROM php:8.4-cli-alpine WORKDIR /usr/local/src/ diff --git a/composer.json b/composer.json index 261c6cc1..191ee326 100644 --- a/composer.json +++ b/composer.json @@ -22,21 +22,22 @@ } }, "require": { - "php": ">=8.1.0", + "php": ">=8.4", "ext-curl": "*", "ext-openssl": "*", "phpmailer/phpmailer": "6.9.1", "giggsey/libphonenumber-for-php-lite": "9.0.23", + "utopia-php/client": "^0.1.3", "utopia-php/telemetry": "^0.4" }, "require-dev": { "phpunit/phpunit": "11.*", "laravel/pint": "1.*", - "phpstan/phpstan": "1.*" + "phpstan/phpstan": "^2.0" }, "config": { "platform": { - "php": "8.3" + "php": "8.4" }, "allow-plugins": { "php-http/discovery": true, diff --git a/composer.lock b/composer.lock index ba1f9046..90e47684 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "94123f2a566774952009659b7e6f241f", + "content-hash": "c9f4636738c26211dee548812bb3a769", "packages": [ { "name": "brick/math", @@ -2034,6 +2034,155 @@ }, "time": "2025-06-29T15:42:06+00:00" }, + { + "name": "utopia-php/client", + "version": "0.1.3", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/client.git", + "reference": "95f32cea3cec81aaa52ab4e9d946a558b70ed7a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/client/zipball/95f32cea3cec81aaa52ab4e9d946a558b70ed7a8", + "reference": "95f32cea3cec81aaa52ab4e9d946a558b70ed7a8", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "utopia-php/pools": "^1.0", + "utopia-php/span": "^1.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "swoole/ide-helper": "^6.0" + }, + "suggest": { + "ext-curl": "Required to use the cURL HTTP client adapter.", + "ext-simplexml": "Required to decode XML responses with Response::xml().", + "ext-swoole": "Required to use the Swoole coroutine HTTP client adapter." + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight PSR-18 HTTP client with cURL and Swoole coroutine backends", + "keywords": [ + "client", + "curl", + "http", + "php", + "psr-18", + "swoole", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/client/issues", + "source": "https://github.com/utopia-php/client/tree/0.1.3" + }, + "time": "2026-06-20T10:46:00+00:00" + }, + { + "name": "utopia-php/pools", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/pools.git", + "reference": "b685ca01883ed820b9898b85163a8f3d970a2da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/pools/zipball/b685ca01883ed820b9898b85163a8f3d970a2da7", + "reference": "b685ca01883ed820b9898b85163a8f3d970a2da7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "utopia-php/telemetry": "^0.4" + }, + "require-dev": { + "swoole/ide-helper": "6.*" + }, + "suggest": { + "ext-swoole": "Required to use the Swoole pool adapter" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Pools\\": "src/Pools" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "description": "A simple library to manage connection pools", + "keywords": [ + "framework", + "php", + "pools", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/pools/issues", + "source": "https://github.com/utopia-php/pools/tree/1.0.8" + }, + "time": "2026-06-20T09:45:06+00:00" + }, + { + "name": "utopia-php/span", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/span.git", + "reference": "d11f2714324cb22b286b2afbf3ea9de32c68de83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/span/zipball/d11f2714324cb22b286b2afbf3ea9de32c68de83", + "reference": "d11f2714324cb22b286b2afbf3ea9de32c68de83", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "swoole/ide-helper": "^5.0" + }, + "suggest": { + "ext-swoole": "Required for coroutine-based storage" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Span\\": "src/Span/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple span tracing library for PHP with coroutine support", + "support": { + "issues": "https://github.com/utopia-php/span/issues", + "source": "https://github.com/utopia-php/span/tree/4.0.1" + }, + "time": "2026-06-20T09:45:06+00:00" + }, { "name": "utopia-php/telemetry", "version": "0.4.1", @@ -2395,15 +2544,15 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.33", + "version": "2.2.2", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", - "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -2422,6 +2571,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -2444,7 +2604,7 @@ "type": "github" } ], - "time": "2026-02-28T20:30:03+00:00" + "time": "2026-06-05T09:00:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3998,13 +4158,13 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1.0", + "php": ">=8.4", "ext-curl": "*", "ext-openssl": "*" }, "platform-dev": {}, "platform-overrides": { - "php": "8.3" + "php": "8.4" }, "plugin-api-version": "2.9.0" } diff --git a/src/Utopia/Messaging/Adapter.php b/src/Utopia/Messaging/Adapter.php index 62ae91c2..e8360a51 100644 --- a/src/Utopia/Messaging/Adapter.php +++ b/src/Utopia/Messaging/Adapter.php @@ -4,6 +4,12 @@ use Exception; use libphonenumber\PhoneNumberUtil; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Utopia\Client; +use Utopia\Client\Adapter\Curl\Client as CurlAdapter; +use Utopia\Psr7\Header; +use Utopia\Psr7\Request\Factory as RequestFactory; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; use Utopia\Telemetry\Counter; @@ -134,22 +140,15 @@ private function recordResponse(Message $message, array $response): void } /** - * Send a single HTTP request. + * Send a single HTTP request and return the client's PSR-7 response. * * @param string $method The HTTP method to use. * @param string $url The URL to send the request to. - * @param array $headers An array of headers to send with the request. + * @param array $headers Headers as "Key: value" strings. * @param array|null $body The body of the request. * @param int $timeout The timeout in seconds. - * @return array{ - * url: string, - * statusCode: int, - * response: array|string|null, - * headers: array, - * error: string|null - * } * - * @throws \Exception If the request fails. + * @throws \Psr\Http\Client\ClientExceptionInterface If the request fails at the transport level. */ protected function request( string $method, @@ -158,79 +157,20 @@ protected function request( ?array $body = null, int $timeout = 30, int $connectTimeout = 10 - ): array { - $ch = \curl_init(); - - foreach ($headers as $header) { - if (\str_contains($header, 'application/json')) { - $body = \json_encode($body); - break; - } - if (\str_contains($header, 'application/x-www-form-urlencoded')) { - $body = \http_build_query($body); - break; - } - } - - if (!\is_null($body)) { - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - - if (\is_string($body)) { - $headers[] = 'Content-Length: '.\strlen($body); - } - } - - $responseHeaders = []; - - \curl_setopt_array($ch, [ - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_URL => $url, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_USERAGENT => "Appwrite {$this->getName()} Message Sender", - CURLOPT_TIMEOUT => $timeout, - CURLOPT_CONNECTTIMEOUT => $connectTimeout, - CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$responseHeaders): int { - $parts = \explode(':', $header, 2); - if (\count($parts) === 2) { - $responseHeaders[\strtolower(\trim($parts[0]))] = \trim($parts[1]); - } - - return \strlen($header); - }, - ]); - - $response = \curl_exec($ch); - - try { - $response = \json_decode($response, true, flags: JSON_THROW_ON_ERROR); - } catch (\JsonException) { - // Ignore - } - - return [ - 'url' => $url, - 'statusCode' => \curl_getinfo($ch, CURLINFO_RESPONSE_CODE), - 'response' => $response, - 'headers' => $responseHeaders, - 'error' => \curl_error($ch), - ]; + ): ResponseInterface { + return $this->client($timeout, $connectTimeout) + ->sendRequest($this->buildRequest($method, $url, $headers, $body)); } /** - * Send multiple concurrent HTTP requests using HTTP/2 multiplexing. + * Send multiple HTTP requests over a single kept-alive HTTP/2 connection. + * Responses are returned in request order, so the Nth response corresponds + * to the Nth recipient. * * @param array $urls - * @param array $headers + * @param array $headers Headers as "Key: value" strings. * @param array> $bodies - * @return array|null, - * headers: array, - * error: string|null - * }> + * @return array * * @throws Exception */ @@ -246,40 +186,6 @@ protected function requestMulti( throw new \Exception('No URLs provided. Must provide at least one URL.'); } - foreach ($headers as $header) { - if (\str_contains($header, 'application/json')) { - foreach ($bodies as $i => $body) { - $bodies[$i] = \json_encode($body); - } - break; - } - if (\str_contains($header, 'application/x-www-form-urlencoded')) { - foreach ($bodies as $i => $body) { - $bodies[$i] = \http_build_query($body); - } - break; - } - } - - $sh = \curl_share_init(); - $mh = \curl_multi_init(); - $ch = \curl_init(); - - \curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); - \curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); - - \curl_setopt_array($ch, [ - CURLOPT_SHARE => $sh, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FORBID_REUSE => false, - CURLOPT_FRESH_CONNECT => false, - CURLOPT_TIMEOUT => $timeout, - CURLOPT_CONNECTTIMEOUT => $connectTimeout, - ]); - $urlCount = \count($urls); $bodyCount = \count($bodies); @@ -293,61 +199,68 @@ protected function requestMulti( $urls = \array_pad($urls, $bodyCount, $urls[0]); } - for ($i = 0; $i < \count($urls); $i++) { - if (!empty($bodies[$i])) { - $headers[] = 'Content-Length: '.\strlen($bodies[$i]); - } + $client = $this->client($timeout, $connectTimeout, multi: true); - \curl_setopt($ch, CURLOPT_URL, $urls[$i]); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $bodies[$i]); - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - \curl_setopt($ch, CURLOPT_PRIVATE, $i); - \curl_multi_add_handle($mh, \curl_copy_handle($ch)); + $responses = []; + foreach ($urls as $i => $url) { + $responses[] = $client->sendRequest($this->buildRequest($method, $url, $headers, $bodies[$i])); } - $active = true; - do { - $status = \curl_multi_exec($mh, $active); + return $responses; + } - if ($active) { - \curl_multi_select($mh); - } - } while ($active && $status == CURLM_OK); + /** + * Build a client carrying the adapter's user agent and timeouts. When + * $multi is set the cURL transport negotiates HTTP/2 and keeps the + * connection alive so a batch of requests to the same host reuses it. + */ + private function client(int $timeout, int $connectTimeout, bool $multi = false): Client + { + $adapter = new CurlAdapter( + options: $multi ? [\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_2_0] : [], + ); + + return (new Client($adapter)) + ->withTimeout((float) $timeout) + ->withConnectTimeout((float) $connectTimeout) + ->withConnectionReuse($multi) + ->withHeaders([Header::USER_AGENT => "Appwrite {$this->getName()} Message Sender"]); + } - $responses = []; + /** + * Translate the legacy "Key: value" header list and body array into a + * PSR-7 request, picking the body encoding from the Content-Type header. + * + * @param array $headers + * @param array|null $body + */ + private function buildRequest(string $method, string $url, array $headers, ?array $body): RequestInterface + { + $factory = new RequestFactory(); + $contentType = ''; + $map = []; - // Check each handle's result - while ($info = \curl_multi_info_read($mh)) { - $ch = $info['handle']; + foreach ($headers as $header) { + [$key, $value] = \array_pad(\explode(':', $header, 2), 2, ''); + $key = \trim($key); + $value = \trim($value); - $response = \curl_multi_getcontent($ch); + if (\strtolower($key) === 'content-type') { + $contentType = \strtolower($value); - try { - $response = \json_decode($response, true, flags: JSON_THROW_ON_ERROR); - } catch (\JsonException) { - // Ignore + continue; } - $responses[] = [ - 'index' => (int)\curl_getinfo($ch, CURLINFO_PRIVATE), - 'url' => \curl_getinfo($ch, CURLINFO_EFFECTIVE_URL), - 'statusCode' => \curl_getinfo($ch, CURLINFO_RESPONSE_CODE), - 'response' => $response, - // Kept in sync with request()'s shape. Response headers are not - // captured here: this path copies a configured handle with - // curl_copy_handle(), and copying a handle that carries a - // CURLOPT_HEADERFUNCTION closure segfaults. Wire up per-handle - // capture (without copy_handle) if a multi-path adapter needs it. - 'headers' => [], - 'error' => \curl_error($ch), - ]; - - \curl_multi_remove_handle($mh, $ch); + $map[$key] = $value; } - \curl_share_close($sh); + $body ??= []; - return $responses; + return match (true) { + \str_contains($contentType, 'application/x-www-form-urlencoded') => $factory->form($method, $url, $body, $map), + \str_contains($contentType, 'multipart/form-data') => $factory->multipart($method, $url, $body, $map), + default => $factory->json($method, $url, $body, $map), + }; } diff --git a/src/Utopia/Messaging/Adapter/Chat/Discord.php b/src/Utopia/Messaging/Adapter/Chat/Discord.php index d0f76ed3..445f18fc 100644 --- a/src/Utopia/Messaging/Adapter/Chat/Discord.php +++ b/src/Utopia/Messaging/Adapter/Chat/Discord.php @@ -114,7 +114,7 @@ protected function process(DiscordMessage $message): array ], ); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { $response->setDeliveredTo(1); diff --git a/src/Utopia/Messaging/Adapter/Email/Mailgun.php b/src/Utopia/Messaging/Adapter/Email/Mailgun.php index c166f7b1..260d63f7 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mailgun.php +++ b/src/Utopia/Messaging/Adapter/Email/Mailgun.php @@ -5,6 +5,7 @@ use Utopia\Messaging\Adapter\Email as EmailAdapter; use Utopia\Messaging\Messages\Email as EmailMessage; use Utopia\Messaging\Response; +use Utopia\Psr7\Request\Multipart\Part; class Mailgun extends EmailAdapter { @@ -117,10 +118,11 @@ protected function process(EmailMessage $message): array foreach ($message->getAttachments() as $index => $attachment) { $isMultipart = true; - $body["attachment[$index]"] = \curl_file_create( + $body["attachment[$index]"] = Part::file( + 'attachment', $attachment->getPath(), - $attachment->getType(), $attachment->getName(), + $attachment->getType(), ); } } @@ -144,7 +146,7 @@ protected function process(EmailMessage $message): array body: $body, ); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { $response->setDeliveredTo(\count($message->getTo())); @@ -152,11 +154,12 @@ protected function process(EmailMessage $message): array $response->addResult($to['email']); } } elseif ($statusCode >= 400 && $statusCode < 500) { + $content = \json_decode((string) $result->getBody(), true); foreach ($message->getTo() as $to) { - if (\is_string($result['response'])) { - $response->addResult($to['email'], $result['response']); - } elseif (isset($result['response']['message'])) { - $response->addResult($to['email'], $result['response']['message']); + if (isset($content['message'])) { + $response->addResult($to['email'], $content['message']); + } elseif ((string) $result->getBody() !== '') { + $response->addResult($to['email'], (string) $result->getBody()); } else { $response->addResult($to['email'], 'Unknown error'); } diff --git a/src/Utopia/Messaging/Adapter/Email/Resend.php b/src/Utopia/Messaging/Adapter/Email/Resend.php index 3624269c..59701272 100644 --- a/src/Utopia/Messaging/Adapter/Email/Resend.php +++ b/src/Utopia/Messaging/Adapter/Email/Resend.php @@ -2,6 +2,7 @@ namespace Utopia\Messaging\Adapter\Email; +use Psr\Http\Message\ResponseInterface; use Utopia\Messaging\Adapter\Email as EmailAdapter; use Utopia\Messaging\Messages\Email as EmailMessage; use Utopia\Messaging\Response; @@ -156,10 +157,10 @@ private function sendBatch(EmailMessage $message, array $emails, array $headers, body: $emails, ); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode === 200) { - $responseData = $result['response']; + $responseData = \json_decode((string) $result->getBody(), true); if (\is_array($responseData) && isset($responseData['errors']) && ! empty($responseData['errors'])) { $failedIndices = []; @@ -184,13 +185,13 @@ private function sendBatch(EmailMessage $message, array $emails, array $headers, } } } elseif ($statusCode >= 400 && $statusCode < 500) { - $errorMessage = $this->extractErrorMessage($result['response'], 'Unknown error'); + $errorMessage = $this->extractErrorMessage($result, 'Unknown error'); foreach ($message->getTo() as $to) { $response->addResult($to['email'], $errorMessage); } } elseif ($statusCode >= 500) { - $errorMessage = $this->extractErrorMessage($result['response'], 'Server error'); + $errorMessage = $this->extractErrorMessage($result, 'Server error'); foreach ($message->getTo() as $to) { $response->addResult($to['email'], $errorMessage); @@ -220,16 +221,16 @@ private function sendIndividually(EmailMessage $message, array $emails, array $h body: $email, ); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { $response->addResult($to['email']); $deliveredTo++; } elseif ($statusCode >= 400 && $statusCode < 500) { - $errorMessage = $this->extractErrorMessage($result['response'], 'Unknown error'); + $errorMessage = $this->extractErrorMessage($result, 'Unknown error'); $response->addResult($to['email'], $errorMessage); } else { - $errorMessage = $this->extractErrorMessage($result['response'], 'Server error'); + $errorMessage = $this->extractErrorMessage($result, 'Server error'); $response->addResult($to['email'], $errorMessage); } } @@ -239,14 +240,9 @@ private function sendIndividually(EmailMessage $message, array $emails, array $h return $response->toArray(); } - /** - * @param array|string|null $body - */ - private function extractErrorMessage(array|string|null $body, string $default): string + private function extractErrorMessage(ResponseInterface $result, string $default): string { - if (\is_string($body)) { - return $body; - } + $body = \json_decode((string) $result->getBody(), true); if (\is_array($body)) { if (isset($body['message']) && \is_string($body['message'])) { @@ -256,6 +252,8 @@ private function extractErrorMessage(array|string|null $body, string $default): if (isset($body['error']) && \is_string($body['error'])) { return $body['error']; } + } elseif ((string) $result->getBody() !== '') { + return (string) $result->getBody(); } return $default; diff --git a/src/Utopia/Messaging/Adapter/Email/SES.php b/src/Utopia/Messaging/Adapter/Email/SES.php index 18565938..19ca0c98 100644 --- a/src/Utopia/Messaging/Adapter/Email/SES.php +++ b/src/Utopia/Messaging/Adapter/Email/SES.php @@ -3,6 +3,7 @@ namespace Utopia\Messaging\Adapter\Email; use PHPMailer\PHPMailer\PHPMailer; +use Psr\Http\Message\ResponseInterface; use Utopia\Messaging\Adapter\Email as EmailAdapter; use Utopia\Messaging\Messages\Email as EmailMessage; use Utopia\Messaging\Response; @@ -245,7 +246,7 @@ private function sendRaw(EmailMessage $message, Response $response): array $result = $this->dispatch('POST', '/v2/email/outbound-emails', $body); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode >= 200 && $statusCode < 300) { $response->addResult($to['email']); @@ -267,13 +268,12 @@ private function sendRaw(EmailMessage $message, Response $response): array * marked failed with the SES error. On success each recipient is mapped * from its corresponding BulkEmailEntryResults entry. * - * @param array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} $result * @return array{deliveredTo: int, type: string, results: array>} */ - private function parseBulkResult(EmailMessage $message, array $result, Response $response): array + private function parseBulkResult(EmailMessage $message, ResponseInterface $result, Response $response): array { $recipients = $message->getTo(); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode < 200 || $statusCode >= 300) { $error = $this->errorMessage($result); @@ -284,8 +284,9 @@ private function parseBulkResult(EmailMessage $message, array $result, Response return $response->toArray(); } - $entryResults = \is_array($result['response']) - ? ($result['response']['BulkEmailEntryResults'] ?? null) + $body = \json_decode((string) $result->getBody(), true); + $entryResults = \is_array($body) + ? ($body['BulkEmailEntryResults'] ?? null) : null; if (! \is_array($entryResults)) { @@ -343,7 +344,7 @@ private function ensureTemplate(EmailMessage $message, string $templateName): vo 'TemplateContent' => $content, ]); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); $created = $statusCode >= 200 && $statusCode < 300; $alreadyExists = $this->errorType($result) === 'AlreadyExistsException'; @@ -384,9 +385,8 @@ private function templateName(EmailMessage $message): string * Whether a SendBulkEmail result indicates the referenced template is * missing, via either the top-level error or per-entry statuses. * - * @param array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} $result */ - private function isTemplateMissing(array $result): bool + private function isTemplateMissing(ResponseInterface $result): bool { $errorType = $this->errorType($result); if ($errorType === 'NotFoundException' || $errorType === 'BadRequestException') { @@ -402,8 +402,9 @@ private function isTemplateMissing(array $result): bool } } - $entryResults = \is_array($result['response'] ?? null) - ? ($result['response']['BulkEmailEntryResults'] ?? null) + $body = \json_decode((string) $result->getBody(), true); + $entryResults = \is_array($body) + ? ($body['BulkEmailEntryResults'] ?? null) : null; if (\is_array($entryResults)) { @@ -528,11 +529,10 @@ private function formatAddress(string $email, ?string $name): string * configured region. * * @param array $body - * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} * * @throws \Exception */ - private function dispatch(string $method, string $path, array $body): array + private function dispatch(string $method, string $path, array $body): ResponseInterface { $host = 'email.'.$this->region.'.amazonaws.com'; $payload = \json_encode($body, JSON_THROW_ON_ERROR); @@ -659,11 +659,10 @@ private function signingKey(string $dateStamp): string /** * Extract a human-readable error message from a SES error response. * - * @param array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} $result */ - private function errorMessage(array $result): string + private function errorMessage(ResponseInterface $result): string { - $body = $result['response']; + $body = \json_decode((string) $result->getBody(), true); if (\is_array($body)) { if (isset($body['message']) && \is_string($body['message'])) { @@ -674,12 +673,9 @@ private function errorMessage(array $result): string } } - if (\is_string($body) && $body !== '') { - return $body; - } - - if (! empty($result['error'])) { - return $result['error']; + $raw = (string) $result->getBody(); + if ($raw !== '') { + return $raw; } return 'Unknown error'; @@ -695,16 +691,15 @@ private function errorMessage(array $result): string * JSON-protocol responses instead carry it in a `__type` (optionally * "prefix#Type") or `code` body field, which is used as a fallback. * - * @param array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} $result */ - private function errorType(array $result): ?string + private function errorType(ResponseInterface $result): ?string { - $header = $result['headers']['x-amzn-errortype'] ?? null; - if (\is_string($header) && $header !== '') { + $header = $result->getHeaderLine('x-amzn-errortype'); + if ($header !== '') { return \trim(\explode(':', $header)[0]); } - $body = $result['response']; + $body = \json_decode((string) $result->getBody(), true); if (\is_array($body)) { $type = $body['__type'] ?? $body['code'] ?? null; if (\is_string($type)) { diff --git a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php index 4b1fd92a..a3de6a84 100644 --- a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php +++ b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php @@ -136,7 +136,7 @@ protected function process(EmailMessage $message): array body: $body, ); - $statusCode = $result['statusCode']; + $statusCode = $result->getStatusCode(); if ($statusCode === 202) { $response->setDeliveredTo(\count($message->getTo())); @@ -144,11 +144,12 @@ protected function process(EmailMessage $message): array $response->addResult($to['email']); } } else { + $content = \json_decode((string) $result->getBody(), true); foreach ($message->getTo() as $to) { - if (\is_string($result['response'])) { - $response->addResult($to['email'], $result['response']); - } elseif (!\is_null($result['response']['errors'][0]['message'] ?? null)) { - $response->addResult($to['email'], $result['response']['errors'][0]['message']); + if (!\is_null($content['errors'][0]['message'] ?? null)) { + $response->addResult($to['email'], $content['errors'][0]['message']); + } elseif ((string) $result->getBody() !== '') { + $response->addResult($to['email'], (string) $result->getBody()); } else { $response->addResult($to['email'], 'Unknown error'); } diff --git a/src/Utopia/Messaging/Adapter/Push/APNS.php b/src/Utopia/Messaging/Adapter/Push/APNS.php index 2e6e4b45..a7257381 100644 --- a/src/Utopia/Messaging/Adapter/Push/APNS.php +++ b/src/Utopia/Messaging/Adapter/Push/APNS.php @@ -123,9 +123,9 @@ public function process(PushMessage $message): array $response = new Response($this->getType()); - foreach ($results as $result) { - $device = \basename($result['url']); - $statusCode = $result['statusCode']; + foreach ($results as $index => $result) { + $device = $message->getTo()[$index]; + $statusCode = $result->getStatusCode(); switch ($statusCode) { case 200: @@ -133,9 +133,10 @@ public function process(PushMessage $message): array $response->addResult($device); break; default: - $error = ($result['response']['reason'] ?? null) === 'ExpiredToken' || ($result['response']['reason'] ?? null) === 'BadDeviceToken' + $body = \json_decode((string) $result->getBody(), true); + $error = ($body['reason'] ?? null) === 'ExpiredToken' || ($body['reason'] ?? null) === 'BadDeviceToken' ? $this->getExpiredErrorMessage() - : $result['response']['reason']; + : $body['reason'] ?? null; $response->addResult($device, $error); break; diff --git a/src/Utopia/Messaging/Adapter/Push/FCM.php b/src/Utopia/Messaging/Adapter/Push/FCM.php index 49866bd8..8e6e9904 100644 --- a/src/Utopia/Messaging/Adapter/Push/FCM.php +++ b/src/Utopia/Messaging/Adapter/Push/FCM.php @@ -81,7 +81,7 @@ protected function process(PushMessage $message): array ] ); - $accessToken = $token['response']['access_token']; + $accessToken = \json_decode((string) $token->getBody(), true)['access_token'] ?? null; $shared = []; @@ -161,18 +161,19 @@ protected function process(PushMessage $message): array $response = new Response($this->getType()); - foreach ($results as $result) { - if ($result['statusCode'] === 200) { + foreach ($results as $index => $result) { + if ($result->getStatusCode() === 200) { $response->incrementDeliveredTo(); - $response->addResult($message->getTo()[$result['index']]); + $response->addResult($message->getTo()[$index]); } else { + $body = \json_decode((string) $result->getBody(), true); $error = - ($result['response']['error']['status'] ?? null) === 'UNREGISTERED' - || ($result['response']['error']['status'] ?? null) === 'NOT_FOUND' + ($body['error']['status'] ?? null) === 'UNREGISTERED' + || ($body['error']['status'] ?? null) === 'NOT_FOUND' ? $this->getExpiredErrorMessage() - : $result['response']['error']['message'] ?? 'Unknown error'; + : $body['error']['message'] ?? 'Unknown error'; - $response->addResult($message->getTo()[$result['index']], $error); + $response->addResult($message->getTo()[$index], $error); } } diff --git a/src/Utopia/Messaging/Adapter/SMS/Clickatell.php b/src/Utopia/Messaging/Adapter/SMS/Clickatell.php index fd8d9a2b..e62aa44a 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Clickatell.php +++ b/src/Utopia/Messaging/Adapter/SMS/Clickatell.php @@ -55,7 +55,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Fast2SMS.php b/src/Utopia/Messaging/Adapter/SMS/Fast2SMS.php index e4378102..2df60a63 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Fast2SMS.php +++ b/src/Utopia/Messaging/Adapter/SMS/Fast2SMS.php @@ -99,8 +99,8 @@ protected function process(SMSMessage $message): array body: $payload ); - $res = $result['response']; - if ($result['statusCode'] === 200 && isset($res['return']) && $res['return'] === true) { + $res = \json_decode((string) $result->getBody(), true); + if ($result->getStatusCode() === 200 && isset($res['return']) && $res['return'] === true) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Infobip.php b/src/Utopia/Messaging/Adapter/SMS/Infobip.php index 25d61367..536a166f 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Infobip.php +++ b/src/Utopia/Messaging/Adapter/SMS/Infobip.php @@ -61,7 +61,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Inforu.php b/src/Utopia/Messaging/Adapter/SMS/Inforu.php index f97bb4d9..4112d084 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Inforu.php +++ b/src/Utopia/Messaging/Adapter/SMS/Inforu.php @@ -65,13 +65,15 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] === 200 && ($result['response']['StatusId'] ?? 0) === 1) { + $body = \json_decode((string) $result->getBody(), true); + + if ($result->getStatusCode() === 200 && ($body['StatusId'] ?? 0) === 1) { $response->setDeliveredTo(count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); } } else { - $errorMessage = $result['response']['StatusDescription'] ?? 'Unknown error'; + $errorMessage = $body['StatusDescription'] ?? 'Unknown error'; foreach ($message->getTo() as $to) { $response->addResult($to, $errorMessage); } diff --git a/src/Utopia/Messaging/Adapter/SMS/Mock.php b/src/Utopia/Messaging/Adapter/SMS/Mock.php index 2f9d72e5..f9dc34ec 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Mock.php +++ b/src/Utopia/Messaging/Adapter/SMS/Mock.php @@ -70,7 +70,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] === 200) { + if ($result->getStatusCode() === 200) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Msg91.php b/src/Utopia/Messaging/Adapter/SMS/Msg91.php index 8c77907c..07f203b8 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Msg91.php +++ b/src/Utopia/Messaging/Adapter/SMS/Msg91.php @@ -66,7 +66,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] === 200) { + if ($result->getStatusCode() === 200) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Plivo.php b/src/Utopia/Messaging/Adapter/SMS/Plivo.php index 67e6aff7..c5b14323 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Plivo.php +++ b/src/Utopia/Messaging/Adapter/SMS/Plivo.php @@ -57,7 +57,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Seven.php b/src/Utopia/Messaging/Adapter/SMS/Seven.php index 2f0569c9..f2de5861 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Seven.php +++ b/src/Utopia/Messaging/Adapter/SMS/Seven.php @@ -55,7 +55,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Sinch.php b/src/Utopia/Messaging/Adapter/SMS/Sinch.php index bd891459..d5f624a7 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Sinch.php +++ b/src/Utopia/Messaging/Adapter/SMS/Sinch.php @@ -59,7 +59,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/Telesign.php b/src/Utopia/Messaging/Adapter/SMS/Telesign.php index 17a2bf7a..6b38a8ec 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Telesign.php +++ b/src/Utopia/Messaging/Adapter/SMS/Telesign.php @@ -61,15 +61,16 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] === 200) { + if ($result->getStatusCode() === 200) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); } } else { + $body = \json_decode((string) $result->getBody(), true); foreach ($message->getTo() as $to) { - if (!\is_null($result['response']['errors'][0]['description'] ?? null)) { - $response->addResult($to, $result['response']['errors'][0]['description']); + if (!\is_null($body['errors'][0]['description'] ?? null)) { + $response->addResult($to, $body['errors'][0]['description']); } else { $response->addResult($to, 'Unknown error'); } diff --git a/src/Utopia/Messaging/Adapter/SMS/Telnyx.php b/src/Utopia/Messaging/Adapter/SMS/Telnyx.php index 1896ae88..1245f6db 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Telnyx.php +++ b/src/Utopia/Messaging/Adapter/SMS/Telnyx.php @@ -53,7 +53,7 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); diff --git a/src/Utopia/Messaging/Adapter/SMS/TextMagic.php b/src/Utopia/Messaging/Adapter/SMS/TextMagic.php index a713482b..00c198b1 100644 --- a/src/Utopia/Messaging/Adapter/SMS/TextMagic.php +++ b/src/Utopia/Messaging/Adapter/SMS/TextMagic.php @@ -63,15 +63,16 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { $response->addResult($to); } } else { + $body = \json_decode((string) $result->getBody(), true); foreach ($message->getTo() as $to) { - if (!\is_null($result['response']['message'] ?? null)) { - $response->addResult($to, $result['response']['message']); + if (!\is_null($body['message'] ?? null)) { + $response->addResult($to, $body['message']); } else { $response->addResult($to, 'Unknown error'); } diff --git a/src/Utopia/Messaging/Adapter/SMS/Twilio.php b/src/Utopia/Messaging/Adapter/SMS/Twilio.php index 5b5b638b..7598657c 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Twilio.php +++ b/src/Utopia/Messaging/Adapter/SMS/Twilio.php @@ -55,12 +55,13 @@ protected function process(SMSMessage $message): array ], ); - if ($result['statusCode'] >= 200 && $result['statusCode'] < 300) { + if ($result->getStatusCode() >= 200 && $result->getStatusCode() < 300) { $response->setDeliveredTo(1); $response->addResult($message->getTo()[0]); } else { - if (!\is_null($result['response']['message'] ?? null)) { - $response->addResult($message->getTo()[0], $result['response']['message']); + $body = \json_decode((string) $result->getBody(), true); + if (!\is_null($body['message'] ?? null)) { + $response->addResult($message->getTo()[0], $body['message']); } else { $response->addResult($message->getTo()[0], 'Unknown error'); } diff --git a/src/Utopia/Messaging/Adapter/SMS/Vonage.php b/src/Utopia/Messaging/Adapter/SMS/Vonage.php index d583104c..5b9a6646 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Vonage.php +++ b/src/Utopia/Messaging/Adapter/SMS/Vonage.php @@ -50,7 +50,7 @@ protected function process(SMS $message): array method: 'POST', url: 'https://rest.nexmo.com/sms/json', headers: [ - 'Content-Type' => 'application/x-www-form-urlencoded', + 'Content-Type: application/x-www-form-urlencoded', ], body: [ 'text' => $message->getContent(), @@ -61,12 +61,14 @@ protected function process(SMS $message): array ], ); - if (($result['response']['messages'][0]['status'] ?? null) === 0) { + $body = \json_decode((string) $result->getBody(), true); + + if (($body['messages'][0]['status'] ?? null) === 0) { $response->setDeliveredTo(1); - $response->addResult($result['response']['messages'][0]['to']); + $response->addResult($body['messages'][0]['to']); } else { - if (!\is_null($result['response']['messages'][0]['error-text'] ?? null)) { - $response->addResult($message->getTo()[0], $result['response']['messages'][0]['error-text']); + if (!\is_null($body['messages'][0]['error-text'] ?? null)) { + $response->addResult($message->getTo()[0], $body['messages'][0]['error-text']); } else { $response->addResult($message->getTo()[0], 'Unknown error'); } diff --git a/src/Utopia/Messaging/Helpers/JWT.php b/src/Utopia/Messaging/Helpers/JWT.php index ba4f4b83..e6020ef2 100644 --- a/src/Utopia/Messaging/Helpers/JWT.php +++ b/src/Utopia/Messaging/Helpers/JWT.php @@ -23,7 +23,7 @@ class JWT * * @throws \Exception */ - public static function encode(array $payload, string $key, string $algorithm, string $keyId = null): string + public static function encode(array $payload, string $key, string $algorithm, ?string $keyId = null): string { $header = [ 'typ' => 'JWT', diff --git a/tests/Messaging/Adapter/Chat/DiscordTest.php b/tests/Messaging/Adapter/Chat/DiscordTest.php index 3147c34a..d3aaf301 100644 --- a/tests/Messaging/Adapter/Chat/DiscordTest.php +++ b/tests/Messaging/Adapter/Chat/DiscordTest.php @@ -63,8 +63,8 @@ public function testValidURLVariations(): void foreach ($validURLs as $label => $url) { try { $discord = new Discord($url); - // If we get here, the URL was accepted - $this->assertTrue(true, "Valid URL variant '{$label}' was accepted as expected"); + // If we get here without an exception, the URL was accepted. + $this->addToAssertionCount(1); } catch (InvalidArgumentException $e) { $this->fail("Valid URL variant '{$label}' was rejected: " . $e->getMessage()); } diff --git a/tests/Messaging/Adapter/Email/ResendRoutingTest.php b/tests/Messaging/Adapter/Email/ResendRoutingTest.php index 2b68fa09..fe32f9e2 100644 --- a/tests/Messaging/Adapter/Email/ResendRoutingTest.php +++ b/tests/Messaging/Adapter/Email/ResendRoutingTest.php @@ -3,9 +3,12 @@ namespace Utopia\Tests\Adapter\Email; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Utopia\Messaging\Adapter\Email\Resend; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Psr7\Response; +use Utopia\Psr7\Stream; class ResendRoutingTest extends TestCase { @@ -135,7 +138,6 @@ class ResendStub extends Resend /** * @param array $headers * @param array|null $body - * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} */ protected function request( string $method, @@ -144,7 +146,7 @@ protected function request( ?array $body = null, int $timeout = 30, int $connectTimeout = 10 - ): array { + ): ResponseInterface { $this->capturedRequests[] = [ 'method' => $method, 'url' => $url, @@ -154,12 +156,8 @@ protected function request( $stub = \array_shift($this->stubResponses) ?? ['statusCode' => 200, 'response' => []]; - return [ - 'url' => $url, - 'statusCode' => $stub['statusCode'], - 'response' => $stub['response'], - 'headers' => [], - 'error' => null, - ]; + $payload = \is_string($stub['response']) ? $stub['response'] : (string) \json_encode($stub['response']); + + return new Response($stub['statusCode'], '', new Stream($payload)); } } diff --git a/tests/Messaging/Adapter/Email/SESRoutingTest.php b/tests/Messaging/Adapter/Email/SESRoutingTest.php index 67a24300..e8177c53 100644 --- a/tests/Messaging/Adapter/Email/SESRoutingTest.php +++ b/tests/Messaging/Adapter/Email/SESRoutingTest.php @@ -3,9 +3,12 @@ namespace Utopia\Tests\Adapter\Email; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Utopia\Messaging\Adapter\Email\SES; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Psr7\Response; +use Utopia\Psr7\Stream; /** * Network-free verification of how the SES adapter builds and routes requests: @@ -657,7 +660,6 @@ class SESStub extends SES /** * @param array $headers * @param array|null $body - * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} */ protected function request( string $method, @@ -666,7 +668,7 @@ protected function request( ?array $body = null, int $timeout = 30, int $connectTimeout = 10 - ): array { + ): ResponseInterface { $this->capturedRequests[] = [ 'method' => $method, 'url' => $url, @@ -676,12 +678,14 @@ protected function request( $stub = \array_shift($this->stubResponses) ?? ['statusCode' => 200, 'response' => []]; - return [ - 'url' => $url, - 'statusCode' => $stub['statusCode'], - 'response' => $stub['response'], - 'headers' => $stub['headers'] ?? [], - 'error' => null, - ]; + $payload = \is_string($stub['response']) ? $stub['response'] : (string) \json_encode($stub['response']); + + $response = new Response($stub['statusCode'], '', new Stream($payload)); + + foreach ($stub['headers'] ?? [] as $name => $value) { + $response = $response->withHeader($name, $value); + } + + return $response; } }