From 3c44d21c8e88c3b72971f726ce639588b1b9adee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Sat, 18 Jul 2026 01:14:38 +0200 Subject: [PATCH 1/4] Add eventual assertion for integration tests --- tests/IntegrationTestCase.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/IntegrationTestCase.php b/tests/IntegrationTestCase.php index 341ddc02d..3f0064cc3 100644 --- a/tests/IntegrationTestCase.php +++ b/tests/IntegrationTestCase.php @@ -10,8 +10,10 @@ use function array_rand; use function bin2hex; +use function hrtime; use function mb_strtolower; use function random_bytes; +use function sleep; /** * @internal @@ -89,6 +91,31 @@ protected static function randomEmail(string $suffix = ''): string return self::randomString($suffix.'@example.com'); } + /** + * @param callable(): bool $condition + * @param positive-int $timeoutInSeconds + */ + protected function assertEventually( + callable $condition, + int $timeoutInSeconds = 10, + ?string $message = null, + ): void { + $retryUntil = hrtime()[0] + $timeoutInSeconds; + $conditionIsMet = $condition(); + + while (!$conditionIsMet && hrtime()[0] < $retryUntil) { + sleep(1); + $conditionIsMet = $condition(); + } + + $this->assertTrue( + $conditionIsMet, + in_array($message, [null, ''], true) + ? "Condition did not become true within {$timeoutInSeconds} seconds." + : $message + ); + } + /** * @return non-empty-string|null */ From 9a54834ff6e033fb55696b4525bd0350e2aea716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Sat, 18 Jul 2026 01:17:44 +0200 Subject: [PATCH 2/4] Use eventual assertion for App Check replay protection --- tests/Integration/AppCheckTest.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/Integration/AppCheckTest.php b/tests/Integration/AppCheckTest.php index 1a58989e8..04a88a100 100644 --- a/tests/Integration/AppCheckTest.php +++ b/tests/Integration/AppCheckTest.php @@ -4,7 +4,6 @@ namespace Kreait\Firebase\Tests\Integration; -use Kreait\Firebase\AppCheck\VerifyAppCheckTokenResponse; use Kreait\Firebase\Contract\AppCheck; use Kreait\Firebase\Contract\AppCheckWithReplayProtection; use Kreait\Firebase\Tests\IntegrationTestCase; @@ -58,24 +57,20 @@ public function testVerifyTokenWithReplayProtection(): void $firstVerification = $this->appCheck->verifyTokenWithReplayProtection($token->token); $secondVerification = $this->appCheck->verifyTokenWithReplayProtection($token->token); - // Replay-consumption state may not be visible immediately, so retry briefly. - if ($secondVerification->alreadyConsumed !== true) { - for ($attempt = 0; $attempt < 3; ++$attempt) { - sleep(1); - $secondVerification = $this->appCheck->verifyTokenWithReplayProtection($token->token); + // Replay-consumption state may not be visible immediately. + $this->assertEventually(function () use ($token, &$secondVerification): bool { + if ($secondVerification->alreadyConsumed === true) { + return true; + } - if ($secondVerification->alreadyConsumed === true) { - break; - } + $secondVerification = $this->appCheck->verifyTokenWithReplayProtection($token->token); - $secondVerification = null; - } - } + return $secondVerification->alreadyConsumed === true; + }, 3, 'Replay-consumption state did not become visible within 3 seconds.'); $this->assertSame(self::$appId, $firstVerification->appId); $this->assertSame(self::$appId, $firstVerification->token->app_id); $this->assertFalse($firstVerification->alreadyConsumed); - $this->assertInstanceOf(VerifyAppCheckTokenResponse::class, $secondVerification); $this->assertTrue($secondVerification->alreadyConsumed); } } From 07d73b3476c75d5bd97081ac92b720d150105815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Sat, 18 Jul 2026 01:17:54 +0200 Subject: [PATCH 3/4] Wait for topic subscription changes in integration test --- .../Integration/Messaging/AppInstanceTest.php | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/tests/Integration/Messaging/AppInstanceTest.php b/tests/Integration/Messaging/AppInstanceTest.php index 912f1545f..92eb55287 100644 --- a/tests/Integration/Messaging/AppInstanceTest.php +++ b/tests/Integration/Messaging/AppInstanceTest.php @@ -36,21 +36,56 @@ public function testItIsSubscribedToTopics(): void $this->messaging->subscribeToTopic($secondTopic, RegistrationToken::fromValue($token)); // Lazy registration token test $this->messaging->subscribeToTopic($thirdTopic, $token); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($firstTopic)); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($secondTopic)); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($thirdTopic)); + $this->assertTopicSubscriptionsEventuallyMatch($token, [ + $firstTopic => true, + $secondTopic => true, + $thirdTopic => true, + ]); $this->messaging->unsubscribeFromTopic($firstTopic, $token); - $this->assertFalse($this->appInstance($token)->isSubscribedToTopic($firstTopic)); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($secondTopic)); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($thirdTopic)); + $this->assertTopicSubscriptionsEventuallyMatch($token, [ + $firstTopic => false, + $secondTopic => true, + $thirdTopic => true, + ]); $this->messaging->unsubscribeFromTopic($secondTopic, $token); - $this->assertFalse($this->appInstance($token)->isSubscribedToTopic($secondTopic)); - $this->assertTrue($this->appInstance($token)->isSubscribedToTopic($thirdTopic)); + $this->assertTopicSubscriptionsEventuallyMatch($token, [ + $firstTopic => false, + $secondTopic => false, + $thirdTopic => true, + ]); $this->messaging->unsubscribeFromAllTopics($token); - $this->assertFalse($this->appInstance($token)->isSubscribedToTopic($thirdTopic)); + $this->assertTopicSubscriptionsEventuallyMatch($token, [ + $firstTopic => false, + $secondTopic => false, + $thirdTopic => false, + ]); + } + + /** + * @param non-empty-string $registrationToken + * @param array $expectedSubscriptions + */ + private function assertTopicSubscriptionsEventuallyMatch( + string $registrationToken, + array $expectedSubscriptions, + ): void { + $this->assertEventually( + function () use ($registrationToken, $expectedSubscriptions): bool { + $appInstance = $this->appInstance($registrationToken); + + foreach ($expectedSubscriptions as $topic => $expectedSubscription) { + if ($appInstance->isSubscribedToTopic($topic) !== $expectedSubscription) { + return false; + } + } + + return true; + }, + message: 'Topic subscriptions did not reach their expected state within 10 seconds.', + ); } /** From 16d37fb1b9b0de6b520fcfacbaa7046f9a37caa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Sun, 19 Jul 2026 00:01:25 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Integration/Messaging/AppInstanceTest.php | 3 ++- tests/IntegrationTestCase.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Messaging/AppInstanceTest.php b/tests/Integration/Messaging/AppInstanceTest.php index 92eb55287..d6e183ae5 100644 --- a/tests/Integration/Messaging/AppInstanceTest.php +++ b/tests/Integration/Messaging/AppInstanceTest.php @@ -84,7 +84,8 @@ function () use ($registrationToken, $expectedSubscriptions): bool { return true; }, - message: 'Topic subscriptions did not reach their expected state within 10 seconds.', + 10, + 'Topic subscriptions did not reach their expected state within 10 seconds.', ); } diff --git a/tests/IntegrationTestCase.php b/tests/IntegrationTestCase.php index 3f0064cc3..577c7886d 100644 --- a/tests/IntegrationTestCase.php +++ b/tests/IntegrationTestCase.php @@ -100,10 +100,10 @@ protected function assertEventually( int $timeoutInSeconds = 10, ?string $message = null, ): void { - $retryUntil = hrtime()[0] + $timeoutInSeconds; + $retryUntil = hrtime(true) + ($timeoutInSeconds * 1_000_000_000); $conditionIsMet = $condition(); - while (!$conditionIsMet && hrtime()[0] < $retryUntil) { + while (!$conditionIsMet && hrtime(true) < $retryUntil) { sleep(1); $conditionIsMet = $condition(); }