Http::response([ 'data' => [ ['status' => 'ok', 'id' => 'ticket-one'], ['status' => 'ok', 'id' => 'ticket-two'], ], ]), ]); $user = User::factory()->create(); $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[first]', 'platform' => 'ios', ]); $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[second]', 'platform' => 'android', ]); (new ExpoPushChannel)->send($user, expoPushTestNotification()); Http::assertSent(fn (Request $request) => $request->url() === 'https://exp.host/--/api/v2/push/send' && $request->data() === [ [ 'to' => 'ExponentPushToken[first]', 'title' => 'Nouveau repas', 'body' => 'Un ami vient de publier son repas.', 'data' => ['meal_post_id' => 123], ], [ 'to' => 'ExponentPushToken[second]', 'title' => 'Nouveau repas', 'body' => 'Un ami vient de publier son repas.', 'data' => ['meal_post_id' => 123], ], ]); }); it('can be used from a notification via the expo channel alias', function () { Http::fake([ 'https://exp.host/*' => Http::response([ 'data' => [ ['status' => 'ok', 'id' => 'ticket-one'], ], ]), ]); $user = User::factory()->create(); $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[first]', 'platform' => 'ios', ]); $user->notify(new class extends Notification { public function via(object $notifiable): array { return ['expo']; } public function toExpoPush(object $notifiable): array { return [ 'title' => 'Nouveau repas', 'body' => 'Un ami vient de publier son repas.', 'data' => ['meal_post_id' => 123], ]; } }); Http::assertSent(fn (Request $request) => $request->data()[0]['to'] === 'ExponentPushToken[first]'); }); it('removes device tokens rejected as not registered by expo', function () { Http::fake([ 'https://exp.host/*' => Http::response([ 'data' => [ [ 'status' => 'error', 'message' => '"ExponentPushToken[invalid]" is not a registered push notification recipient', 'details' => ['error' => 'DeviceNotRegistered'], ], ['status' => 'ok', 'id' => 'ticket-two'], ], ]), ]); $user = User::factory()->create(); $invalidToken = $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[invalid]', 'platform' => 'ios', ]); $validToken = $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[valid]', 'platform' => 'android', ]); (new ExpoPushChannel)->send($user, expoPushTestNotification()); expect(DeviceToken::query()->whereKey($invalidToken->getKey())->exists())->toBeFalse() ->and(DeviceToken::query()->whereKey($validToken->getKey())->exists())->toBeTrue(); }); it('chunks expo push notifications into requests of one hundred messages', function () { Http::fake([ 'https://exp.host/*' => Http::response(['data' => []]), ]); $user = User::factory()->create(); foreach (range(1, 101) as $number) { $user->deviceTokens()->create([ 'expo_push_token' => sprintf('ExponentPushToken[%03d]', $number), 'platform' => 'ios', ]); } (new ExpoPushChannel)->send($user, expoPushTestNotification()); Http::assertSentCount(2); $requests = Http::recorded() ->map(fn (array $pair) => $pair[0]->data()) ->values(); expect($requests[0])->toHaveCount(100) ->and($requests[1])->toHaveCount(1) ->and($requests[1][0]['to'])->toBe('ExponentPushToken[101]'); }); function expoPushTestNotification(): Notification { return new class extends Notification { public function toExpoPush(object $notifiable): array { return [ 'title' => 'Nouveau repas', 'body' => 'Un ami vient de publier son repas.', 'data' => ['meal_post_id' => 123], ]; } }; }