From 2af52bcd7daa0ac0c2dd0ebf10527177b2665e06 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:03:44 +0530 Subject: [PATCH 1/5] feat: add Vonage Messages API adapter --- .../Messaging/Adapter/SMS/VonageMessages.php | 88 +++++++++++++++++++ .../Messaging/Adapter/VonageMessagesBase.php | 45 ++++++++++ .../Adapter/SMS/VonageMessagesTest.php | 39 ++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/Utopia/Messaging/Adapter/SMS/VonageMessages.php create mode 100644 src/Utopia/Messaging/Adapter/VonageMessagesBase.php create mode 100644 tests/Messaging/Adapter/SMS/VonageMessagesTest.php diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php new file mode 100644 index 00000000..eed43878 --- /dev/null +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -0,0 +1,88 @@ +getTo()[0], '+'); + $from = $this->from ?? $message->getFrom(); + + $response = new Response($this->getType()); + + $result = $this->request( + method: 'POST', + url: $this->getApiEndpoint(), + headers: $this->getRequestHeaders(), + body: [ + 'message_type' => 'text', + 'to' => $to, + 'from' => $from, + 'text' => $message->getContent(), + 'channel' => 'sms', + ], + ); + + if ($result['statusCode'] === 202) { + $response->setDeliveredTo(1); + $response->addResult($message->getTo()[0]); + } else { + $errorMessage = 'Unknown error'; + if (isset($result['response']['detail'])) { + $errorMessage = $result['response']['detail']; + } elseif (isset($result['response']['title'])) { + $errorMessage = $result['response']['title']; + } elseif (!empty($result['error'])) { + $errorMessage = $result['error']; + } + + $response->addResult($message->getTo()[0], $errorMessage); + } + + return $response->toArray(); + } +} diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php new file mode 100644 index 00000000..577a2a44 --- /dev/null +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -0,0 +1,45 @@ +apiKey}:{$this->apiSecret}"); + } + + /** + * Build the request headers for the Messages API. + * + * @return array + */ + protected function getRequestHeaders(): array + { + return [ + "Authorization: {$this->getAuthorizationHeader()}", + 'Content-Type: application/json', + 'Accept: application/json', + ]; + } +} diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php new file mode 100644 index 00000000..6eeaca94 --- /dev/null +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Vonage Messages credentials are not available.'); + } + + $sender = new VonageMessages( + apiKey: $apiKey, + apiSecret: $apiSecret, + from: \getenv('VONAGE_FROM') ?: 'Vonage', + ); + + $message = new SMS( + to: [\getenv('VONAGE_TO')], + content: 'Test Content', + from: \getenv('VONAGE_FROM') + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + } +} From 6710210c76682474d5a4f680139e40c39877adcf Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:55:24 +0530 Subject: [PATCH 2/5] fix: address code review feedback (Vonage domain, from formatting, test coverage) --- .../Messaging/Adapter/SMS/VonageMessages.php | 9 ++++- .../Messaging/Adapter/VonageMessagesBase.php | 5 ++- .../Adapter/SMS/VonageMessagesTest.php | 37 +++++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index eed43878..286a9f1d 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -26,9 +26,7 @@ class VonageMessages extends SMSAdapter * @param string $apiSecret Vonage API Secret */ public function __construct( - /** @phpstan-ignore property.onlyWritten */ private string $apiKey, - /** @phpstan-ignore property.onlyWritten */ private string $apiSecret, private ?string $from = null ) { @@ -54,6 +52,13 @@ protected function process(SMSMessage $message): array $response = new Response($this->getType()); + if (empty($from)) { + $response->addResult($message->getTo()[0], 'The "from" field is required for the Vonage Messages API.'); + return $response->toArray(); + } + + $from = \ltrim($from, '+'); + $result = $this->request( method: 'POST', url: $this->getApiEndpoint(), diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php index 577a2a44..09ce5e2f 100644 --- a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -21,9 +21,12 @@ trait VonageMessagesBase */ protected function getApiEndpoint(): string { - return 'https://api.nexmo.com/v1/messages'; + return 'https://api.vonage.com/v1/messages'; } + /** + * @todo Implement JWT authentication for non-SMS channels + */ protected function getAuthorizationHeader(): string { return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}"); diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php index 6eeaca94..25890d4b 100644 --- a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -15,9 +15,10 @@ public function testSendSMS(): void { $apiKey = \getenv('VONAGE_API_KEY'); $apiSecret = \getenv('VONAGE_API_SECRET'); + $to = \getenv('VONAGE_TO'); - if (!$apiKey || !$apiSecret) { - $this->markTestSkipped('Vonage Messages credentials are not available.'); + if (!$apiKey || !$apiSecret || !$to) { + $this->markTestSkipped('Vonage Messages credentials or recipient are not available.'); } $sender = new VonageMessages( @@ -27,7 +28,7 @@ public function testSendSMS(): void ); $message = new SMS( - to: [\getenv('VONAGE_TO')], + to: [$to], content: 'Test Content', from: \getenv('VONAGE_FROM') ); @@ -36,4 +37,34 @@ public function testSendSMS(): void $this->assertResponse($response); } + + /** + * @throws \Exception + */ + public function testSendSMSWithFallbackFrom(): void + { + $apiKey = \getenv('VONAGE_API_KEY'); + $apiSecret = \getenv('VONAGE_API_SECRET'); + $to = \getenv('VONAGE_TO'); + $from = \getenv('VONAGE_FROM'); + + if (!$apiKey || !$apiSecret || !$to || !$from) { + $this->markTestSkipped('Vonage Messages credentials or sender/recipient are not available.'); + } + + $sender = new VonageMessages( + apiKey: $apiKey, + apiSecret: $apiSecret, + ); + + $message = new SMS( + to: [$to], + content: 'Test Content', + from: $from + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + } } From 81c75ee22af06a4621bab2d84413b459459a1e0c Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:07:17 +0530 Subject: [PATCH 3/5] test: refine type safety and isolate fallback logic in Vonage tests --- tests/Messaging/Adapter/SMS/VonageMessagesTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php index 25890d4b..b26a758a 100644 --- a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -30,7 +30,6 @@ public function testSendSMS(): void $message = new SMS( to: [$to], content: 'Test Content', - from: \getenv('VONAGE_FROM') ); $response = $sender->send($message); @@ -46,7 +45,7 @@ public function testSendSMSWithFallbackFrom(): void $apiKey = \getenv('VONAGE_API_KEY'); $apiSecret = \getenv('VONAGE_API_SECRET'); $to = \getenv('VONAGE_TO'); - $from = \getenv('VONAGE_FROM'); + $from = \getenv('VONAGE_FROM') ?: null; if (!$apiKey || !$apiSecret || !$to || !$from) { $this->markTestSkipped('Vonage Messages credentials or sender/recipient are not available.'); @@ -60,7 +59,7 @@ public function testSendSMSWithFallbackFrom(): void $message = new SMS( to: [$to], content: 'Test Content', - from: $from + from: $from, ); $response = $sender->send($message); From b0d9aecf4e4dca9596541ae785203fd24b195b32 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:12:52 +0530 Subject: [PATCH 4/5] docs: improve docstring coverage for Vonage methods to meet CI threshold --- src/Utopia/Messaging/Adapter/SMS/VonageMessages.php | 6 ++++++ src/Utopia/Messaging/Adapter/VonageMessagesBase.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index 286a9f1d..a316513e 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -32,11 +32,17 @@ public function __construct( ) { } + /** + * Get adapter name. + */ public function getName(): string { return static::NAME; } + /** + * Get max messages per request. + */ public function getMaxMessagesPerRequest(): int { return 1; diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php index 09ce5e2f..22659fa0 100644 --- a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -25,6 +25,8 @@ protected function getApiEndpoint(): string } /** + * Get the authorization header value for the API request. + * * @todo Implement JWT authentication for non-SMS channels */ protected function getAuthorizationHeader(): string From 62b741ec99fdb0e3dd734666f29fd6ab8c656244 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:37:22 +0530 Subject: [PATCH 5/5] fix: move from field trimming before empty guard --- src/Utopia/Messaging/Adapter/SMS/VonageMessages.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index a316513e..396180ef 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -55,6 +55,7 @@ protected function process(SMSMessage $message): array { $to = \ltrim($message->getTo()[0], '+'); $from = $this->from ?? $message->getFrom(); + $from = $from !== null ? \ltrim($from, '+') : null; $response = new Response($this->getType()); @@ -63,8 +64,6 @@ protected function process(SMSMessage $message): array return $response->toArray(); } - $from = \ltrim($from, '+'); - $result = $this->request( method: 'POST', url: $this->getApiEndpoint(),