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); } } diff --git a/tests/Integration/Messaging/AppInstanceTest.php b/tests/Integration/Messaging/AppInstanceTest.php index 912f1545f..d6e183ae5 100644 --- a/tests/Integration/Messaging/AppInstanceTest.php +++ b/tests/Integration/Messaging/AppInstanceTest.php @@ -36,21 +36,57 @@ 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; + }, + 10, + 'Topic subscriptions did not reach their expected state within 10 seconds.', + ); } /** diff --git a/tests/IntegrationTestCase.php b/tests/IntegrationTestCase.php index 341ddc02d..577c7886d 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(true) + ($timeoutInSeconds * 1_000_000_000); + $conditionIsMet = $condition(); + + while (!$conditionIsMet && hrtime(true) < $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 */