diff --git a/src/Lunr/Vortex/FCM/FCMBatchResponse.php b/src/Lunr/Vortex/FCM/FCMBatchResponse.php index f6f0f07..e9391c1 100644 --- a/src/Lunr/Vortex/FCM/FCMBatchResponse.php +++ b/src/Lunr/Vortex/FCM/FCMBatchResponse.php @@ -11,6 +11,7 @@ namespace Lunr\Vortex\FCM; use InvalidArgumentException; +use Lunr\Vortex\PushNotificationBroadcastResponseInterface; use Lunr\Vortex\PushNotificationResponseInterface; use Lunr\Vortex\PushNotificationStatus; use Psr\Log\LoggerInterface; @@ -21,7 +22,7 @@ /** * Firebase Cloud Messaging Push Notification response wrapper. */ -class FCMBatchResponse implements PushNotificationResponseInterface +class FCMBatchResponse implements PushNotificationResponseInterface, PushNotificationBroadcastResponseInterface { /** * Shared instance of a Logger class. @@ -47,6 +48,12 @@ class FCMBatchResponse implements PushNotificationResponseInterface */ private array $endpoints; + /** + * The status for a broadcast. + * @var PushNotificationStatus + */ + protected PushNotificationStatus $broadcast_status; + /** * Set of error types that indicate a curl error. * @var array @@ -71,7 +78,11 @@ public function __construct(array $responses, LoggerInterface $logger, array $en $this->endpoints = $endpoints; $this->responses = $responses; - if (array_is_list($responses) && count($responses) === 1) + if ($endpoints === []) + { + $this->set_broadcast_status(); + } + elseif (array_is_list($responses) && count($responses) === 1) { foreach ($endpoints as $endpoint) { @@ -93,6 +104,7 @@ public function __destruct() unset($this->responses); unset($this->statuses); unset($this->endpoints); + unset($this->broadcast_status); } /** @@ -215,6 +227,84 @@ private function report_endpoint_error(string $endpoint, Response $response): vo $this->statuses[$endpoint] = $status; } + /** + * Get notification delivery status for a broadcast. + * + * @return PushNotificationStatus Delivery status for the broadcast + */ + public function get_broadcast_status(): PushNotificationStatus + { + return $this->broadcast_status ?? PushNotificationStatus::Unknown; + } + + /** + * Set endpoint statuses + * + * @return void + */ + private function set_broadcast_status(): void + { + $response = $this->responses[0]; + + if ($response instanceof RequestsException) + { + $this->logger->warning( + 'Dispatching FCM broadcast failed: {error}', + [ 'error' => $response->getMessage() ] + ); + + if (in_array($response->getType(), self::CURL_ERROR_TYPES)) + { + $this->broadcast_status = PushNotificationStatus::TemporaryError; + return; + } + + $this->broadcast_status = PushNotificationStatus::Unknown; + return; + } + + $json_content = json_decode($response->body, TRUE); + + $error_message = $json_content['error']['message'] ?? NULL; + $error_code = $json_content['error']['details'][0]['errorCode'] ?? NULL; + + switch ($response->status_code) + { + case 200: + $this->broadcast_status = PushNotificationStatus::Success; + return; + case 400: + $status = PushNotificationStatus::Error; + $error_message ??= 'Invalid argument'; + break; + case 401: + $status = PushNotificationStatus::Error; + $error_message ??= 'Error with authentication'; + break; + case 429: + $status = PushNotificationStatus::TemporaryError; + $error_message ??= 'Exceeded qouta error'; + break; + case 500: + $status = PushNotificationStatus::TemporaryError; + $error_message ??= 'Internal error'; + break; + case 503: + $status = PushNotificationStatus::TemporaryError; + $error_message ??= 'Timeout'; + break; + default: + $status = PushNotificationStatus::Unknown; + $error_message ??= $error_code ?? 'Unknown error'; + break; + } + + $context = [ 'error' => $error_message ]; + $this->logger->warning('Dispatching FCM broadcast failed: {error}', $context); + + $this->broadcast_status = $status; + } + } ?> diff --git a/src/Lunr/Vortex/FCM/FCMDispatcher.php b/src/Lunr/Vortex/FCM/FCMDispatcher.php index e78e173..9f3d00e 100644 --- a/src/Lunr/Vortex/FCM/FCMDispatcher.php +++ b/src/Lunr/Vortex/FCM/FCMDispatcher.php @@ -284,7 +284,9 @@ public function push(object $payload, array &$endpoints): FCMResponse if ($payload->is_broadcast()) { - $fcm_response->add_batch_response($this->push_batch($payload, $endpoints), $endpoints); + $batch_response = $this->push_batch($payload, $endpoints); + + $fcm_response->add_broadcast_response($batch_response); return $fcm_response; } diff --git a/src/Lunr/Vortex/FCM/FCMResponse.php b/src/Lunr/Vortex/FCM/FCMResponse.php index e7fee5a..99689a3 100644 --- a/src/Lunr/Vortex/FCM/FCMResponse.php +++ b/src/Lunr/Vortex/FCM/FCMResponse.php @@ -10,13 +10,14 @@ namespace Lunr\Vortex\FCM; +use Lunr\Vortex\PushNotificationBroadcastResponseInterface; use Lunr\Vortex\PushNotificationResponseInterface; use Lunr\Vortex\PushNotificationStatus; /** * Firebase Cloud Messaging Push Notification response wrapper. */ -class FCMResponse implements PushNotificationResponseInterface +class FCMResponse implements PushNotificationResponseInterface, PushNotificationBroadcastResponseInterface { /** @@ -25,6 +26,12 @@ class FCMResponse implements PushNotificationResponseInterface */ protected array $statuses; + /** + * The status for a broadcast. + * @var PushNotificationStatus + */ + protected PushNotificationStatus $broadcast_status; + /** * Constructor. */ @@ -39,6 +46,7 @@ public function __construct() public function __destruct() { unset($this->statuses); + unset($this->broadcast_status); } /** @@ -57,6 +65,18 @@ public function add_batch_response(FCMBatchResponse $batch_response, array $endp } } + /** + * Add the results of a batch response. + * + * @param FCMBatchResponse $batch_response Push response. + * + * @return void + */ + public function add_broadcast_response(FCMBatchResponse $batch_response): void + { + $this->broadcast_status = $batch_response->get_broadcast_status(); + } + /** * Get notification delivery status for an endpoint. * @@ -69,6 +89,16 @@ public function get_status(string $endpoint): PushNotificationStatus return isset($this->statuses[$endpoint]) ? $this->statuses[$endpoint] : PushNotificationStatus::Unknown; } + /** + * Get notification delivery status for a broadcast. + * + * @return PushNotificationStatus Delivery status for the broadcast + */ + public function get_broadcast_status(): PushNotificationStatus + { + return $this->broadcast_status ?? PushNotificationStatus::Unknown; + } + } ?> diff --git a/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseBasePushTest.php b/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseBasePushTest.php index e504074..c883ae2 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseBasePushTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseBasePushTest.php @@ -725,6 +725,159 @@ public function testPushErrorsMultiple(): void $this->assertPropertySame('responses', $responses); } + /** + * Test constructor behavior for push success with broadcast success. + * + * @covers Lunr\Vortex\FCM\FCMBatchResponse::__construct + */ + public function testPushSuccessWithBroadcastSuccess(): void + { + $content = file_get_contents(TEST_STATICS . '/Vortex/fcm/response_single_success.json'); + $endpoints = []; + + $this->response->status_code = 200; + $this->response->body = $content; + + $this->logger->expects($this->never()) + ->method('warning'); + + $responses = [ + $this->response, + ]; + + $this->class = new FCMBatchResponse($responses, $this->logger, $endpoints); + + parent::baseSetUp($this->class); + + $broadcast_status = PushNotificationStatus::Success; + + $this->assertPropertySame('logger', $this->logger); + $this->assertPropertySame('endpoints', $endpoints); + $this->assertPropertySame('broadcast_status', $broadcast_status); + $this->assertPropertySame('responses', $responses); + } + + /** + * Test constructor behavior for error of push notification in case of failed request. + * + * @covers Lunr\Vortex\FCM\FCMBatchResponse::__construct + */ + public function testPushErrorFailedRequestWithBroadcast(): void + { + $this->mock_function('curl_errno', function () { return 10; }); + + $endpoints = []; + + $responses = [ new RequestsException('cURL error 10: Request error', 'curlerror', NULL) ]; + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Dispatching FCM broadcast failed: {error}', + [ 'error' => 'cURL error 10: Request error' ] + ); + + $this->class = new FCMBatchResponse($responses, $this->logger, $endpoints); + + parent::baseSetUp($this->class); + + $this->assertPropertySame('logger', $this->logger); + $this->assertPropertySame('endpoints', $endpoints); + $this->assertPropertySame('broadcast_status', PushNotificationStatus::TemporaryError); + $this->assertPropertySame('responses', $responses); + + $this->unmock_function('curl_errno'); + } + + /** + * Test constructor behavior for error of push notification in case of failed request. + * + * @covers Lunr\Vortex\FCM\FCMBatchResponse::__construct + */ + public function testPushErrorUnknownFailedRequestWithBroadcast(): void + { + $this->mock_function('curl_errno', function () { return 10; }); + + $endpoints = []; + + $responses = [ new RequestsException('Unknown error', 'error', NULL) ]; + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Dispatching FCM broadcast failed: {error}', + [ 'error' => 'Unknown error' ] + ); + + $this->class = new FCMBatchResponse($responses, $this->logger, $endpoints); + + parent::baseSetUp($this->class); + + $this->assertPropertySame('logger', $this->logger); + $this->assertPropertySame('endpoints', $endpoints); + $this->assertPropertySame('broadcast_status', PushNotificationStatus::Unknown); + $this->assertPropertySame('responses', $responses); + + $this->unmock_function('curl_errno'); + } + + /** + * Unit test data provider. + * + * @return array $data array of fcm errors + */ + public function errorBroadcastDataProvider(): array + { + $data = []; + + $data[] = [ 'Invalid argument', 400, PushNotificationStatus::Error ]; + $data[] = [ 'Error with authentication', 401, PushNotificationStatus::Error ]; + $data[] = [ 'Exceeded qouta error', 429, PushNotificationStatus::TemporaryError ]; + $data[] = [ 'Internal error', 500, PushNotificationStatus::TemporaryError ]; + $data[] = [ 'Timeout', 503, PushNotificationStatus::TemporaryError ]; + $data[] = [ 'Unknown error', 440, PushNotificationStatus::Unknown ]; + + return $data; + } + + /** + * Test constructor behavior for push success with broadcast success. + * + * @param string $error_msg Error message. + * @param int $http_code Http code of the response. + * @param PushNotificationStatus $status The expected status. + * + * @dataProvider errorBroadcastDataProvider + * @covers Lunr\Vortex\FCM\FCMBatchResponse::__construct + */ + public function testPushWithBroadcastFailures(string $error_msg, int $http_code, PushNotificationStatus $status): void + { + $endpoints = []; + + $this->response->status_code = $http_code; + $this->response->body = ''; + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Dispatching FCM broadcast failed: {error}', + [ 'error' => $error_msg ] + ); + + $responses = [ + $this->response, + ]; + + $this->class = new FCMBatchResponse($responses, $this->logger, $endpoints); + + parent::baseSetUp($this->class); + + $this->assertPropertySame('logger', $this->logger); + $this->assertPropertySame('endpoints', $endpoints); + $this->assertPropertySame('broadcast_status', $status); + $this->assertPropertySame('responses', $responses); + } + } ?> diff --git a/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseGetBroadcastStatusTest.php b/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseGetBroadcastStatusTest.php new file mode 100644 index 0000000..e88e699 --- /dev/null +++ b/src/Lunr/Vortex/FCM/Tests/FCMBatchResponseGetBroadcastStatusTest.php @@ -0,0 +1,66 @@ +response->status_code = 200; + $this->response->body = $content; + + $this->class = new FCMBatchResponse([ $this->response ], $this->logger, []); + + parent::baseSetUp($this->class); + } + + /** + * Test the get_broadcast_status() returns Success + * + * @covers Lunr\Vortex\FCM\FCMBatchResponse::get_broadcast_status + */ + public function testGetStatusReturnsSuccess(): void + { + $this->assertSame(PushNotificationStatus::Success, $this->class->get_broadcast_status()); + } + + /** + * Test the get_broadcast_status() returns Unknown + * + * @covers Lunr\Vortex\FCM\FCMBatchResponse::get_broadcast_status + */ + public function testGetStatusReturnsUnknown(): void + { + $this->set_reflection_property_value('broadcast_status', PushNotificationStatus::Unknown); + + $this->assertSame(PushNotificationStatus::Unknown, $this->class->get_broadcast_status()); + } + +} + +?> diff --git a/src/Lunr/Vortex/FCM/Tests/FCMResponseBaseTest.php b/src/Lunr/Vortex/FCM/Tests/FCMResponseBaseTest.php index c86eeb7..51cdbc3 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMResponseBaseTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMResponseBaseTest.php @@ -10,6 +10,8 @@ namespace Lunr\Vortex\FCM\Tests; +use Lunr\Vortex\PushNotificationStatus; + /** * This class contains tests for the constructor of the FCMResponse class * in case of a push notification error. @@ -27,6 +29,46 @@ public function testStatusesIsInitializedAsEmptyArray(): void $this->assertArrayEmpty($this->get_reflection_property_value('statuses')); } + /** + * Test get_broadcast_status when status is not set. + * + * @covers Lunr\Vortex\FCM\FCMResponse::get_broadcast_status + */ + public function testGetBroadcastStatusWhenNotSet(): void + { + $this->assertSame(PushNotificationStatus::Unknown, $this->class->get_broadcast_status()); + } + + /** + * Test get_broadcast_status when status is set. + * + * @covers Lunr\Vortex\FCM\FCMResponse::get_broadcast_status + */ + public function testGetBroadcastStatusWhenSet(): void + { + $this->set_reflection_property_value('broadcast_status', PushNotificationStatus::Success); + + $this->assertSame(PushNotificationStatus::Success, $this->class->get_broadcast_status()); + } + + /** + * Test add_broadcast_response sets broadcast status. + * + * @covers Lunr\Vortex\FCM\FCMResponse::add_broadcast_response + */ + public function testAddBroadcastResponseSetsStatus(): void + { + $this->set_reflection_property_value('broadcast_status', PushNotificationStatus::Unknown); + + $this->batch_response->expects($this->once()) + ->method('get_broadcast_status') + ->willReturn(PushNotificationStatus::Success); + + $this->class->add_broadcast_response($this->batch_response); + + $this->assertSame(PushNotificationStatus::Success, $this->get_reflection_property_value('broadcast_status')); + } + } ?> diff --git a/src/Lunr/Vortex/Tests/PushNotificationDispatcherDispatchTest.php b/src/Lunr/Vortex/Tests/PushNotificationDispatcherDispatchTest.php index f65d0dc..bc6ce03 100644 --- a/src/Lunr/Vortex/Tests/PushNotificationDispatcherDispatchTest.php +++ b/src/Lunr/Vortex/Tests/PushNotificationDispatcherDispatchTest.php @@ -979,11 +979,15 @@ public function testDispatchSendsCorrectBroadcastPayload(): void $this->fcm_response->expects($this->never()) ->method('get_status'); + $this->fcm_response->expects($this->once()) + ->method('get_broadcast_status') + ->willReturn(PushNotificationStatus::Success); + $this->class->dispatch([], $payloads); $expected_statuses = [ - PushNotificationStatus::Unknown->value => [ 'fcm' => [ 'data' => $data_payload ], 'email' => [ 'email' => $email_payload ]], - PushNotificationStatus::Success->value => [], + PushNotificationStatus::Unknown->value => [ 'email' => [ 'email' => $email_payload ]], + PushNotificationStatus::Success->value => [ 'fcm' => [ 'data' => $data_payload ] ], PushNotificationStatus::TemporaryError->value => [], PushNotificationStatus::InvalidEndpoint->value => [], PushNotificationStatus::ClientError->value => [],