diff --git a/composer.json b/composer.json index 553590e..770c56c 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require-dev": { "lunr/halo": "dev-master", "phpunit/phpunit": ">=9.0 <9.6", + "mockery/mockery": "~1.5", "ext-xdebug": ">=3.1" }, "config": { diff --git a/decomposer.json b/decomposer.json index 8a767af..1145b58 100644 --- a/decomposer.json +++ b/decomposer.json @@ -55,5 +55,13 @@ "prefix": "WpOrg\\Requests", "search-path": "/src/" } + }, + "Mockery": { + "url": "https://github.com/mockery/mockery.git", + "version": "1.6.7", + "psr0": { + "path": "/library/" + }, + "development-only": true } } diff --git a/src/Lunr/Vortex/FCM/FCMDispatcher.php b/src/Lunr/Vortex/FCM/FCMDispatcher.php index adc61a1..0914819 100644 --- a/src/Lunr/Vortex/FCM/FCMDispatcher.php +++ b/src/Lunr/Vortex/FCM/FCMDispatcher.php @@ -10,9 +10,18 @@ namespace Lunr\Vortex\FCM; +use BadMethodCallException; +use DateTimeImmutable; use InvalidArgumentException; +use Lcobucci\JWT\Encoding\ChainedFormatter; +use Lcobucci\JWT\Encoding\JoseEncoder; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; +use Lcobucci\JWT\Token\Builder; use Lunr\Vortex\PushNotificationDispatcherInterface; use Psr\Log\LoggerInterface; +use RuntimeException; +use UnexpectedValueException; use WpOrg\Requests\Exception as RequestsException; use WpOrg\Requests\Response; use WpOrg\Requests\Session; @@ -23,10 +32,28 @@ class FCMDispatcher implements PushNotificationDispatcherInterface { /** - * Push Notification authentication token. + * Push Notification Oauth token. * @var string */ - protected string $auth_token; + protected ?string $oauth_token; + + /** + * FCM id of the project. + * @var ?string + */ + protected ?string $project_id; + + /** + * FCM client email of the project. + * @var ?string + */ + protected ?string $client_email; + + /** + * FCM id of the project. + * @var ?string + */ + protected ?string $private_key; /** * Shared instance of the Requests\Session class. @@ -45,7 +72,19 @@ class FCMDispatcher implements PushNotificationDispatcherInterface * Url to send the FCM push notification to. * @var string */ - private const GOOGLE_SEND_URL = 'https://fcm.googleapis.com/fcm/send'; + private const GOOGLE_SEND_URL = 'https://fcm.googleapis.com/v1/projects/'; + + /** + * Url to fetch the OAuth2 token. + * @var string + */ + private const GOOGLE_OAUTH_URL = 'https://oauth2.googleapis.com/token'; + + /** + * Default lifetime for the OAuth token. + * @var string + */ + private const DEFAULT_OAUTH_LIFETIME = '+10 minutes'; /** * Constructor. @@ -55,9 +94,12 @@ class FCMDispatcher implements PushNotificationDispatcherInterface */ public function __construct(Session $http, LoggerInterface $logger) { - $this->http = $http; - $this->logger = $logger; - $this->auth_token = ''; + $this->http = $http; + $this->logger = $logger; + $this->oauth_token = NULL; + $this->project_id = NULL; + $this->client_email = NULL; + $this->private_key = NULL; } /** @@ -65,11 +107,86 @@ public function __construct(Session $http, LoggerInterface $logger) */ public function __destruct() { - unset($this->auth_token); + unset($this->oauth_token); + unset($this->project_id); + unset($this->client_email); + unset($this->private_key); unset($this->http); unset($this->logger); } + /** + * Set the FCM project id for sending notifications. + * + * @param string $project_id The id of the FCM project + * + * @return $this + */ + public function set_project_id(string $project_id): static + { + $this->project_id = $project_id; + + return $this; + } + + /** + * Set the FCM client email for sending notifications. + * + * @param string $client_email The client email of the FCM project + * + * @return $this + */ + public function set_client_email(string $client_email): static + { + $this->client_email = $client_email; + + return $this; + } + + /** + * Set the FCM private key for sending notifications. + * + * @param string $private_key The private key of the FCM project + * + * @return $this + */ + public function set_private_key(string $private_key): static + { + $this->private_key = $private_key; + + return $this; + } + + /** + * Set a token to authenticate with. + * + * @param string $token The OAuth token to use + * + * @return $this + */ + public function set_oauth_token(string $token): static + { + $this->oauth_token = $token; + + return $this; + } + + /** + * Request and set an oauth token from FCM. + * + * @param string $oauth_lifetime Relative time as a string for strtotime() to parse into an expiry timestamp. + * + * @see https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative + * + * @return $this + */ + public function configure_oauth_token(string $oauth_lifetime = self::DEFAULT_OAUTH_LIFETIME): static + { + $this->set_oauth_token($this->get_oauth_token($oauth_lifetime)); + + return $this; + } + /** * Getter for FCMResponse. * @@ -105,9 +222,25 @@ public function push(object $payload, array &$endpoints): FCMResponse throw new InvalidArgumentException('No endpoints provided!'); } + if ($this->oauth_token === NULL) + { + $context = [ 'endpoint' => $endpoints[0] ]; + $this->logger->warning('Tried to push FCM notification to {endpoint} but wasn\'t authenticated.', $context); + + return $this->get_response($this->get_new_response_object_for_failed_request(401), $this->logger, $endpoints[0], $payload->get_payload()); + } + + if ($this->project_id === NULL) + { + $context = [ 'endpoint' => $endpoints[0] ]; + $this->logger->warning('Tried to push FCM notification to {endpoint} but project id is not provided.', $context); + + return $this->get_response($this->get_new_response_object_for_failed_request(400), $this->logger, $endpoints[0], $payload->get_payload()); + } + $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=' . $this->auth_token, + 'Authorization' => 'Bearer ' . $this->oauth_token, ]; $tmp_payload = json_decode($payload->get_payload(), TRUE); @@ -123,7 +256,7 @@ public function push(object $payload, array &$endpoints): FCMResponse 'connect_timeout' => 15 // timeout in seconds ]; - $http_response = $this->http->post(self::GOOGLE_SEND_URL, $headers, $json_payload, $options); + $http_response = $this->http->post(self::GOOGLE_SEND_URL . $this->project_id . '/messages:send', $headers, $json_payload, $options); } catch (RequestsException $e) { @@ -144,29 +277,103 @@ public function push(object $payload, array &$endpoints): FCMResponse } /** - * Set the the auth token for the http headers. + * Get the oauth token for the http headers. * - * @param string $auth_token The auth token for the fcm push notifications + * @param string $oauth_lifetime Relative time as a string for strtotime() to parse into an expiry timestamp * - * @return FCMDispatcher Self reference + * @see https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative + * + * @return string The OAuth_token */ - public function set_auth_token(string $auth_token): self + public function get_oauth_token(string $oauth_lifetime = self::DEFAULT_OAUTH_LIFETIME): string { - $this->auth_token = $auth_token; + if (strtotime($oauth_lifetime) === FALSE) + { + throw new InvalidArgumentException('Invalid oauth lifetime!'); + } - return $this; + if ($this->client_email === NULL) + { + throw new BadMethodCallException('Requesting token failed: No client email provided'); + } + + if ($this->private_key === NULL) + { + throw new BadMethodCallException('Requesting token failed: No private key provided'); + } + + $issued_at = new DateTimeImmutable(); + + $token_builder = new Builder(new JoseEncoder(), ChainedFormatter::default()); + + $token = $token_builder->issuedBy($this->client_email) + ->permittedFor('https://oauth2.googleapis.com/token') + ->issuedAt($issued_at) + ->expiresAt($issued_at->modify($oauth_lifetime)) + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->withHeader('alg', 'RS2256') + ->withHeader('typ', 'JWT') + ->getToken(new Sha256(), InMemory::plainText($this->private_key)); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $token->toString(), + ]; + + try + { + $http_response = $this->http->post(self::GOOGLE_OAUTH_URL, $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []); + } + catch (RequestsException $e) + { + $context = [ 'message' => $e->getMessage() ]; + $this->logger->warning('Fetching OAuth token for FCM notification(s) failed: {message}', $context); + + throw new RuntimeException('Fetching OAuth token for FCM notification(s) failed', 0, $e); + } + + $response_body = json_decode($http_response->body, TRUE); + + if (json_last_error() !== JSON_ERROR_NONE) + { + $context = [ 'message' => json_last_error_msg() ]; + $this->logger->warning('Processing json response for fetching OAuth token for FCM notification(s) failed: {message}', $context); + + $message = 'Processing json response for fetching OAuth token for FCM notification(s) failed: ' . $context['message']; + throw new UnexpectedValueException($message); + } + + if (!array_key_exists('access_token', $response_body)) + { + $error_msg = $response_body['error_description'] ?? 'No access token in the response body'; + + $context = [ 'error' => $error_msg ]; + $this->logger->warning('Fetching OAuth token for FCM notification(s) failed: {error}', $context); + + throw new UnexpectedValueException('Fetching OAuth token for FCM notification(s) failed: ' . $error_msg); + } + + return $response_body['access_token']; } /** * Get a Requests\Response object for a failed request. * + * @param int $http_code Set http code for the request. + * * @return Response New instance of a Requests\Response object. */ - protected function get_new_response_object_for_failed_request(): Response + protected function get_new_response_object_for_failed_request(?int $http_code = NULL): Response { $http_response = new Response(); - $http_response->url = self::GOOGLE_SEND_URL; + $http_response->url = self::GOOGLE_SEND_URL . $this->project_id . '/messages:send'; + + $http_response->status_code = $http_code ?? FALSE; return $http_response; } diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherBaseTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherBaseTest.php index 4c2e9f9..47d75d8 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherBaseTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherBaseTest.php @@ -31,11 +31,35 @@ public function testRequestsSessionIsSetCorrectly(): void } /** - * Test that the auth token is set to an empty string by default. + * Test that the OAuth token is set to null by default. */ - public function testAuthTokenIsEmptyString(): void + public function testOAuthTokenIsSetToNull(): void { - $this->assertPropertyEquals('auth_token', ''); + $this->assertPropertyEquals('oauth_token', NULL); + } + + /** + * Test that the project_id is set to null by default. + */ + public function testProjectIdIsSetToNull(): void + { + $this->assertPropertyEquals('project_id', NULL); + } + + /** + * Test that the client_email is set to null by default. + */ + public function testClientEmailIsSetToNull(): void + { + $this->assertPropertyEquals('client_email', NULL); + } + + /** + * Test that the private_key is set to null by default. + */ + public function testPrivateKeyIsSetToNull(): void + { + $this->assertPropertyEquals('private_key', NULL); } /** @@ -45,12 +69,14 @@ public function testAuthTokenIsEmptyString(): void */ public function testGetNewResponseObjectForFailedRequest(): void { + $this->set_reflection_property_value('project_id', 'fcm-project'); + $method = $this->get_accessible_reflection_method('get_new_response_object_for_failed_request'); $result = $method->invoke($this->class); $this->assertInstanceOf('WpOrg\Requests\Response', $result); - $this->assertEquals('https://fcm.googleapis.com/fcm/send', $result->url); + $this->assertEquals('https://fcm.googleapis.com/v1/projects/fcm-project/messages:send', $result->url); } /** diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherConfigureOAuthTokenTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherConfigureOAuthTokenTest.php new file mode 100644 index 0000000..ce2713a --- /dev/null +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherConfigureOAuthTokenTest.php @@ -0,0 +1,570 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid oauth lifetime!'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->configure_oauth_token('invalid relative time'); + } + + /** + * Test configure_oauth_token fails when client_email is NULL. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenFailsWhenClientEmailIsNull(): void + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Requesting token failed: No client email provided'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', NULL); + } + + /** + * Test configure_oauth_token fails when private_key is NULL. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenFailsWhenPrivateKeyIsNull(): void + { + $this->set_reflection_property_value('client_email', 'email_client'); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Requesting token failed: No private key provided'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', NULL); + } + + /** + * Test configure_oauth_token when fetching token fails. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenWhenFetchingTokenFails(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willThrowException(new RequestsException('cURL error 10: Request error', 'curlerror', NULL)); + + $this->logger->expects($this->once()) + ->method('warning') + ->with('Fetching OAuth token for FCM notification(s) failed: {message}', [ 'message' => 'cURL error 10: Request error' ]); + + $this->class->configure_oauth_token('+10 minutes'); + + $this->assertPropertyEquals('oauth_token', NULL); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test configure_oauth_token when processing json response fails. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenWhenProcessingJsonResponseFails(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Processing json response for fetching OAuth token for FCM notification(s) failed: Syntax error'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Processing json response for fetching OAuth token for FCM notification(s) failed: {message}', + [ 'message' => 'Syntax error' ] + ); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', NULL); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test configure_oauth_token when processing response fails with general error. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenFailsWithGeneralError(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed: No access token in the response body'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{"token":"oauth_token1"}'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Fetching OAuth token for FCM notification(s) failed: {error}', + [ 'error' => 'No access token in the response body' ] + ); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', NULL); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test Get_oauth_token when processing response fails with upstream error. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenFailsWithUpstreamError(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $content = file_get_contents(TEST_STATICS . '/Vortex/fcm/oauth_error.json'); + $error_msg = json_decode($content, TRUE)['error_description']; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed: ' . $error_msg); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = $content; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Fetching OAuth token for FCM notification(s) failed: {error}', + [ 'error' => $error_msg ] + ); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', NULL); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test configure_oauth_token when fetching token succeeds. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::configure_oauth_token + */ + public function testConfigureOAuthTokenWhenFetchingTokenSucceeds(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{"access_token":"oauth_token1"}'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->class->configure_oauth_token(); + + $this->assertPropertyEquals('oauth_token', 'oauth_token1'); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + +} + +?> diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherGetOAuthTokenTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherGetOAuthTokenTest.php new file mode 100644 index 0000000..01cbfe9 --- /dev/null +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherGetOAuthTokenTest.php @@ -0,0 +1,556 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid oauth lifetime!'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->get_oauth_token('invalid relative time'); + } + + /** + * Test get_oauth_token fails when client_email is NULL. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenFailsWhenClientEmailIsNull(): void + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Requesting token failed: No client email provided'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->get_oauth_token(); + } + + /** + * Test get_oauth_token fails when private_key is NULL. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenFailsWhenPrivateKeyIsNull(): void + { + $this->set_reflection_property_value('client_email', 'email_client'); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Requesting token failed: No private key provided'); + + $this->token_builder->shouldReceive('issuedBy') + ->never(); + + $this->http->expects($this->never()) + ->method('post'); + + $this->logger->expects($this->never()) + ->method('warning'); + + $this->class->get_oauth_token(); + } + + /** + * Test get_oauth_token when fetching token fails. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenWhenFetchingTokenFails(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willThrowException(new RequestsException('cURL error 10: Request error', 'curlerror', NULL)); + + $this->logger->expects($this->once()) + ->method('warning') + ->with('Fetching OAuth token for FCM notification(s) failed: {message}', [ 'message' => 'cURL error 10: Request error' ]); + + $this->class->get_oauth_token(); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test get_oauth_token when processing json response fails. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenWhenProcessingJsonResponseFails(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Processing json response for fetching OAuth token for FCM notification(s) failed: Syntax error'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Processing json response for fetching OAuth token for FCM notification(s) failed: {message}', + [ 'message' => 'Syntax error' ] + ); + + $this->class->get_oauth_token(); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test get_oauth_token when processing response fails with general error. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenFailsWithGeneralError(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed: No access token in the response body'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{"token":"oauth_token1"}'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Fetching OAuth token for FCM notification(s) failed: {error}', + [ 'error' => 'No access token in the response body' ] + ); + + $this->class->get_oauth_token(); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test get_oauth_token when processing response fails with upstream error. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenFailsWithUpstreamError(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $content = file_get_contents(TEST_STATICS . '/Vortex/fcm/oauth_error.json'); + $error_msg = json_decode($content, TRUE)['error_description']; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Fetching OAuth token for FCM notification(s) failed: ' . $error_msg); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = $content; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Fetching OAuth token for FCM notification(s) failed: {error}', + [ 'error' => $error_msg ] + ); + + $this->class->get_oauth_token(); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + + /** + * Test get_oauth_token when fetching token succeeds. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::get_oauth_token + */ + public function testGetOAuthTokenWhenFetchingTokenSucceeds(): void + { + if (!extension_loaded('uopz')) + { + $this->markTestSkipped('The uopz extension is not available.'); + } + + $this->set_reflection_property_value('client_email', 'email_client'); + $this->set_reflection_property_value('private_key', 'secret_key'); + + $issued_at = Mockery::mock(DateTimeImmutable::class); + $expires_at = Mockery::mock(DateTimeImmutable::class); + + $issued_at->expects() + ->modify('+10 minutes') + ->andReturn($expires_at); + + uopz_set_mock(DateTimeImmutable::class, $issued_at); + uopz_set_mock(Builder::class, $this->token_builder); + + $this->token_builder->expects() + ->issuedBy('email_client') + ->andReturnSelf(); + + $this->token_builder->expects() + ->permittedFor('https://oauth2.googleapis.com/token') + ->andReturnSelf(); + + $this->token_builder->expects() + ->issuedAt($issued_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->expiresAt($expires_at) + ->andReturnSelf(); + + $this->token_builder->expects() + ->withClaim('scope', 'https://www.googleapis.com/auth/firebase.messaging') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('alg', 'RS2256') + ->andReturnSelf(); + + $this->token_builder->expects() + ->withHeader('typ', 'JWT') + ->andReturnSelf(); + + uopz_set_return($this->token_builder::class, 'getToken', $this->token_plain); + + $this->token_plain->expects($this->once()) + ->method('toString') + ->willReturn('jwt_token'); + + $headers = [ + 'Content-Type' => 'application/json' + ]; + + $payload = [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => 'jwt_token', + ]; + + $response = new Response(); + + $response->body = '{"access_token":"oauth_token1"}'; + + $this->http->expects($this->once()) + ->method('post') + ->with('https://oauth2.googleapis.com/token', $headers, json_encode($payload, JSON_UNESCAPED_UNICODE), []) + ->willReturn($response); + + $this->assertSame('oauth_token1', $this->class->get_oauth_token()); + + uopz_unset_return($this->token_builder::class, 'getToken'); + uopz_unset_mock(DateTimeImmutable::class); + uopz_unset_mock(Builder::class); + } + +} + +?> diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherPushTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherPushTest.php index 473a4b0..7d923ea 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherPushTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherPushTest.php @@ -60,7 +60,7 @@ public function testPushingWithUnsupportedPayloadThrowsException($payload): void } /** - * Test that push() returns FCMResponseObject. + * Test that push() throws exception when no endpoints are provided. * * @covers Lunr\Vortex\FCM\FCMDispatcher::push */ @@ -74,6 +74,73 @@ public function testPushWithEmptyEndpointsThrowException(): void $this->class->push($this->payload, $endpoints); } + /** + * Test that push() returns response when oauth_token is null. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::push + */ + public function testPushWhenOAuthTokenIsNull(): void + { + $endpoints = [ 'endpoint' ]; + + $this->payload->expects($this->once()) + ->method('get_payload') + ->willReturn('{"collapse_key":"abcde-12345"}'); + + $this->set_reflection_property_value('oauth_token', NULL); + + $this->logger->expects($this->exactly(2)) + ->method('warning') + ->withConsecutive( + [ + 'Tried to push FCM notification to {endpoint} but wasn\'t authenticated.', + [ 'endpoint' => 'endpoint' ] + ], + [ + 'Dispatching FCM notification failed for endpoint {endpoint}: {error}', + [ 'endpoint' => 'endpoint', 'error' => 'Error with authentication' ] + ] + ); + + $result = $this->class->push($this->payload, $endpoints); + + $this->assertInstanceOf('Lunr\Vortex\FCM\FCMResponse', $result); + } + + /** + * Test that push() returns response when project_id is null. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::push + */ + public function testPushWhenProjectIdIsNull(): void + { + $endpoints = [ 'endpoint' ]; + + $this->payload->expects($this->once()) + ->method('get_payload') + ->willReturn('{"collapse_key":"abcde-12345"}'); + + $this->set_reflection_property_value('oauth_token', 'test'); + $this->set_reflection_property_value('project_id', NULL); + + $this->logger->expects($this->exactly(2)) + ->method('warning') + ->withConsecutive( + [ + 'Tried to push FCM notification to {endpoint} but project id is not provided.', + [ 'endpoint' => 'endpoint' ] + ], + [ + 'Dispatching FCM notification failed for endpoint {endpoint}: {error}', + [ 'endpoint' => 'endpoint', 'error' => 'Invalid JSON ()' ] + ] + ); + + $result = $this->class->push($this->payload, $endpoints); + + $this->assertInstanceOf('Lunr\Vortex\FCM\FCMResponse', $result); + } + /** * Test that push() resets the properties after a push. * @@ -87,7 +154,8 @@ public function testPushResetsProperties(): void ->method('get_payload') ->willReturn('{"collapse_key":"abcde-12345"}'); - $this->set_reflection_property_value('auth_token', 'auth_token'); + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); $response = $this->getMockBuilder('WpOrg\Requests\Response')->getMock(); @@ -97,7 +165,8 @@ public function testPushResetsProperties(): void $this->class->push($this->payload, $endpoints); - $this->assertPropertyEquals('auth_token', 'auth_token'); + $this->assertPropertyEquals('oauth_token', 'oauth_token'); + $this->assertPropertyEquals('project_id', 'fcm-project'); } /** @@ -107,16 +176,19 @@ public function testPushResetsProperties(): void */ public function testPushWithFailedRequest(): void { + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); + $this->mock_function('curl_errno', function () { return 10; }); $endpoints = [ 'endpoint' ]; $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=', + 'Authorization' => 'Bearer oauth_token', ]; - $url = 'https://fcm.googleapis.com/fcm/send'; + $url = 'https://fcm.googleapis.com/v1/projects/fcm-project/messages:send'; $post = '{"to":"endpoint"}'; $options = [ @@ -150,16 +222,19 @@ public function testPushWithFailedRequest(): void */ public function testPushWithTimeoutRequest() { + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); + $this->mock_function('curl_errno', function () { return 28; }); $endpoints = [ 'endpoint' ]; $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=', + 'Authorization' => 'Bearer oauth_token', ]; - $url = 'https://fcm.googleapis.com/fcm/send'; + $url = 'https://fcm.googleapis.com/v1/projects/fcm-project/messages:send'; $post = '{"to":"endpoint"}'; $options = [ @@ -195,16 +270,19 @@ public function testPushWithTimeoutRequest() */ public function testPushRequestWithDefaultValues(): void { + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); + $endpoints = [ 'endpoint' ]; $response = $this->getMockBuilder('WpOrg\Requests\Response')->getMock(); $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=', + 'Authorization' => 'Bearer oauth_token', ]; - $url = 'https://fcm.googleapis.com/fcm/send'; + $url = 'https://fcm.googleapis.com/v1/projects/fcm-project/messages:send'; $post = '{"to":"endpoint"}'; $options = [ @@ -227,22 +305,23 @@ public function testPushRequestWithDefaultValues(): void */ public function testPushRequestWithSingleEndpoint(): void { + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); + $endpoints = [ 'endpoint' ]; $this->payload->expects($this->once()) ->method('get_payload') ->willReturn('{"collapse_key":"abcde-12345"}'); - $this->set_reflection_property_value('auth_token', 'auth_token'); - $response = $this->getMockBuilder('WpOrg\Requests\Response')->getMock(); $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=auth_token', + 'Authorization' => 'Bearer oauth_token', ]; - $url = 'https://fcm.googleapis.com/fcm/send'; + $url = 'https://fcm.googleapis.com/v1/projects/fcm-project/messages:send'; $post = '{"collapse_key":"abcde-12345","to":"endpoint"}'; $options = [ @@ -265,22 +344,23 @@ public function testPushRequestWithSingleEndpoint(): void */ public function testPushRequestWithMultibyteCharacters(): void { + $this->set_reflection_property_value('oauth_token', 'oauth_token'); + $this->set_reflection_property_value('project_id', 'fcm-project'); + $endpoints = [ 'endpoint' ]; $this->payload->expects($this->once()) ->method('get_payload') ->willReturn('{"collapse_key":"abcde-12345","data":{"message":"凄い"}}'); - $this->set_reflection_property_value('auth_token', 'auth_token'); - $response = $this->getMockBuilder('WpOrg\Requests\Response')->getMock(); $headers = [ 'Content-Type' => 'application/json', - 'Authorization' => 'key=auth_token', + 'Authorization' => 'Bearer oauth_token', ]; - $url = 'https://fcm.googleapis.com/fcm/send'; + $url = 'https://fcm.googleapis.com/v1/projects/fcm-project/messages:send'; $post = '{"collapse_key":"abcde-12345","data":{"message":"凄い"},"to":"endpoint"}'; $options = [ diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherSetTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherSetTest.php index 57fcdfa..9cd7250 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherSetTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherSetTest.php @@ -10,6 +10,8 @@ namespace Lunr\Vortex\FCM\Tests; +use InvalidArgumentException; + /** * This class contains tests for the setters of the FCMDispatcher class. * @@ -19,25 +21,81 @@ class FCMDispatcherSetTest extends FCMDispatcherTest { /** - * Test that set_auth_token() sets the endpoint. + * Test that set_project_id() sets the project_id. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_project_id + */ + public function testSetProjectIDSetsProjectId(): void + { + $this->class->set_project_id('project_id'); + + $this->assertPropertyEquals('project_id', 'project_id'); + } + + /** + * Test the return of set_project_id(). + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_project_id + */ + public function testSetProjectIdReturnsSelfReference(): void + { + $this->assertEquals($this->class, $this->class->set_project_id('project_id')); + } + + /** + * Test that set_client_email() sets the client_email. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_client_email + */ + public function testSetClientEmailSetsClientEmail(): void + { + $this->class->set_client_email('email'); + + $this->assertPropertyEquals('client_email', 'email'); + } + + /** + * Test the return of set_client_email(). * - * @covers Lunr\Vortex\FCM\FCMDispatcher::set_auth_token + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_client_email */ - public function testSetAuthTokenSetsPayload(): void + public function testSetClientEmailReturnsSelfReference(): void { - $this->class->set_auth_token('auth_token'); + $this->assertEquals($this->class, $this->class->set_client_email('email')); + } - $this->assertPropertyEquals('auth_token', 'auth_token'); + /** + * Test that set_private_key() sets the private_key. + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_private_key + */ + public function testSetPrivateKeySetsPrivateKey(): void + { + $this->class->set_private_key('key'); + + $this->assertPropertyEquals('private_key', 'key'); + } + + /** + * Test the return of set_private_key(). + * + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_private_key + */ + public function testSetPrivateKeyReturnsSelfReference(): void + { + $this->assertEquals($this->class, $this->class->set_private_key('key')); } /** - * Test the fluid interface of set_auth_token(). + * Test that set_oauth_token() sets the oauth_token. * - * @covers Lunr\Vortex\FCM\FCMDispatcher::set_auth_token + * @covers Lunr\Vortex\FCM\FCMDispatcher::set_oauth_token */ - public function testSetAuthTokenReturnsSelfReference(): void + public function testSetOAuthTokenSetsOAuthToken(): void { - $this->assertEquals($this->class, $this->class->set_auth_token('auth_token')); + $this->class->set_oauth_token('token'); + + $this->assertPropertyEquals('oauth_token', 'token'); } } diff --git a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherTest.php b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherTest.php index 2c63ee9..b22d315 100644 --- a/src/Lunr/Vortex/FCM/Tests/FCMDispatcherTest.php +++ b/src/Lunr/Vortex/FCM/Tests/FCMDispatcherTest.php @@ -10,13 +10,17 @@ namespace Lunr\Vortex\FCM\Tests; -use Lunr\Vortex\FCM\FCMDispatcher; +use Lcobucci\JWT\Builder; +use Lcobucci\JWT\UnencryptedToken; use Lunr\Halo\LunrBaseTest; +use Lunr\Vortex\FCM\FCMDispatcher; use Lunr\Vortex\FCM\FCMPayload; +use Mockery; +use Mockery\MockInterface; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use WpOrg\Requests\Response; use WpOrg\Requests\Session; -use ReflectionClass; /** * This class contains common setup routines, providers @@ -50,6 +54,18 @@ abstract class FCMDispatcherTest extends LunrBaseTest */ protected $payload; + /** + * Mock instance of the token builder class. + * @var MockInterface + */ + protected $token_builder; + + /** + * Mock instance of the token UnencryptedToken class. + * @var MockObject + */ + protected $token_plain; + /** * Instance of the tested class. * @var FCMDispatcher @@ -68,6 +84,10 @@ public function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->token_builder = Mockery::mock(Builder::class); + + $this->token_plain = $this->getMockBuilder(UnencryptedToken::class)->getMock(); + $this->class = new FCMDispatcher($this->http, $this->logger); parent::baseSetUp($this->class); @@ -78,6 +98,8 @@ public function setUp(): void */ public function tearDown(): void { + Mockery::close(); + unset($this->logger); unset($this->payload); unset($this->class); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 59c5fca..d0676d9 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -47,6 +47,8 @@ ../src/Lunr/Vortex/FCM/Tests/FCMResponseBasePushSuccessTest.php ../src/Lunr/Vortex/FCM/Tests/FCMResponseGetStatusTest.php ../src/Lunr/Vortex/FCM/Tests/FCMDispatcherBaseTest.php + ../src/Lunr/Vortex/FCM/Tests/FCMDispatcherConfigureOAuthTokenTest.php + ../src/Lunr/Vortex/FCM/Tests/FCMDispatcherGetOAuthTokenTest.php ../src/Lunr/Vortex/FCM/Tests/FCMDispatcherSetTest.php ../src/Lunr/Vortex/FCM/Tests/FCMDispatcherPushTest.php diff --git a/tests/statics/Vortex/fcm/oauth_error.json b/tests/statics/Vortex/fcm/oauth_error.json new file mode 100644 index 0000000..e31d06c --- /dev/null +++ b/tests/statics/Vortex/fcm/oauth_error.json @@ -0,0 +1,4 @@ +{ + "error":"invalid_grant", + "error_description":"Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe. Check your iat and exp values in the JWT claim." +}