Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/ExpoPush.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
);
}
}
4 changes: 2 additions & 2 deletions src/Request/RequestExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
56 changes: 56 additions & 0 deletions tests/Feature/GetReceiptsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
56 changes: 56 additions & 0 deletions tests/Feature/SendNotificationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
18 changes: 17 additions & 1 deletion tests/Unit/Request/RequestExceptionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
}
Loading