diff --git a/README.md b/README.md index 1c3a520..221e694 100644 --- a/README.md +++ b/README.md @@ -195,23 +195,23 @@ $clientJWT = new Client( ### Core Methods -| Category | Method | Description | -| ------------------ | ---------------------------------------------------- | --------------------------------- | -| **Messages** | `SendMessage(Message $message)` | Send SMS message | -| | `GetMessageState(string $id)` | Get message status by ID | -| | `RequestInboxExport(MessagesExportRequest $request)` | Request inbox export via webhooks | -| **Devices** | `ListDevices()` | List registered devices | -| | `RemoveDevice(string $id)` | Remove device by ID | -| **System** | `HealthCheck()` | Check API health status | -| | `GetLogs(?string $from, ?string $to)` | Retrieve system logs | -| **Settings** | `GetSettings()` | Get account settings | -| | `PatchSettings(Settings $settings)` | Partially update account settings | -| | `ReplaceSettings(Settings $settings)` | Replace account settings | -| **Webhooks** | `ListWebhooks()` | List registered webhooks | -| | `RegisterWebhook(Webhook $webhook)` | Register new webhook | -| | `DeleteWebhook(string $id)` | Delete webhook by ID | -| **Authentication** | `GenerateToken(TokenRequest $request)` | Generate a new JWT token | -| | `RevokeToken(string $jti)` | Revoke a JWT token by ID | +| Category | Method | Description | +| ------------------ | -------------------------------------------- | --------------------------------- | +| **Messages** | `SendMessage(Message $message)` | Send SMS message | +| | `GetMessageState(string $id)` | Get message status by ID | +| | `RefreshInbox(InboxRefreshRequest $request)` | Refresh inbox, deliver webhooks | +| **Devices** | `ListDevices()` | List registered devices | +| | `RemoveDevice(string $id)` | Remove device by ID | +| **System** | `HealthCheck()` | Check API health status | +| | `GetLogs(?string $from, ?string $to)` | Retrieve system logs | +| **Settings** | `GetSettings()` | Get account settings | +| | `PatchSettings(Settings $settings)` | Partially update account settings | +| | `ReplaceSettings(Settings $settings)` | Replace account settings | +| **Webhooks** | `ListWebhooks()` | List registered webhooks | +| | `RegisterWebhook(Webhook $webhook)` | Register new webhook | +| | `DeleteWebhook(string $id)` | Delete webhook by ID | +| **Authentication** | `GenerateToken(TokenRequest $request)` | Generate a new JWT token | +| | `RevokeToken(string $jti)` | Revoke a JWT token by ID | ### Builder Methods ```php diff --git a/src/Client.php b/src/Client.php index c15791d..749c435 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,6 +2,7 @@ namespace AndroidSmsGateway; +use AndroidSmsGateway\Domain\InboxRefreshRequest; use AndroidSmsGateway\Domain\Message; use AndroidSmsGateway\Domain\MessageState; use AndroidSmsGateway\Domain\Device; @@ -290,6 +291,7 @@ public function HealthCheck(): object { /** * Request inbox messages export * + * @deprecated Use RefreshInbox instead * @param MessagesExportRequest $request * @return object */ @@ -308,6 +310,22 @@ public function RequestInboxExport(MessagesExportRequest $request): object { return $response; } + /** + * Request inbox messages refresh + * + * @param InboxRefreshRequest $request + * @return void + */ + public function RefreshInbox(InboxRefreshRequest $request): void { + $path = '/inbox/refresh'; + + $this->sendRequest( + 'POST', + $path, + $request + ); + } + /** * Get logs within a specified time range * diff --git a/src/Domain/InboxRefreshRequest.php b/src/Domain/InboxRefreshRequest.php new file mode 100644 index 0000000..18fb66b --- /dev/null +++ b/src/Domain/InboxRefreshRequest.php @@ -0,0 +1,88 @@ + + */ + private array $messageTypes; + + /** + * Delivery mode for webhooks + */ + private ?WebhookDelivery $webhookDelivery; + + + /** + * @param string|null $deviceId + * @param string $since + * @param string $until + * @param array $messageTypes + * @param WebhookDelivery|null $webhookDelivery + */ + public function __construct( + ?string $deviceId, + string $since, + string $until, + array $messageTypes, + ?WebhookDelivery $webhookDelivery = null + ) { + $this->deviceId = $deviceId; + $this->since = $since; + $this->until = $until; + $this->messageTypes = $messageTypes; + $this->webhookDelivery = $webhookDelivery; + } + + /** + * @return object + */ + public function ToObject(): object { + $obj = [ + 'since' => $this->since, + 'until' => $this->until, + ]; + + if ($this->deviceId !== null) { + $obj['deviceId'] = $this->deviceId; + } + + if (!empty($this->messageTypes)) { + $obj['messageTypes'] = array_values(array_map( + static fn(IncomingMessageType $type): string => $type->Value(), + $this->messageTypes + )); + } + + if ($this->webhookDelivery !== null) { + $obj['webhookDelivery'] = $this->webhookDelivery->Value(); + } + + return (object) $obj; + } +} diff --git a/src/Domain/MmsBatchDownloadedPayload.php b/src/Domain/MmsBatchDownloadedPayload.php new file mode 100644 index 0000000..05634e9 --- /dev/null +++ b/src/Domain/MmsBatchDownloadedPayload.php @@ -0,0 +1,41 @@ +messages = $messages; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + $messages = []; + if (isset($obj->messages) && is_array($obj->messages)) { + $messages = array_map( + static fn($m) => MmsDownloadedPayload::FromObject($m), + $obj->messages + ); + } + + return new self($messages); + } + + /** + * @return MmsDownloadedPayload[] + */ + public function Messages(): array { + return $this->messages; + } +} diff --git a/src/Domain/MmsBatchReceivedPayload.php b/src/Domain/MmsBatchReceivedPayload.php new file mode 100644 index 0000000..8a34298 --- /dev/null +++ b/src/Domain/MmsBatchReceivedPayload.php @@ -0,0 +1,41 @@ +messages = $messages; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + $messages = []; + if (isset($obj->messages) && is_array($obj->messages)) { + $messages = array_map( + static fn($m) => MmsReceivedPayload::FromObject($m), + $obj->messages + ); + } + + return new self($messages); + } + + /** + * @return MmsReceivedPayload[] + */ + public function Messages(): array { + return $this->messages; + } +} diff --git a/src/Domain/SmsBatchDataReceivedPayload.php b/src/Domain/SmsBatchDataReceivedPayload.php new file mode 100644 index 0000000..da1f300 --- /dev/null +++ b/src/Domain/SmsBatchDataReceivedPayload.php @@ -0,0 +1,41 @@ +messages = $messages; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + $messages = []; + if (isset($obj->messages) && is_array($obj->messages)) { + $messages = array_map( + static fn($m) => SmsDataReceivedPayload::FromObject($m), + $obj->messages + ); + } + + return new self($messages); + } + + /** + * @return SmsDataReceivedPayload[] + */ + public function Messages(): array { + return $this->messages; + } +} diff --git a/src/Domain/SmsBatchReceivedPayload.php b/src/Domain/SmsBatchReceivedPayload.php new file mode 100644 index 0000000..5ae0ca2 --- /dev/null +++ b/src/Domain/SmsBatchReceivedPayload.php @@ -0,0 +1,41 @@ +messages = $messages; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + $messages = []; + if (isset($obj->messages) && is_array($obj->messages)) { + $messages = array_map( + static fn($m) => SmsReceivedPayload::FromObject($m), + $obj->messages + ); + } + + return new self($messages); + } + + /** + * @return SmsReceivedPayload[] + */ + public function Messages(): array { + return $this->messages; + } +} diff --git a/src/Domain/SmsDataReceivedPayload.php b/src/Domain/SmsDataReceivedPayload.php new file mode 100644 index 0000000..305ed93 --- /dev/null +++ b/src/Domain/SmsDataReceivedPayload.php @@ -0,0 +1,78 @@ +messageId = $messageId; + $this->phoneNumber = $phoneNumber; + $this->sender = $sender; + $this->recipient = $recipient; + $this->simNumber = $simNumber; + $this->data = $data; + $this->receivedAt = $receivedAt; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + return new self( + $obj->messageId, + $obj->phoneNumber, + $obj->sender, + $obj->recipient ?? null, + $obj->simNumber ?? null, + $obj->data, + $obj->receivedAt + ); + } + + public function MessageId(): string { + return $this->messageId; + } + + public function PhoneNumber(): string { + return $this->phoneNumber; + } + + public function Sender(): string { + return $this->sender; + } + + public function Recipient(): ?string { + return $this->recipient; + } + + public function SimNumber(): ?int { + return $this->simNumber; + } + + public function Data(): string { + return $this->data; + } + + public function ReceivedAt(): string { + return $this->receivedAt; + } +} diff --git a/src/Domain/SmsReceivedPayload.php b/src/Domain/SmsReceivedPayload.php new file mode 100644 index 0000000..8a8a307 --- /dev/null +++ b/src/Domain/SmsReceivedPayload.php @@ -0,0 +1,78 @@ +messageId = $messageId; + $this->phoneNumber = $phoneNumber; + $this->sender = $sender; + $this->recipient = $recipient; + $this->simNumber = $simNumber; + $this->message = $message; + $this->receivedAt = $receivedAt; + } + + /** + * @param object $obj + * @return self + */ + public static function FromObject(object $obj): self { + return new self( + $obj->messageId, + $obj->phoneNumber, + $obj->sender, + $obj->recipient ?? null, + $obj->simNumber ?? null, + $obj->message, + $obj->receivedAt + ); + } + + public function MessageId(): string { + return $this->messageId; + } + + public function PhoneNumber(): string { + return $this->phoneNumber; + } + + public function Sender(): string { + return $this->sender; + } + + public function Recipient(): ?string { + return $this->recipient; + } + + public function SimNumber(): ?int { + return $this->simNumber; + } + + public function Message(): string { + return $this->message; + } + + public function ReceivedAt(): string { + return $this->receivedAt; + } +} diff --git a/src/Enums/IncomingMessageType.php b/src/Enums/IncomingMessageType.php new file mode 100644 index 0000000..d9daf55 --- /dev/null +++ b/src/Enums/IncomingMessageType.php @@ -0,0 +1,55 @@ +value = $value; + } + + public static function SMS(): self { + return new self(self::SMS); + } + + public static function DATA_SMS(): self { + return new self(self::DATA_SMS); + } + + public static function MMS(): self { + return new self(self::MMS); + } + + public static function MMS_DOWNLOADED(): self { + return new self(self::MMS_DOWNLOADED); + } + + public static function FromValue(string $value): self { + return new self($value); + } + + public function Value(): string { + return $this->value; + } + + public function __toString(): string { + return $this->value; + } +} diff --git a/src/Enums/WebhookDelivery.php b/src/Enums/WebhookDelivery.php new file mode 100644 index 0000000..b9ef658 --- /dev/null +++ b/src/Enums/WebhookDelivery.php @@ -0,0 +1,49 @@ +value = $value; + } + + public static function DISABLED(): self { + return new self(self::DISABLED); + } + + public static function INDIVIDUAL(): self { + return new self(self::INDIVIDUAL); + } + + public static function BATCH(): self { + return new self(self::BATCH); + } + + public static function FromValue(string $value): self { + return new self($value); + } + + public function Value(): string { + return $this->value; + } + + public function __toString(): string { + return $this->value; + } +} diff --git a/src/Enums/WebhookEvent.php b/src/Enums/WebhookEvent.php index 353dbd1..8c021cd 100644 --- a/src/Enums/WebhookEvent.php +++ b/src/Enums/WebhookEvent.php @@ -12,6 +12,10 @@ final class WebhookEvent { public const APP_STARTED = 'app:started'; public const MMS_RECEIVED = 'mms:received'; public const MMS_DOWNLOADED = 'mms:downloaded'; + public const SMS_BATCH_RECEIVED = 'sms:batch:received'; + public const SMS_DATA_BATCH_RECEIVED = 'sms:batch:data-received'; + public const MMS_BATCH_RECEIVED = 'mms:batch:received'; + public const MMS_BATCH_DOWNLOADED = 'mms:batch:downloaded'; private const _ALL_ = [ self::SMS_RECEIVED, @@ -23,6 +27,10 @@ final class WebhookEvent { self::APP_STARTED, self::MMS_RECEIVED, self::MMS_DOWNLOADED, + self::SMS_BATCH_RECEIVED, + self::SMS_DATA_BATCH_RECEIVED, + self::MMS_BATCH_RECEIVED, + self::MMS_BATCH_DOWNLOADED, ]; private string $value; @@ -71,6 +79,22 @@ public static function MMS_DOWNLOADED(): self { return new self(self::MMS_DOWNLOADED); } + public static function SMS_BATCH_RECEIVED(): self { + return new self(self::SMS_BATCH_RECEIVED); + } + + public static function SMS_DATA_BATCH_RECEIVED(): self { + return new self(self::SMS_DATA_BATCH_RECEIVED); + } + + public static function MMS_BATCH_RECEIVED(): self { + return new self(self::MMS_BATCH_RECEIVED); + } + + public static function MMS_BATCH_DOWNLOADED(): self { + return new self(self::MMS_BATCH_DOWNLOADED); + } + public static function FromValue(string $value): self { return new self($value); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6ad4955..4bbf742 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -3,6 +3,7 @@ namespace AndroidSmsGateway\Tests; use AndroidSmsGateway\Client; +use AndroidSmsGateway\Domain\InboxRefreshRequest; use AndroidSmsGateway\Domain\Message; use AndroidSmsGateway\Domain\MessagesExportRequest; use AndroidSmsGateway\Domain\MessageState; @@ -10,8 +11,11 @@ use AndroidSmsGateway\Domain\Webhook; use AndroidSmsGateway\Domain\TokenRequest; use AndroidSmsGateway\Domain\TokenResponse; +use AndroidSmsGateway\Enums\IncomingMessageType; use AndroidSmsGateway\Enums\ProcessState; +use AndroidSmsGateway\Enums\WebhookDelivery; use AndroidSmsGateway\Enums\WebhookEvent; +use AndroidSmsGateway\Exceptions\HttpException; use Http\Client\Curl\Client as CurlClient; use Http\Discovery\Psr17FactoryDiscovery; use Http\Mock\Client as MockClient; @@ -253,6 +257,90 @@ public function testRequestInboxExport(): void { $this->assertEquals('accepted', $result->status); } + public function testRefreshInbox(): void { + $request = new InboxRefreshRequest( + null, + '2020-01-01T00:00:00Z', + '2020-01-02T00:00:00Z', + [IncomingMessageType::SMS(), IncomingMessageType::DATA_SMS()], + WebhookDelivery::BATCH() + ); + + $responseMock = self::mockResponse('', 202); + + $this->mockClient->addResponse($responseMock); + + $this->client->RefreshInbox($request); + $req = $this->mockClient->getLastRequest(); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('/3rdparty/v1/inbox/refresh', $req->getUri()->getPath()); + $this->assertEquals( + 'Basic ' . base64_encode(self::MOCK_LOGIN . ':' . self::MOCK_PASSWORD), + $req->getHeaderLine('Authorization') + ); + $this->assertEquals( + 'application/json', + $req->getHeaderLine('Content-Type') + ); + + $body = json_decode((string) $req->getBody(), true); + $this->assertIsArray($body); + $this->assertEquals('Batch', $body['webhookDelivery']); + $this->assertEquals('2020-01-01T00:00:00Z', $body['since']); + $this->assertEquals('2020-01-02T00:00:00Z', $body['until']); + $this->assertEquals(['SMS', 'DATA_SMS'], $body['messageTypes']); + } + + public function testRefreshInboxServerError(): void { + $request = new InboxRefreshRequest( + null, + '2020-01-01T00:00:00Z', + '2020-01-02T00:00:00Z', + [IncomingMessageType::SMS()], + WebhookDelivery::BATCH() + ); + + $responseMock = self::mockResponse( + '{"message":"internal error"}', + 500, + ['Content-Type' => 'application/json'] + ); + + $this->mockClient->addResponse($responseMock); + + try { + $this->client->RefreshInbox($request); + $this->fail('Expected HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(500, $e->getCode()); + $this->assertSame(500, $e->getResponse()->getStatusCode()); + } + } + + public function testRefreshInboxClientError(): void { + $request = new InboxRefreshRequest( + null, + '2020-01-01T00:00:00Z', + '2020-01-02T00:00:00Z', + [], + ); + + $responseMock = self::mockResponse( + '{"message":"invalid request"}', + 400, + ['Content-Type' => 'application/json'] + ); + + $this->mockClient->addResponse($responseMock); + + try { + $this->client->RefreshInbox($request); + $this->fail('Expected HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(400, $e->getCode()); + } + } + public function testGetLogs(): void { $responseMock = self::mockResponse( '[{"id":1,"message":"Test log","module":"test","priority":"INFO","createdAt":"2020-01-01T00:00:00Z"}]', diff --git a/tests/Domain/InboxRefreshRequestTest.php b/tests/Domain/InboxRefreshRequestTest.php new file mode 100644 index 0000000..bd03ad1 --- /dev/null +++ b/tests/Domain/InboxRefreshRequestTest.php @@ -0,0 +1,87 @@ +assertSame([ + 'since' => '2026-01-01T00:00:00Z', + 'until' => '2026-01-02T00:00:00Z', + ], $this->decode($request)); + } + + public function testToObjectEmitsSetFields(): void { + $request = new InboxRefreshRequest( + 'dev-123', + '2026-01-01T00:00:00Z', + '2026-01-02T00:00:00Z', + [IncomingMessageType::SMS(), IncomingMessageType::DATA_SMS()], + WebhookDelivery::BATCH() + ); + + $this->assertSame([ + 'since' => '2026-01-01T00:00:00Z', + 'until' => '2026-01-02T00:00:00Z', + 'deviceId' => 'dev-123', + 'messageTypes' => ['SMS', 'DATA_SMS'], + 'webhookDelivery' => 'Batch', + ], $this->decode($request)); + } + + public function testToObjectEmitsWebhookDeliveryOnly(): void { + $request = new InboxRefreshRequest( + null, + '2026-01-01T00:00:00Z', + '2026-01-02T00:00:00Z', + [], + WebhookDelivery::INDIVIDUAL() + ); + + $this->assertSame([ + 'since' => '2026-01-01T00:00:00Z', + 'until' => '2026-01-02T00:00:00Z', + 'webhookDelivery' => 'Individual', + ], $this->decode($request)); + } + + public function testToObjectEmitsSparseMessageTypesAsJsonArray(): void { + $request = new InboxRefreshRequest( + null, + '2026-01-01T00:00:00Z', + '2026-01-02T00:00:00Z', + [5 => IncomingMessageType::SMS(), 2 => IncomingMessageType::DATA_SMS()], + null + ); + + $this->assertSame(['SMS', 'DATA_SMS'], $this->decode($request)['messageTypes']); + $this->assertSame( + '{"since":"2026-01-01T00:00:00Z","until":"2026-01-02T00:00:00Z","messageTypes":["SMS","DATA_SMS"]}', + json_encode($request->ToObject(), JSON_THROW_ON_ERROR) + ); + } + + /** + * @return array + */ + private function decode(InboxRefreshRequest $request): array { + $json = json_encode($request->ToObject(), JSON_THROW_ON_ERROR); + + /** @var array $decoded */ + $decoded = json_decode($json, true); + + return $decoded; + } +} diff --git a/tests/Domain/IncomingMessageTypeTest.php b/tests/Domain/IncomingMessageTypeTest.php new file mode 100644 index 0000000..4102718 --- /dev/null +++ b/tests/Domain/IncomingMessageTypeTest.php @@ -0,0 +1,54 @@ +assertSame($value, $type->Value()); + } + } + + public function testFactories(): void { + $this->assertSame(IncomingMessageType::SMS, IncomingMessageType::SMS()->Value()); + $this->assertSame(IncomingMessageType::DATA_SMS, IncomingMessageType::DATA_SMS()->Value()); + $this->assertSame(IncomingMessageType::MMS, IncomingMessageType::MMS()->Value()); + $this->assertSame(IncomingMessageType::MMS_DOWNLOADED, IncomingMessageType::MMS_DOWNLOADED()->Value()); + } + + public function testValue(): void { + $type = IncomingMessageType::FromValue(IncomingMessageType::MMS); + + $this->assertSame(IncomingMessageType::MMS, $type->Value()); + } + + public function testToString(): void { + $type = IncomingMessageType::FromValue(IncomingMessageType::DATA_SMS); + + $this->assertSame(IncomingMessageType::DATA_SMS, (string) $type); + } + + public function testFromValueInvalid(): void { + foreach (['', 'sms', 'TEXT', 'SMS '] as $value) { + try { + IncomingMessageType::FromValue($value); + + $this->fail('Expected InvalidArgumentException for value "' . $value . '"'); + } catch (\InvalidArgumentException $e) { + $this->assertSame('Invalid value', $e->getMessage()); + } + } + } +} diff --git a/tests/Domain/MmsBatchDownloadedPayloadTest.php b/tests/Domain/MmsBatchDownloadedPayloadTest.php new file mode 100644 index 0000000..c6ca8ef --- /dev/null +++ b/tests/Domain/MmsBatchDownloadedPayloadTest.php @@ -0,0 +1,92 @@ +assertInstanceOf(MmsBatchDownloadedPayload::class, $payload); + } + + public function testFromObjectMapsMessagesToInnerPayloadInstancesWithAttachments(): void { + $json = '{"messages":[' + . '{"messageId":"mms-1","phoneNumber":"+79990001111","sender":"+79990002222","receivedAt":"2026-08-18T10:00:00Z","subject":"Hello","body":"Hello World!","attachments":[' + . '{"partId":1,"contentType":"image/jpeg","name":"photo.jpg","data":"SGVsbG8gV29ybGQh","size":1024},' + . '{"partId":2,"contentType":"audio/amr","name":null,"data":null,"size":null}' + . ']},' + . '{"messageId":"mms-2","phoneNumber":"+79990003333","sender":"+79990004444","receivedAt":"2026-08-18T10:01:00Z","recipient":"+79990003333","simNumber":1,"attachments":[]},' + . '{"messageId":"mms-3","phoneNumber":"+79990005555","sender":"+79990006666","receivedAt":"2026-08-18T10:02:00Z"}' + . ']}'; + + $batch = MmsBatchDownloadedPayload::FromObject(self::decodeObject($json)); + $messages = $batch->Messages(); + + $this->assertCount(3, $messages); + $this->assertContainsOnlyInstancesOf(MmsDownloadedPayload::class, $messages); + + $this->assertSame('mms-1', $messages[0]->MessageId()); + $this->assertSame('+79990001111', $messages[0]->PhoneNumber()); + $this->assertSame('+79990002222', $messages[0]->Sender()); + $this->assertSame('Hello', $messages[0]->Subject()); + $this->assertSame('Hello World!', $messages[0]->Body()); + $this->assertNull($messages[0]->Recipient()); + $this->assertNull($messages[0]->SimNumber()); + $attachments = $messages[0]->Attachments(); + $this->assertCount(2, $attachments); + $this->assertContainsOnlyInstancesOf(MmsDownloadedAttachment::class, $attachments); + $this->assertSame(1, $attachments[0]->PartId()); + $this->assertSame('image/jpeg', $attachments[0]->ContentType()); + $this->assertSame('photo.jpg', $attachments[0]->Name()); + $this->assertSame('SGVsbG8gV29ybGQh', $attachments[0]->Data()); + $this->assertSame(1024, $attachments[0]->Size()); + $this->assertSame(2, $attachments[1]->PartId()); + $this->assertSame('audio/amr', $attachments[1]->ContentType()); + $this->assertNull($attachments[1]->Name()); + $this->assertNull($attachments[1]->Data()); + $this->assertNull($attachments[1]->Size()); + + $this->assertSame('mms-2', $messages[1]->MessageId()); + $this->assertSame('+79990003333', $messages[1]->Recipient()); + $this->assertSame(1, $messages[1]->SimNumber()); + $this->assertSame([], $messages[1]->Attachments()); + + $this->assertSame('mms-3', $messages[2]->MessageId()); + $this->assertSame([], $messages[2]->Attachments()); + $this->assertNull($messages[2]->Subject()); + $this->assertNull($messages[2]->Body()); + } + + public function testFromObjectWithMissingMessagesReturnsEmptyArray(): void { + $batch = MmsBatchDownloadedPayload::FromObject(self::decodeObject('{}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithNonArrayMessagesReturnsEmptyArray(): void { + $batch = MmsBatchDownloadedPayload::FromObject(self::decodeObject('{"messages":"not-an-array"}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithObjectMessagesReturnsEmptyArray(): void { + $batch = MmsBatchDownloadedPayload::FromObject(self::decodeObject('{"messages":{}}')); + + $this->assertSame([], $batch->Messages()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +} diff --git a/tests/Domain/MmsBatchReceivedPayloadTest.php b/tests/Domain/MmsBatchReceivedPayloadTest.php new file mode 100644 index 0000000..c27e657 --- /dev/null +++ b/tests/Domain/MmsBatchReceivedPayloadTest.php @@ -0,0 +1,74 @@ +assertInstanceOf(MmsBatchReceivedPayload::class, $payload); + } + + public function testFromObjectMapsMessagesToInnerPayloadInstances(): void { + $json = '{"messages":[' + . '{"messageId":"mms-1","phoneNumber":"+79990001111","sender":"+79990002222","transactionId":"tx-1","contentClass":"MMS","size":1024,"receivedAt":"2026-08-18T10:00:00Z"},' + . '{"messageId":"mms-2","phoneNumber":"+79990003333","sender":"+79990004444","transactionId":"tx-2","contentClass":"text","size":0,"receivedAt":"2026-08-18T10:01:00Z","recipient":"+79990003333","simNumber":1,"subject":"Hello"},' + . '{"messageId":"mms-3","phoneNumber":"+79990005555","sender":"+79990006666","transactionId":"tx-3","contentClass":"image","size":1048576,"receivedAt":"2026-08-18T10:02:00Z"}' + . ']}'; + + $batch = MmsBatchReceivedPayload::FromObject(self::decodeObject($json)); + $messages = $batch->Messages(); + + $this->assertCount(3, $messages); + $this->assertContainsOnlyInstancesOf(MmsReceivedPayload::class, $messages); + $this->assertSame('mms-1', $messages[0]->MessageId()); + $this->assertSame('tx-1', $messages[0]->TransactionId()); + $this->assertSame('MMS', $messages[0]->ContentClass()); + $this->assertSame(1024, $messages[0]->Size()); + $this->assertNull($messages[0]->Recipient()); + $this->assertNull($messages[0]->SimNumber()); + $this->assertNull($messages[0]->Subject()); + $this->assertSame('mms-2', $messages[1]->MessageId()); + $this->assertSame('tx-2', $messages[1]->TransactionId()); + $this->assertSame('text', $messages[1]->ContentClass()); + $this->assertSame(0, $messages[1]->Size()); + $this->assertSame('+79990003333', $messages[1]->Recipient()); + $this->assertSame(1, $messages[1]->SimNumber()); + $this->assertSame('Hello', $messages[1]->Subject()); + $this->assertSame('mms-3', $messages[2]->MessageId()); + $this->assertSame('tx-3', $messages[2]->TransactionId()); + $this->assertSame(1048576, $messages[2]->Size()); + } + + public function testFromObjectWithMissingMessagesReturnsEmptyArray(): void { + $batch = MmsBatchReceivedPayload::FromObject(self::decodeObject('{}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithNonArrayMessagesReturnsEmptyArray(): void { + $batch = MmsBatchReceivedPayload::FromObject(self::decodeObject('{"messages":"not-an-array"}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithObjectMessagesReturnsEmptyArray(): void { + $batch = MmsBatchReceivedPayload::FromObject(self::decodeObject('{"messages":{}}')); + + $this->assertSame([], $batch->Messages()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +} diff --git a/tests/Domain/SmsBatchDataReceivedPayloadTest.php b/tests/Domain/SmsBatchDataReceivedPayloadTest.php new file mode 100644 index 0000000..1fbbfa7 --- /dev/null +++ b/tests/Domain/SmsBatchDataReceivedPayloadTest.php @@ -0,0 +1,66 @@ +assertInstanceOf(SmsBatchDataReceivedPayload::class, $payload); + } + + public function testFromObjectMapsMessagesToInnerPayloadInstances(): void { + $json = '{"messages":[' + . '{"messageId":"d1","phoneNumber":"+79990001111","sender":"+79990002222","data":"SGVsbG8=","receivedAt":"2026-08-18T10:00:00Z"},' + . '{"messageId":"d2","phoneNumber":"+79990003333","sender":"+79990004444","recipient":"+79990003333","simNumber":2,"data":"V29ybGQ=","receivedAt":"2026-08-18T10:01:00Z"},' + . '{"messageId":"d3","phoneNumber":"+79990005555","sender":"+79990006666","data":"IQ==","receivedAt":"2026-08-18T10:02:00Z"}' + . ']}'; + + $batch = SmsBatchDataReceivedPayload::FromObject(self::decodeObject($json)); + $messages = $batch->Messages(); + + $this->assertCount(3, $messages); + $this->assertContainsOnlyInstancesOf(SmsDataReceivedPayload::class, $messages); + $this->assertSame('d1', $messages[0]->MessageId()); + $this->assertSame('SGVsbG8=', $messages[0]->Data()); + $this->assertNull($messages[0]->Recipient()); + $this->assertSame('d2', $messages[1]->MessageId()); + $this->assertSame('V29ybGQ=', $messages[1]->Data()); + $this->assertSame('+79990003333', $messages[1]->Recipient()); + $this->assertSame(2, $messages[1]->SimNumber()); + $this->assertSame('d3', $messages[2]->MessageId()); + $this->assertSame('IQ==', $messages[2]->Data()); + } + + public function testFromObjectWithMissingMessagesReturnsEmptyArray(): void { + $batch = SmsBatchDataReceivedPayload::FromObject(self::decodeObject('{}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithNonArrayMessagesReturnsEmptyArray(): void { + $batch = SmsBatchDataReceivedPayload::FromObject(self::decodeObject('{"messages":"not-an-array"}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithObjectMessagesReturnsEmptyArray(): void { + $batch = SmsBatchDataReceivedPayload::FromObject(self::decodeObject('{"messages":{}}')); + + $this->assertSame([], $batch->Messages()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +} diff --git a/tests/Domain/SmsBatchReceivedPayloadTest.php b/tests/Domain/SmsBatchReceivedPayloadTest.php new file mode 100644 index 0000000..d881586 --- /dev/null +++ b/tests/Domain/SmsBatchReceivedPayloadTest.php @@ -0,0 +1,66 @@ +assertInstanceOf(SmsBatchReceivedPayload::class, $payload); + } + + public function testFromObjectMapsMessagesToInnerPayloadInstances(): void { + $json = '{"messages":[' + . '{"messageId":"m1","phoneNumber":"+79990001111","sender":"+79990002222","message":"first","receivedAt":"2026-08-18T10:00:00Z"},' + . '{"messageId":"m2","phoneNumber":"+79990003333","sender":"+79990004444","recipient":"+79990003333","simNumber":2,"message":"second","receivedAt":"2026-08-18T10:01:00Z"},' + . '{"messageId":"m3","phoneNumber":"+79990005555","sender":"+79990006666","message":"third","receivedAt":"2026-08-18T10:02:00Z"}' + . ']}'; + + $batch = SmsBatchReceivedPayload::FromObject(self::decodeObject($json)); + $messages = $batch->Messages(); + + $this->assertCount(3, $messages); + $this->assertContainsOnlyInstancesOf(SmsReceivedPayload::class, $messages); + $this->assertSame('m1', $messages[0]->MessageId()); + $this->assertSame('first', $messages[0]->Message()); + $this->assertNull($messages[0]->Recipient()); + $this->assertSame('m2', $messages[1]->MessageId()); + $this->assertSame('second', $messages[1]->Message()); + $this->assertSame('+79990003333', $messages[1]->Recipient()); + $this->assertSame(2, $messages[1]->SimNumber()); + $this->assertSame('m3', $messages[2]->MessageId()); + $this->assertSame('third', $messages[2]->Message()); + } + + public function testFromObjectWithMissingMessagesReturnsEmptyArray(): void { + $batch = SmsBatchReceivedPayload::FromObject(self::decodeObject('{}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithNonArrayMessagesReturnsEmptyArray(): void { + $batch = SmsBatchReceivedPayload::FromObject(self::decodeObject('{"messages":"not-an-array"}')); + + $this->assertSame([], $batch->Messages()); + } + + public function testFromObjectWithObjectMessagesReturnsEmptyArray(): void { + $batch = SmsBatchReceivedPayload::FromObject(self::decodeObject('{"messages":{}}')); + + $this->assertSame([], $batch->Messages()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +} diff --git a/tests/Domain/SmsDataReceivedPayloadTest.php b/tests/Domain/SmsDataReceivedPayloadTest.php new file mode 100644 index 0000000..d9a4e1c --- /dev/null +++ b/tests/Domain/SmsDataReceivedPayloadTest.php @@ -0,0 +1,95 @@ + + */ + public static function payloadProvider(): array { + return [ + 'with recipient and simNumber' => [ + '{"messageId":"data-1","phoneNumber":"+79990001111","sender":"+79990001234","recipient":"+79990001111","simNumber":2,"data":"SGVsbG8gV29ybGQh","receivedAt":"2026-08-18T10:00:00Z"}', + 'data-1', + '+79990001111', + '+79990001234', + '+79990001111', + 2, + 'SGVsbG8gV29ybGQh', + '2026-08-18T10:00:00Z' + ], + 'without optional fields and empty data' => [ + '{"messageId":"data-2","phoneNumber":"+79990002222","sender":"+79990003333","data":"","receivedAt":"2026-08-18T11:00:00Z"}', + 'data-2', + '+79990002222', + '+79990003333', + null, + null, + '', + '2026-08-18T11:00:00Z' + ], + 'extreme values' => [ + '{"messageId":"data-3-very-long-identifier","phoneNumber":"+100000000000000","sender":"","recipient":"+100000000000000","simNumber":0,"data":"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODk=","receivedAt":"1970-01-01T00:00:00Z"}', + 'data-3-very-long-identifier', + '+100000000000000', + '', + '+100000000000000', + 0, + 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODk=', + '1970-01-01T00:00:00Z' + ] + ]; + } + + public function testCanBeInstantiated(): void { + $payload = new SmsDataReceivedPayload('data-1', '+79990001111', '+79990001234', '+79990001111', 2, 'SGVsbG8=', '2026-08-18T10:00:00Z'); + $this->assertInstanceOf(SmsDataReceivedPayload::class, $payload); + } + + /** + * @dataProvider payloadProvider + */ + public function testFromObjectParsesAllFields( + string $json, + string $messageId, + string $phoneNumber, + string $sender, + ?string $recipient, + ?int $simNumber, + string $data, + string $receivedAt + ): void { + $payload = SmsDataReceivedPayload::FromObject(self::decodeObject($json)); + + $this->assertSame($messageId, $payload->MessageId()); + $this->assertSame($phoneNumber, $payload->PhoneNumber()); + $this->assertSame($sender, $payload->Sender()); + $this->assertSame($recipient, $payload->Recipient()); + $this->assertSame($simNumber, $payload->SimNumber()); + $this->assertSame($data, $payload->Data()); + $this->assertSame($receivedAt, $payload->ReceivedAt()); + } + + public function testFromObjectDefaultsAbsentOptionalFieldsToNull(): void { + $payload = SmsDataReceivedPayload::FromObject(self::decodeObject( + '{"messageId":"data-4","phoneNumber":"+79990004444","sender":"+79990005555","data":"SGVsbG8=","receivedAt":"2026-08-18T12:00:00Z"}' + )); + + $this->assertNull($payload->Recipient()); + $this->assertNull($payload->SimNumber()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +} diff --git a/tests/Domain/SmsReceivedPayloadTest.php b/tests/Domain/SmsReceivedPayloadTest.php new file mode 100644 index 0000000..eaa1568 --- /dev/null +++ b/tests/Domain/SmsReceivedPayloadTest.php @@ -0,0 +1,95 @@ + + */ + public static function payloadProvider(): array { + return [ + 'with recipient and simNumber' => [ + '{"messageId":"msg-1","phoneNumber":"+79990001111","sender":"+79990001234","recipient":"+79990001111","simNumber":1,"message":"Hello World!","receivedAt":"2026-08-18T10:00:00Z"}', + 'msg-1', + '+79990001111', + '+79990001234', + '+79990001111', + 1, + 'Hello World!', + '2026-08-18T10:00:00Z' + ], + 'without optional fields' => [ + '{"messageId":"msg-2","phoneNumber":"+79990002222","sender":"+79990003333","message":"","receivedAt":"2026-08-18T11:00:00Z"}', + 'msg-2', + '+79990002222', + '+79990003333', + null, + null, + '', + '2026-08-18T11:00:00Z' + ], + 'extreme values' => [ + '{"messageId":"msg-3-very-long-identifier","phoneNumber":"+100000000000000","sender":"","recipient":"+100000000000000","simNumber":0,"message":"First line\nSecond line","receivedAt":"1970-01-01T00:00:00Z"}', + 'msg-3-very-long-identifier', + '+100000000000000', + '', + '+100000000000000', + 0, + "First line\nSecond line", + '1970-01-01T00:00:00Z' + ] + ]; + } + + public function testCanBeInstantiated(): void { + $payload = new SmsReceivedPayload('msg-1', '+79990001111', '+79990001234', '+79990001111', 1, 'Hello', '2026-08-18T10:00:00Z'); + $this->assertInstanceOf(SmsReceivedPayload::class, $payload); + } + + /** + * @dataProvider payloadProvider + */ + public function testFromObjectParsesAllFields( + string $json, + string $messageId, + string $phoneNumber, + string $sender, + ?string $recipient, + ?int $simNumber, + string $message, + string $receivedAt + ): void { + $payload = SmsReceivedPayload::FromObject(self::decodeObject($json)); + + $this->assertSame($messageId, $payload->MessageId()); + $this->assertSame($phoneNumber, $payload->PhoneNumber()); + $this->assertSame($sender, $payload->Sender()); + $this->assertSame($recipient, $payload->Recipient()); + $this->assertSame($simNumber, $payload->SimNumber()); + $this->assertSame($message, $payload->Message()); + $this->assertSame($receivedAt, $payload->ReceivedAt()); + } + + public function testFromObjectDefaultsAbsentOptionalFieldsToNull(): void { + $payload = SmsReceivedPayload::FromObject(self::decodeObject( + '{"messageId":"msg-4","phoneNumber":"+79990004444","sender":"+79990005555","message":"No extras","receivedAt":"2026-08-18T12:00:00Z"}' + )); + + $this->assertNull($payload->Recipient()); + $this->assertNull($payload->SimNumber()); + } + + private static function decodeObject(string $json): stdClass { + $obj = json_decode($json, false); + if (!$obj instanceof stdClass) { + throw new RuntimeException('Test fixture must decode to an object: ' . $json); + } + + return $obj; + } +}