diff --git a/src/ExpoPush.php b/src/ExpoPush.php index 4d1c830..1964302 100644 --- a/src/ExpoPush.php +++ b/src/ExpoPush.php @@ -58,7 +58,7 @@ public function sendNotifications(PushMessageCollection|array $pushMessages): Se // Prepare a new request pool $batchSize = SendNotificationsRequest::MAX_NOTIFICATION_COUNT; - $pool = $this->makeRequestPool($batchSize, $errors); + $pool = $this->makeRequestPool($batchSize, $pushMessages->notificationCount(), $errors); // Split the message collection into a set of requests and add them to the pool $pool->setRequests(function () use ($batchSize, $pushMessages): Generator { @@ -122,7 +122,7 @@ public function getReceipts(PushReceiptIdCollection|array $receiptIds): GetRecei // Prepare a new request pool $batchSize = GetReceiptsRequest::MAX_RECEIPT_COUNT; - $pool = $this->makeRequestPool($batchSize, $errors); + $pool = $this->makeRequestPool($batchSize, $receiptIds->count(), $errors); // Split the receipt ID collection into a set of requests and add them to the pool $pool->setRequests(function () use ($batchSize, $receiptIds): Generator { @@ -181,16 +181,17 @@ public function withMockClient(MockClient $mockClient): self * Make a request pool with a concurrency limit and error handler * * @param int $batchSize The max number of elements to be sent per request + * @param int $totalCount The total number of elements being sent across all requests * @param PushErrorCollection $errors A collection in which to store any push errors * * @return Pool */ - protected function makeRequestPool(int $batchSize, PushErrorCollection $errors): Pool + protected function makeRequestPool(int $batchSize, int $totalCount, PushErrorCollection $errors): Pool { // Make a new pool with a concurrency limit and exception handler return $this->connector->pool( concurrency: $this->connector::MAX_CONCURRENT_REQUESTS, - exceptionHandler: new RequestExceptionHandler($batchSize, $errors), + exceptionHandler: new RequestExceptionHandler($batchSize, $totalCount, $errors), ); } } diff --git a/src/Request/RequestExceptionHandler.php b/src/Request/RequestExceptionHandler.php index d471f5f..eb0d9a7 100644 --- a/src/Request/RequestExceptionHandler.php +++ b/src/Request/RequestExceptionHandler.php @@ -12,12 +12,12 @@ final class RequestExceptionHandler { - public function __construct(protected int $batchSize, protected PushErrorCollection $errors) {} + public function __construct(protected int $batchSize, protected int $totalCount, protected PushErrorCollection $errors) {} public function __invoke(FatalRequestException|RequestException|RateLimitReachedException $exception, int $requestIndex): void { $startIndex = $requestIndex * $this->batchSize; - $endIndex = $startIndex + $this->batchSize - 1; + $endIndex = min($startIndex + $this->batchSize, $this->totalCount) - 1; // The request completely failed if ($exception instanceof FatalRequestException) { diff --git a/tests/Feature/GetReceiptsTest.php b/tests/Feature/GetReceiptsTest.php index 2771c22..fd536cb 100644 --- a/tests/Feature/GetReceiptsTest.php +++ b/tests/Feature/GetReceiptsTest.php @@ -174,6 +174,62 @@ public function get_receipts_leaves_index_gaps_for_request_errors(): void $this->assertEquals(PushErrorCode::PushTooManyReceipts, $result->errors->get(0)->code); } + #[Test] + public function get_receipts_reports_correct_end_index_for_partial_final_batch(): void + { + $receiptIds = $this->generatePushReceiptIds(10500); + + foreach ($receiptIds->chunk(1000) as $index => $receiptIdChunk) { + + if ($index === 10) { + $this->mockClient->addResponse( + MockResponse::make( + body: [ + 'errors' => [ + [ + 'code' => 'PUSH_TOO_MANY_RECEIPTS', + 'message' => 'You are trying to get more than 1000 push receipts in one request', + ], + ], + ], + status: 400, + headers: ['Content-Type' => 'application/json'], + ), + ); + + continue; + } + + $responseBody = [ + 'data' => array_combine( + $receiptIdChunk->toArray(), + array_fill(0, count($receiptIdChunk), ['status' => 'ok']), + ), + ]; + + $this->mockClient->addResponse( + MockResponse::make( + body: $responseBody, + headers: ['Content-Type' => 'application/json'], + ), + ); + } + + $result = $this->service->getReceipts($receiptIds); + + $this->mockClient->assertSentCount(11, GetReceiptsRequest::class); + + $this->assertCount(10000, $result->receipts); + + $this->assertTrue($result->hasErrors()); + $this->assertCount(1, $result->errors); + + $error = $result->errors->get(0); + $this->assertEquals(PushErrorCode::PushTooManyReceipts, $error->code); + $this->assertEquals(10000, $error->startIndex); + $this->assertEquals(10499, $error->endIndex); + } + #[Test] public function get_receipts_exposes_push_errors_for_each_request_error(): void { diff --git a/tests/Feature/SendNotificationsTest.php b/tests/Feature/SendNotificationsTest.php index 1e69547..fe5c475 100644 --- a/tests/Feature/SendNotificationsTest.php +++ b/tests/Feature/SendNotificationsTest.php @@ -305,6 +305,62 @@ public function send_notifications_leaves_index_gaps_for_request_errors(): void $this->assertEquals(PushErrorCode::PushTooManyExperienceIds, $result->errors->get(0)->code); } + #[Test] + public function send_notifications_reports_correct_end_index_for_partial_final_batch(): void + { + $messages = $this->generatePushMessages(950); + + foreach ($messages->chunk(100) as $index => $messageChunk) { + + if ($index === 9) { + $this->mockClient->addResponse( + MockResponse::make( + body: [ + 'errors' => [ + [ + 'code' => 'PUSH_TOO_MANY_EXPERIENCE_IDS', + 'message' => 'You are trying to send push notifications to different Expo experiences', + ], + ], + ], + status: 400, + headers: ['Content-Type' => 'application/json'], + ), + ); + + continue; + } + + $responseBody = [ + 'data' => array_map(fn(PushMessage $message) => [ + 'status' => 'ok', + 'id' => $this->generatePushReceiptId(), + ], $messageChunk->toArray()), + ]; + + $this->mockClient->addResponse( + MockResponse::make( + body: $responseBody, + headers: ['Content-Type' => 'application/json'], + ), + ); + } + + $result = $this->service->sendNotifications($messages); + + $this->mockClient->assertSentCount(10, SendNotificationsRequest::class); + + $this->assertCount(900, $result->tickets); + + $this->assertTrue($result->hasErrors()); + $this->assertCount(1, $result->errors); + + $error = $result->errors->get(0); + $this->assertEquals(PushErrorCode::PushTooManyExperienceIds, $error->code); + $this->assertEquals(900, $error->startIndex); + $this->assertEquals(949, $error->endIndex); + } + #[Test] public function send_notifications_exposes_push_errors_for_each_request_error(): void { diff --git a/tests/Unit/Request/RequestExceptionHandlerTest.php b/tests/Unit/Request/RequestExceptionHandlerTest.php index aca8365..059fc9b 100644 --- a/tests/Unit/Request/RequestExceptionHandlerTest.php +++ b/tests/Unit/Request/RequestExceptionHandlerTest.php @@ -26,7 +26,7 @@ protected function setUp(): void parent::setUp(); $this->errors = new PushErrorCollection(); - $this->handler = new RequestExceptionHandler(batchSize: 100, errors: $this->errors); + $this->handler = new RequestExceptionHandler(batchSize: 100, totalCount: 1000, errors: $this->errors); } #[Test] @@ -194,4 +194,20 @@ public function invoke_with_request_exception_and_missing_errors_key_adds_generi $this->assertEquals(400, $error->startIndex); $this->assertEquals(499, $error->endIndex); } + + #[Test] + public function invoke_clamps_end_index_for_partial_final_batch(): void + { + $handler = new RequestExceptionHandler(batchSize: 100, totalCount: 950, errors: $this->errors); + + $fatalRequestException = $this->createMock(FatalRequestException::class); + + $handler($fatalRequestException, 9); + + $this->assertCount(1, $this->errors); + + $error = $this->errors->get(0); + $this->assertEquals(900, $error->startIndex); + $this->assertEquals(949, $error->endIndex); + } }