From caed3df95b8733d2f3d59d7d8a8b8eca512f0bce Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:18:09 +0530 Subject: [PATCH 1/6] Add MSG91 metadata tracking --- src/Utopia/Messaging/Adapter/SMS/Msg91.php | 32 ++++++-- .../Adapter/SMS/Msg91/MetadataParameter.php | 21 +++++ src/Utopia/Messaging/Messages/SMS.php | 20 +++++ tests/Messaging/Adapter/SMS/Msg91Test.php | 80 +++++++++++++++++++ 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/Utopia/Messaging/Adapter/SMS/Msg91/MetadataParameter.php diff --git a/src/Utopia/Messaging/Adapter/SMS/Msg91.php b/src/Utopia/Messaging/Adapter/SMS/Msg91.php index 8c77907c..a99c3f69 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Msg91.php +++ b/src/Utopia/Messaging/Adapter/SMS/Msg91.php @@ -2,6 +2,7 @@ namespace Utopia\Messaging\Adapter\SMS; +use Utopia\Messaging\Adapter\SMS\Msg91\MetadataParameter; use Utopia\Messaging\Adapter\SMS as SMSAdapter; use Utopia\Messaging\Messages\SMS as SMSMessage; use Utopia\Messaging\Response; @@ -51,6 +52,31 @@ protected function process(SMSMessage $message): array ]; } + $body = [ + 'sender' => $this->senderId, + 'template_id' => $this->templateId, + 'recipients' => $recipients, + ]; + + $metadata = $message->getMetadata() ?? []; + $metadata = \array_intersect_key($metadata, \array_flip(\array_column(MetadataParameter::cases(), 'value'))); + + foreach ([MetadataParameter::CRQID, MetadataParameter::UUID] as $parameter) { + $key = $parameter->value; + + if (!isset($metadata[$key])) { + continue; + } + + if (\strlen($metadata[$key]) > 80 || !\preg_match('/^[A-Za-z0-9_.-]+$/', $metadata[$key])) { + throw new \InvalidArgumentException("Msg91 {$key} metadata must be 80 characters or less and contain only alphanumeric characters, underscores, dots, or hyphens."); + } + } + + foreach ($metadata as $key => $value) { + $body[$key] = $value; + } + $response = new Response($this->getType()); $result = $this->request( method: 'POST', @@ -59,11 +85,7 @@ protected function process(SMSMessage $message): array 'Content-Type: application/json', 'Authkey: '. $this->authKey, ], - body: [ - 'sender' => $this->senderId, - 'template_id' => $this->templateId, - 'recipients' => $recipients, - ], + body: $body, ); if ($result['statusCode'] === 200) { diff --git a/src/Utopia/Messaging/Adapter/SMS/Msg91/MetadataParameter.php b/src/Utopia/Messaging/Adapter/SMS/Msg91/MetadataParameter.php new file mode 100644 index 00000000..7d897baf --- /dev/null +++ b/src/Utopia/Messaging/Adapter/SMS/Msg91/MetadataParameter.php @@ -0,0 +1,21 @@ + $to * @param array|null $attachments + * @param array|null $metadata */ public function __construct( private array $to, private string $content, private ?string $from = null, private ?array $attachments = null, + private ?array $metadata = null, ) { } @@ -46,6 +48,24 @@ public function getAttachments(): ?array return $this->attachments; } + /** + * @return array|null + */ + public function getMetadata(): ?array + { + return $this->metadata; + } + + /** + * @param array|null $metadata + */ + public function setMetadata(?array $metadata): self + { + $this->metadata = $metadata; + + return $this; + } + public function setOrigin(?string $origin): self { $this->origin = $origin; diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php index 854bdeab..aa56ca51 100644 --- a/tests/Messaging/Adapter/SMS/Msg91Test.php +++ b/tests/Messaging/Adapter/SMS/Msg91Test.php @@ -21,4 +21,84 @@ public function testSendSMS(): void $this->assertResponse($response); } + + public function testSendSMSWithMetadata(): void + { + $sender = new Msg91TestAdapter('sender', 'auth', 'template'); + + $message = new SMS( + to: ['+911234567890'], + content: 'Test Content', + metadata: [ + 'clientId' => 'client-123', + 'CRQID' => 'request_123', + 'UUID' => 'uuid.123', + 'ignored' => 'value', + ], + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + $this->assertEquals('client-123', $sender->body['clientId']); + $this->assertEquals('request_123', $sender->body['CRQID']); + $this->assertEquals('uuid.123', $sender->body['UUID']); + $this->assertArrayNotHasKey('ignored', $sender->body); + } + + public function testSendSMSWithInvalidMetadata(): void + { + $sender = new Msg91TestAdapter('sender', 'auth', 'template'); + + $message = new SMS( + to: ['+911234567890'], + content: 'Test Content', + metadata: [ + 'CRQID' => 'invalid value', + ], + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Msg91 CRQID metadata must be 80 characters or less'); + + $sender->send($message); + } +} + +class Msg91TestAdapter extends Msg91 +{ + /** + * @var array + */ + public array $body = []; + + /** + * @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, + string $url, + array $headers = [], + ?array $body = null, + int $timeout = 30, + int $connectTimeout = 10 + ): array { + $this->body = $body ?? []; + + return [ + 'url' => $url, + 'statusCode' => 200, + 'response' => [], + 'headers' => [], + 'error' => null, + ]; + } } From cac2597d43714a517cdbae5f6975d1815725b163 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:27:58 +0530 Subject: [PATCH 2/6] Address MSG91 metadata review feedback --- src/Utopia/Messaging/Adapter/SMS/GEOSMS.php | 3 +- src/Utopia/Messaging/Adapter/SMS/Msg91.php | 43 +++++++++++++-------- tests/Messaging/Adapter/SMS/GEOSMSTest.php | 32 +++++++++++++++ tests/Messaging/Adapter/SMS/Msg91Test.php | 33 +++++++++++++++- 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php index b3ec5c19..d13302b3 100644 --- a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php +++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php @@ -92,7 +92,8 @@ protected function process(SMS $message): array to: $nextRecipients, content: $message->getContent(), from: $message->getFrom(), - attachments: $message->getAttachments() + attachments: $message->getAttachments(), + metadata: $message->getMetadata() ))->setOrigin($message->getOrigin()) ); } catch (\Exception $e) { diff --git a/src/Utopia/Messaging/Adapter/SMS/Msg91.php b/src/Utopia/Messaging/Adapter/SMS/Msg91.php index a99c3f69..969d8172 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Msg91.php +++ b/src/Utopia/Messaging/Adapter/SMS/Msg91.php @@ -43,28 +43,19 @@ public function getMaxMessagesPerRequest(): int */ protected function process(SMSMessage $message): array { - $recipients = []; - foreach ($message->getTo() as $recipient) { - $recipients[] = [ - 'mobiles' => \ltrim($recipient, '+'), - 'content' => $message->getContent(), - 'otp' => $message->getContent(), - ]; - } - - $body = [ - 'sender' => $this->senderId, - 'template_id' => $this->templateId, - 'recipients' => $recipients, - ]; - $metadata = $message->getMetadata() ?? []; $metadata = \array_intersect_key($metadata, \array_flip(\array_column(MetadataParameter::cases(), 'value'))); + foreach ($metadata as $key => $value) { + if (!\is_string($value)) { + throw new \InvalidArgumentException("Msg91 {$key} metadata must be a string."); + } + } + foreach ([MetadataParameter::CRQID, MetadataParameter::UUID] as $parameter) { $key = $parameter->value; - if (!isset($metadata[$key])) { + if (!\array_key_exists($key, $metadata)) { continue; } @@ -73,6 +64,26 @@ protected function process(SMSMessage $message): array } } + $recipientMetadata = \array_intersect_key($metadata, \array_flip([ + MetadataParameter::CRQID->value, + MetadataParameter::UUID->value, + ])); + + $recipients = []; + foreach ($message->getTo() as $recipient) { + $recipients[] = [ + 'mobiles' => \ltrim($recipient, '+'), + 'content' => $message->getContent(), + 'otp' => $message->getContent(), + ] + $recipientMetadata; + } + + $body = [ + 'sender' => $this->senderId, + 'template_id' => $this->templateId, + 'recipients' => $recipients, + ]; + foreach ($metadata as $key => $value) { $body[$key] = $value; } diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php index 82633336..a1d96beb 100644 --- a/tests/Messaging/Adapter/SMS/GEOSMSTest.php +++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php @@ -111,4 +111,36 @@ public function testSendSMSUsingGroupedLocalAdapter(): void $this->assertEquals(1, count($result)); $this->assertEquals('success', $result['local']['results'][0]['status']); } + + public function testSendSMSForwardsMetadata(): void + { + $metadata = [ + 'clientId' => 'client-123', + 'CRQID' => 'request_123', + 'UUID' => 'uuid.123', + ]; + + $defaultAdapterMock = $this->createMock(SMSAdapter::class); + $defaultAdapterMock->method('getName')->willReturn('default'); + $defaultAdapterMock + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (SMS $message) use ($metadata): bool { + return $message->getMetadata() === $metadata; + })) + ->willReturn(['results' => [['status' => 'success']]]); + + $adapter = new GEOSMS($defaultAdapterMock); + + $message = new SMS( + to: ['+11234567890'], + content: 'Test Content', + metadata: $metadata + ); + + $result = $adapter->send($message); + + $this->assertEquals(1, count($result)); + $this->assertEquals('success', $result['default']['results'][0]['status']); + } } diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php index aa56ca51..91c8e95a 100644 --- a/tests/Messaging/Adapter/SMS/Msg91Test.php +++ b/tests/Messaging/Adapter/SMS/Msg91Test.php @@ -27,7 +27,7 @@ public function testSendSMSWithMetadata(): void $sender = new Msg91TestAdapter('sender', 'auth', 'template'); $message = new SMS( - to: ['+911234567890'], + to: ['+911234567890', '+911234567891'], content: 'Test Content', metadata: [ 'clientId' => 'client-123', @@ -39,10 +39,18 @@ public function testSendSMSWithMetadata(): void $response = $sender->send($message); - $this->assertResponse($response); + $this->assertEquals(2, $response['deliveredTo'], \var_export($response, true)); + $this->assertEquals('', $response['results'][0]['error'], \var_export($response, true)); + $this->assertEquals('success', $response['results'][0]['status'], \var_export($response, true)); + $this->assertEquals('', $response['results'][1]['error'], \var_export($response, true)); + $this->assertEquals('success', $response['results'][1]['status'], \var_export($response, true)); $this->assertEquals('client-123', $sender->body['clientId']); $this->assertEquals('request_123', $sender->body['CRQID']); $this->assertEquals('uuid.123', $sender->body['UUID']); + $this->assertEquals('request_123', $sender->body['recipients'][0]['CRQID']); + $this->assertEquals('uuid.123', $sender->body['recipients'][0]['UUID']); + $this->assertEquals('request_123', $sender->body['recipients'][1]['CRQID']); + $this->assertEquals('uuid.123', $sender->body['recipients'][1]['UUID']); $this->assertArrayNotHasKey('ignored', $sender->body); } @@ -63,6 +71,27 @@ public function testSendSMSWithInvalidMetadata(): void $sender->send($message); } + + public function testSendSMSWithNullMetadata(): void + { + $sender = new Msg91TestAdapter('sender', 'auth', 'template'); + + /** @var array $metadata */ + $metadata = [ + 'CRQID' => null, + ]; + + $message = new SMS( + to: ['+911234567890'], + content: 'Test Content', + metadata: $metadata, + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Msg91 CRQID metadata must be a string'); + + $sender->send($message); + } } class Msg91TestAdapter extends Msg91 From 08728c06c00f74bbf57217610c78053268ce56f4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:35:09 +0530 Subject: [PATCH 3/6] Avoid duplicate GEOSMS tracking metadata --- src/Utopia/Messaging/Adapter/SMS/GEOSMS.php | 26 ++++++++--- tests/Messaging/Adapter/SMS/GEOSMSTest.php | 48 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php index d13302b3..3e03232c 100644 --- a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php +++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php @@ -82,29 +82,41 @@ protected function process(SMS $message): array { $results = []; $recipients = $message->getTo(); + $batches = []; do { [$nextRecipients, $nextAdapter] = $this->getNextRecipientsAndAdapter($recipients); + $batches[] = [ + 'recipients' => $nextRecipients, + 'adapter' => $nextAdapter, + ]; + $recipients = \array_diff($recipients, $nextRecipients); + } while (count($recipients) > 0); + + $metadata = $message->getMetadata(); + if (\count($batches) > 1 && $metadata !== null) { + unset($metadata['CRQID'], $metadata['UUID']); + } + + foreach ($batches as $batch) { try { - $results[$nextAdapter->getName()] = $nextAdapter->send( + $results[$batch['adapter']->getName()] = $batch['adapter']->send( (new SMS( - to: $nextRecipients, + to: $batch['recipients'], content: $message->getContent(), from: $message->getFrom(), attachments: $message->getAttachments(), - metadata: $message->getMetadata() + metadata: $metadata ))->setOrigin($message->getOrigin()) ); } catch (\Exception $e) { - $results[$nextAdapter->getName()] = [ + $results[$batch['adapter']->getName()] = [ 'type' => 'error', 'message' => $e->getMessage(), ]; } - - $recipients = \array_diff($recipients, $nextRecipients); - } while (count($recipients) > 0); + } return $results; } diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php index a1d96beb..63774164 100644 --- a/tests/Messaging/Adapter/SMS/GEOSMSTest.php +++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php @@ -143,4 +143,52 @@ public function testSendSMSForwardsMetadata(): void $this->assertEquals(1, count($result)); $this->assertEquals('success', $result['default']['results'][0]['status']); } + + public function testSendSMSRemovesRequestTrackingMetadataWhenSplit(): void + { + $metadata = [ + 'clientId' => 'client-123', + 'CRQID' => 'request_123', + 'UUID' => 'uuid.123', + ]; + + $expectedMetadata = [ + 'clientId' => 'client-123', + ]; + + $defaultAdapterMock = $this->createMock(SMSAdapter::class); + $defaultAdapterMock->method('getName')->willReturn('default'); + $defaultAdapterMock + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (SMS $message) use ($expectedMetadata): bool { + return $message->getMetadata() === $expectedMetadata; + })) + ->willReturn(['results' => [['status' => 'success']]]); + + $localAdapterMock = $this->createMock(SMSAdapter::class); + $localAdapterMock->method('getName')->willReturn('local'); + $localAdapterMock + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (SMS $message) use ($expectedMetadata): bool { + return $message->getMetadata() === $expectedMetadata; + })) + ->willReturn(['results' => [['status' => 'success']]]); + + $adapter = new GEOSMS($defaultAdapterMock); + $adapter->setLocal(CallingCode::INDIA, $localAdapterMock); + + $message = new SMS( + to: ['+911234567890', '+11234567890'], + content: 'Test Content', + metadata: $metadata + ); + + $result = $adapter->send($message); + + $this->assertEquals(2, count($result)); + $this->assertEquals('success', $result['local']['results'][0]['status']); + $this->assertEquals('success', $result['default']['results'][0]['status']); + } } From 53497b0bec656c54eab8960c393d5d0964cebf00 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:36:44 +0530 Subject: [PATCH 4/6] Compact metadata tests --- tests/Messaging/Adapter/SMS/GEOSMSTest.php | 11 +------- tests/Messaging/Adapter/SMS/Msg91Test.php | 30 ++++++++++------------ 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php index 63774164..0f4f4296 100644 --- a/tests/Messaging/Adapter/SMS/GEOSMSTest.php +++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php @@ -112,7 +112,7 @@ public function testSendSMSUsingGroupedLocalAdapter(): void $this->assertEquals('success', $result['local']['results'][0]['status']); } - public function testSendSMSForwardsMetadata(): void + public function testSendSMSHandlesMetadata(): void { $metadata = [ 'clientId' => 'client-123', @@ -142,15 +142,6 @@ public function testSendSMSForwardsMetadata(): void $this->assertEquals(1, count($result)); $this->assertEquals('success', $result['default']['results'][0]['status']); - } - - public function testSendSMSRemovesRequestTrackingMetadataWhenSplit(): void - { - $metadata = [ - 'clientId' => 'client-123', - 'CRQID' => 'request_123', - 'UUID' => 'uuid.123', - ]; $expectedMetadata = [ 'clientId' => 'client-123', diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php index 91c8e95a..0c6a7049 100644 --- a/tests/Messaging/Adapter/SMS/Msg91Test.php +++ b/tests/Messaging/Adapter/SMS/Msg91Test.php @@ -22,7 +22,7 @@ public function testSendSMS(): void $this->assertResponse($response); } - public function testSendSMSWithMetadata(): void + public function testSendSMSHandlesMetadata(): void { $sender = new Msg91TestAdapter('sender', 'auth', 'template'); @@ -52,10 +52,7 @@ public function testSendSMSWithMetadata(): void $this->assertEquals('request_123', $sender->body['recipients'][1]['CRQID']); $this->assertEquals('uuid.123', $sender->body['recipients'][1]['UUID']); $this->assertArrayNotHasKey('ignored', $sender->body); - } - public function testSendSMSWithInvalidMetadata(): void - { $sender = new Msg91TestAdapter('sender', 'auth', 'template'); $message = new SMS( @@ -66,15 +63,12 @@ public function testSendSMSWithInvalidMetadata(): void ], ); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Msg91 CRQID metadata must be 80 characters or less'); - - $sender->send($message); - } - - public function testSendSMSWithNullMetadata(): void - { - $sender = new Msg91TestAdapter('sender', 'auth', 'template'); + try { + $sender->send($message); + $this->fail('Expected invalid MSG91 metadata to throw.'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('Msg91 CRQID metadata must be 80 characters or less', $e->getMessage()); + } /** @var array $metadata */ $metadata = [ @@ -87,10 +81,12 @@ public function testSendSMSWithNullMetadata(): void metadata: $metadata, ); - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Msg91 CRQID metadata must be a string'); - - $sender->send($message); + try { + $sender->send($message); + $this->fail('Expected null MSG91 metadata to throw.'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('Msg91 CRQID metadata must be a string', $e->getMessage()); + } } } From 5fcff37ad657db309e6cba2c4220bc91e01cffa7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:44:25 +0530 Subject: [PATCH 5/6] Refine MSG91 tracking metadata handling --- src/Utopia/Messaging/Adapter/SMS/GEOSMS.php | 17 ++++++++++++----- src/Utopia/Messaging/Adapter/SMS/Msg91.php | 7 +------ tests/Messaging/Adapter/SMS/GEOSMSTest.php | 18 +++++++++++++----- tests/Messaging/Adapter/SMS/Msg91Test.php | 8 ++++---- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php index 3e03232c..bf7f7828 100644 --- a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php +++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php @@ -94,12 +94,19 @@ protected function process(SMS $message): array $recipients = \array_diff($recipients, $nextRecipients); } while (count($recipients) > 0); - $metadata = $message->getMetadata(); - if (\count($batches) > 1 && $metadata !== null) { - unset($metadata['CRQID'], $metadata['UUID']); - } + foreach ($batches as $index => $batch) { + $metadata = $message->getMetadata(); + if (\count($batches) > 1 && $metadata !== null) { + foreach (['CRQID', 'UUID'] as $key) { + if (!isset($metadata[$key])) { + continue; + } + + $suffix = '-'.($index + 1); + $metadata[$key] = \substr($metadata[$key], 0, 80 - \strlen($suffix)).$suffix; + } + } - foreach ($batches as $batch) { try { $results[$batch['adapter']->getName()] = $batch['adapter']->send( (new SMS( diff --git a/src/Utopia/Messaging/Adapter/SMS/Msg91.php b/src/Utopia/Messaging/Adapter/SMS/Msg91.php index 969d8172..660ccfe4 100644 --- a/src/Utopia/Messaging/Adapter/SMS/Msg91.php +++ b/src/Utopia/Messaging/Adapter/SMS/Msg91.php @@ -64,18 +64,13 @@ protected function process(SMSMessage $message): array } } - $recipientMetadata = \array_intersect_key($metadata, \array_flip([ - MetadataParameter::CRQID->value, - MetadataParameter::UUID->value, - ])); - $recipients = []; foreach ($message->getTo() as $recipient) { $recipients[] = [ 'mobiles' => \ltrim($recipient, '+'), 'content' => $message->getContent(), 'otp' => $message->getContent(), - ] + $recipientMetadata; + ]; } $body = [ diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php index 0f4f4296..d2f3c8c2 100644 --- a/tests/Messaging/Adapter/SMS/GEOSMSTest.php +++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php @@ -143,8 +143,16 @@ public function testSendSMSHandlesMetadata(): void $this->assertEquals(1, count($result)); $this->assertEquals('success', $result['default']['results'][0]['status']); - $expectedMetadata = [ + $defaultMetadata = [ 'clientId' => 'client-123', + 'CRQID' => 'request_123-2', + 'UUID' => 'uuid.123-2', + ]; + + $localMetadata = [ + 'clientId' => 'client-123', + 'CRQID' => 'request_123-1', + 'UUID' => 'uuid.123-1', ]; $defaultAdapterMock = $this->createMock(SMSAdapter::class); @@ -152,8 +160,8 @@ public function testSendSMSHandlesMetadata(): void $defaultAdapterMock ->expects($this->once()) ->method('send') - ->with($this->callback(function (SMS $message) use ($expectedMetadata): bool { - return $message->getMetadata() === $expectedMetadata; + ->with($this->callback(function (SMS $message) use ($defaultMetadata): bool { + return $message->getMetadata() === $defaultMetadata; })) ->willReturn(['results' => [['status' => 'success']]]); @@ -162,8 +170,8 @@ public function testSendSMSHandlesMetadata(): void $localAdapterMock ->expects($this->once()) ->method('send') - ->with($this->callback(function (SMS $message) use ($expectedMetadata): bool { - return $message->getMetadata() === $expectedMetadata; + ->with($this->callback(function (SMS $message) use ($localMetadata): bool { + return $message->getMetadata() === $localMetadata; })) ->willReturn(['results' => [['status' => 'success']]]); diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php index 0c6a7049..3dff601f 100644 --- a/tests/Messaging/Adapter/SMS/Msg91Test.php +++ b/tests/Messaging/Adapter/SMS/Msg91Test.php @@ -47,10 +47,10 @@ public function testSendSMSHandlesMetadata(): void $this->assertEquals('client-123', $sender->body['clientId']); $this->assertEquals('request_123', $sender->body['CRQID']); $this->assertEquals('uuid.123', $sender->body['UUID']); - $this->assertEquals('request_123', $sender->body['recipients'][0]['CRQID']); - $this->assertEquals('uuid.123', $sender->body['recipients'][0]['UUID']); - $this->assertEquals('request_123', $sender->body['recipients'][1]['CRQID']); - $this->assertEquals('uuid.123', $sender->body['recipients'][1]['UUID']); + $this->assertArrayNotHasKey('CRQID', $sender->body['recipients'][0]); + $this->assertArrayNotHasKey('UUID', $sender->body['recipients'][0]); + $this->assertArrayNotHasKey('CRQID', $sender->body['recipients'][1]); + $this->assertArrayNotHasKey('UUID', $sender->body['recipients'][1]); $this->assertArrayNotHasKey('ignored', $sender->body); $sender = new Msg91TestAdapter('sender', 'auth', 'template'); From 740b7956462ac4fb10d64e7fac114207de5e9924 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 23 Jun 2026 09:52:30 +0530 Subject: [PATCH 6/6] Validate GEOSMS tracking metadata before suffixing --- src/Utopia/Messaging/Adapter/SMS/GEOSMS.php | 16 ++++++++-- tests/Messaging/Adapter/SMS/GEOSMSTest.php | 33 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php index bf7f7828..e88d8a57 100644 --- a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php +++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php @@ -4,6 +4,7 @@ use Utopia\Messaging\Adapter\SMS as SMSAdapter; use Utopia\Messaging\Adapter\SMS\GEOSMS\CallingCode; +use Utopia\Messaging\Adapter\SMS\Msg91\MetadataParameter; use Utopia\Messaging\Messages\SMS; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; @@ -95,13 +96,24 @@ protected function process(SMS $message): array } while (count($recipients) > 0); foreach ($batches as $index => $batch) { + /** @var array|null $metadata */ $metadata = $message->getMetadata(); if (\count($batches) > 1 && $metadata !== null) { - foreach (['CRQID', 'UUID'] as $key) { - if (!isset($metadata[$key])) { + foreach ([MetadataParameter::CRQID, MetadataParameter::UUID] as $parameter) { + $key = $parameter->value; + + if (!\array_key_exists($key, $metadata)) { continue; } + if (!\is_string($metadata[$key])) { + throw new \InvalidArgumentException("Msg91 {$key} metadata must be a string."); + } + + if (\strlen($metadata[$key]) > 80 || !\preg_match('/^[A-Za-z0-9_.-]+$/', $metadata[$key])) { + throw new \InvalidArgumentException("Msg91 {$key} metadata must be 80 characters or less and contain only alphanumeric characters, underscores, dots, or hyphens."); + } + $suffix = '-'.($index + 1); $metadata[$key] = \substr($metadata[$key], 0, 80 - \strlen($suffix)).$suffix; } diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php index d2f3c8c2..893d87d6 100644 --- a/tests/Messaging/Adapter/SMS/GEOSMSTest.php +++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php @@ -189,5 +189,38 @@ public function testSendSMSHandlesMetadata(): void $this->assertEquals(2, count($result)); $this->assertEquals('success', $result['local']['results'][0]['status']); $this->assertEquals('success', $result['default']['results'][0]['status']); + + /** @var array $invalidMetadata */ + $invalidMetadata = [ + 'CRQID' => [], + ]; + + $message = new SMS( + to: ['+911234567890', '+11234567890'], + content: 'Test Content', + metadata: $invalidMetadata + ); + + try { + $adapter->send($message); + $this->fail('Expected invalid GEOSMS metadata to throw.'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('Msg91 CRQID metadata must be a string', $e->getMessage()); + } + + $message = new SMS( + to: ['+911234567890', '+11234567890'], + content: 'Test Content', + metadata: [ + 'CRQID' => '', + ] + ); + + try { + $adapter->send($message); + $this->fail('Expected empty GEOSMS metadata to throw.'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('Msg91 CRQID metadata must be 80 characters or less', $e->getMessage()); + } } }