diff --git a/README.md b/README.md index 8ab46d0..2a58ccd 100644 --- a/README.md +++ b/README.md @@ -46,49 +46,35 @@ A complete example with html+JS frontend and php backend using `web-push-php` ca ```php $subscription, 'payload' => '{"message":"Hello World!"}', ], [ - // current PushSubscription format (browsers might change this in the future) - 'subscription' => Subscription::create([ - "endpoint" => "https://example.com/other/endpoint/of/another/vendor/abcdef...", - "keys" => [ - 'p256dh' => '(stringOf88Chars)', - 'auth' => '(stringOf24Chars)' - ], - ]), - 'payload' => '{"message":"Hello World!"}', - ], [ - // old Firefox PushSubscription format - 'subscription' => Subscription::create([ - 'endpoint' => 'https://updates.push.services.mozilla.com/push/abc...', // Firefox 43+, - 'publicKey' => 'BPcMbnWQL5GOYX/5LKZXT6sLmHiMsJSiEvIFvfcDvX7IZ9qqtq68onpTPEYmyxSQNiH7UD/98AUcQ12kBoxz/0s=', // base 64 encoded, should be 88 chars - 'authToken' => 'CxVX6QsVToEGEcjfYPqXQw==', // base 64 encoded, should be 24 chars - ]), - 'payload' => 'hello !', - ], [ - // old Chrome PushSubscription format - 'subscription' => Subscription::create([ - 'endpoint' => 'https://fcm.googleapis.com/fcm/send/abcdef...', + // current PushSubscription format (browsers might change this in the future) + 'subscription' => Subscription::create([ + 'endpoint' => 'https://example.com/other/endpoint/of/another/vendor/abcdef...', + 'keys' => [ + 'p256dh' => '(stringOf88Chars)', + 'auth' => '(stringOf24Chars)', + ], + // key 'contentEncoding' is optional and defaults to ContentEncoding::aes128gcm ]), - 'payload' => null, + 'payload' => '{"message":"Hello World!"}', ], [ // old PushSubscription format 'subscription' => Subscription::create([ 'endpoint' => 'https://example.com/other/endpoint/of/another/vendor/abcdef...', 'publicKey' => '(stringOf88Chars)', 'authToken' => '(stringOf24Chars)', - 'contentEncoding' => 'aesgcm', // one of PushManager.supportedContentEncodings + 'contentEncoding' => 'aesgcm', // (optional) one of PushManager.supportedContentEncodings ]), 'payload' => '{"message":"test"}', ] @@ -96,16 +82,16 @@ $notifications = [ $webPush = new WebPush(); -// send multiple notifications with payload +// Send multiple push messages with payload. foreach ($notifications as $notification) { $webPush->queueNotification( $notification['subscription'], - $notification['payload'] // optional (defaults null) + $notification['payload'], // optional (defaults null) ); } /** - * Check sent results + * Check sent results. * @var MessageSentReport $report */ foreach ($webPush->flush() as $report) { @@ -119,7 +105,7 @@ foreach ($webPush->flush() as $report) { } /** - * send one notification and flush directly + * Send one push message and flush directly. * @var MessageSentReport $report */ $report = $webPush->sendOneNotification( diff --git a/src/ContentEncoding.php b/src/ContentEncoding.php new file mode 100644 index 0000000..b525608 --- /dev/null +++ b/src/ContentEncoding.php @@ -0,0 +1,11 @@ +value, $context, $contentEncoding); $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16); // section 3.3, derive the nonce @@ -132,16 +138,19 @@ public static function deterministicEncrypt(string $payload, string $userPublicK ]; } - public static function getContentCodingHeader(string $salt, string $localPublicKey, string $contentEncoding): string + public static function getContentCodingHeader(string $salt, string $localPublicKey, ContentEncoding $contentEncoding): string { - if ($contentEncoding === "aes128gcm") { + if ($contentEncoding === ContentEncoding::aesgcm) { + return ""; + } + if ($contentEncoding === ContentEncoding::aes128gcm) { return $salt .pack('N*', 4096) .pack('C*', Utils::safeStrlen($localPublicKey)) .$localPublicKey; } - return ""; + throw new \ValueError("This content encoding is not implemented."); } /** @@ -182,19 +191,19 @@ private static function hkdf(string $salt, string $ikm, string $info, int $lengt * * @throws \ErrorException */ - private static function createContext(string $clientPublicKey, string $serverPublicKey, string $contentEncoding): ?string + private static function createContext(string $clientPublicKey, string $serverPublicKey, ContentEncoding $contentEncoding): ?string { - if ($contentEncoding === "aes128gcm") { + if ($contentEncoding === ContentEncoding::aes128gcm) { return null; } if (Utils::safeStrlen($clientPublicKey) !== 65) { - throw new \ErrorException('Invalid client public key length'); + throw new \ErrorException('Invalid client public key length.'); } // This one should never happen, because it's our code that generates the key if (Utils::safeStrlen($serverPublicKey) !== 65) { - throw new \ErrorException('Invalid server public key length'); + throw new \ErrorException('Invalid server public key length.'); } $len = chr(0).'A'; // 65 as Uint16BE @@ -212,25 +221,25 @@ private static function createContext(string $clientPublicKey, string $serverPub * * @throws \ErrorException */ - private static function createInfo(string $type, ?string $context, string $contentEncoding): string + private static function createInfo(string $type, ?string $context, ContentEncoding $contentEncoding): string { - if ($contentEncoding === "aesgcm") { + if ($contentEncoding === ContentEncoding::aesgcm) { if (!$context) { - throw new \ErrorException('Context must exist'); + throw new \ValueError('Context must exist.'); } if (Utils::safeStrlen($context) !== 135) { - throw new \ErrorException('Context argument has invalid size'); + throw new \ValueError('Context argument has invalid size.'); } return 'Content-Encoding: '.$type.chr(0).'P-256'.$context; } - if ($contentEncoding === "aes128gcm") { + if ($contentEncoding === ContentEncoding::aes128gcm) { return 'Content-Encoding: '.$type.chr(0); } - throw new \ErrorException('This content encoding is not supported.'); + throw new \ErrorException('This content encoding is not implemented.'); } private static function createLocalKeyObject(): array @@ -262,17 +271,18 @@ private static function createLocalKeyObject(): array /** * @throws \ValueError */ - private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string + private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, ContentEncoding $contentEncoding): string { if (empty($userAuthToken)) { return $sharedSecret; } - if($contentEncoding === "aesgcm") { + + if ($contentEncoding === ContentEncoding::aesgcm) { $info = 'Content-Encoding: auth'.chr(0); - } elseif($contentEncoding === "aes128gcm") { + } elseif ($contentEncoding === ContentEncoding::aes128gcm) { $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey; } else { - throw new \ValueError("This content encoding is not supported."); + throw new \ValueError("This content encoding is not implemented."); } return self::hkdf($userAuthToken, $sharedSecret, $info, 32); diff --git a/src/Subscription.php b/src/Subscription.php index bad3061..3d505ff 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -15,22 +15,33 @@ class Subscription implements SubscriptionInterface { + protected ContentEncoding $contentEncoding; /** - * @param string|null $contentEncoding (Optional) Must be "aesgcm" + * This is a data class. No key validation is done. + * @param string|\Minishlink\WebPush\ContentEncoding $contentEncoding (Optional) defaults to "aes128gcm" as defined to rfc8291. * @throws \ErrorException */ public function __construct( - private string $endpoint, - private ?string $publicKey = null, - private ?string $authToken = null, - private ?string $contentEncoding = null + protected readonly string $endpoint, + protected readonly string $publicKey, + protected readonly string $authToken, + ContentEncoding|string $contentEncoding = ContentEncoding::aes128gcm, ) { - if($publicKey || $authToken || $contentEncoding) { - $supportedContentEncodings = ['aesgcm', 'aes128gcm']; - if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings, true)) { - throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.'); + if(is_string($contentEncoding)) { + try { + if(empty($contentEncoding)) { + $this->contentEncoding = ContentEncoding::aesgcm; // default + } else { + $this->contentEncoding = ContentEncoding::from($contentEncoding); + } + } catch(\ValueError) { + throw new \ValueError('This content encoding ('.$contentEncoding.') is not supported.'); } - $this->contentEncoding = $contentEncoding ?: "aesgcm"; + } else { + $this->contentEncoding = $contentEncoding; + } + if(empty($publicKey) || empty($authToken)) { + throw new \ValueError('Missing values.'); } } @@ -42,55 +53,37 @@ public static function create(array $associativeArray): self { if (array_key_exists('keys', $associativeArray) && is_array($associativeArray['keys'])) { return new self( - $associativeArray['endpoint'], - $associativeArray['keys']['p256dh'] ?? null, - $associativeArray['keys']['auth'] ?? null, - $associativeArray['contentEncoding'] ?? "aesgcm" - ); - } - - if (array_key_exists('publicKey', $associativeArray) || array_key_exists('authToken', $associativeArray) || array_key_exists('contentEncoding', $associativeArray)) { - return new self( - $associativeArray['endpoint'], - $associativeArray['publicKey'] ?? null, - $associativeArray['authToken'] ?? null, - $associativeArray['contentEncoding'] ?? "aesgcm" + $associativeArray['endpoint'] ?? "", + $associativeArray['keys']['p256dh'] ?? "", + $associativeArray['keys']['auth'] ?? "", + $associativeArray['contentEncoding'] ?? ContentEncoding::aes128gcm, ); } return new self( - $associativeArray['endpoint'] + $associativeArray['endpoint'] ?? "", + $associativeArray['publicKey'] ?? "", + $associativeArray['authToken'] ?? "", + $associativeArray['contentEncoding'] ?? ContentEncoding::aes128gcm, ); } - /** - * {@inheritDoc} - */ public function getEndpoint(): string { return $this->endpoint; } - /** - * {@inheritDoc} - */ - public function getPublicKey(): ?string + public function getPublicKey(): string { return $this->publicKey; } - /** - * {@inheritDoc} - */ - public function getAuthToken(): ?string + public function getAuthToken(): string { return $this->authToken; } - /** - * {@inheritDoc} - */ - public function getContentEncoding(): ?string + public function getContentEncoding(): ContentEncoding { return $this->contentEncoding; } diff --git a/src/SubscriptionInterface.php b/src/SubscriptionInterface.php index 51f9744..8cbe8f7 100644 --- a/src/SubscriptionInterface.php +++ b/src/SubscriptionInterface.php @@ -14,15 +14,16 @@ namespace Minishlink\WebPush; /** + * Subscription details from user agent. * @author Sergii Bondarenko */ interface SubscriptionInterface { public function getEndpoint(): string; - public function getPublicKey(): ?string; + public function getPublicKey(): string; - public function getAuthToken(): ?string; + public function getAuthToken(): string; - public function getContentEncoding(): ?string; + public function getContentEncoding(): ContentEncoding; } diff --git a/src/VAPID.php b/src/VAPID.php index 4ba3de9..50e85d1 100644 --- a/src/VAPID.php +++ b/src/VAPID.php @@ -97,7 +97,7 @@ public static function validate(array $vapid): array * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers * @throws \ErrorException */ - public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null): array + public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, ContentEncoding $contentEncoding, ?int $expiration = null): array { $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h if (null === $expiration || $expiration > $expirationLimit) { @@ -138,14 +138,14 @@ public static function getVapidHeaders(string $audience, string $subject, string $jwt = $jwsCompactSerializer->serialize($jws, 0); $encodedPublicKey = Base64UrlSafe::encodeUnpadded($publicKey); - if ($contentEncoding === "aesgcm") { + if ($contentEncoding === ContentEncoding::aesgcm) { return [ 'Authorization' => 'WebPush '.$jwt, 'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey, ]; } - if ($contentEncoding === 'aes128gcm') { + if ($contentEncoding === ContentEncoding::aes128gcm) { return [ 'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey, ]; diff --git a/src/WebPush.php b/src/WebPush.php index 4f20faa..de6004a 100644 --- a/src/WebPush.php +++ b/src/WebPush.php @@ -92,10 +92,6 @@ public function queueNotification(SubscriptionInterface $subscription, ?string $ } $contentEncoding = $subscription->getContentEncoding(); - if (!$contentEncoding) { - throw new \ErrorException('Subscription should have a content encoding'); - } - $payload = Encryption::padPayload($payload, $this->automaticPadding, $contentEncoding); } @@ -193,10 +189,6 @@ protected function prepare(array $notifications): array $auth = $notification->getAuth($this->auth); if (!empty($payload) && !empty($userPublicKey) && !empty($userAuthToken)) { - if (!$contentEncoding) { - throw new \ErrorException('Subscription should have a content encoding'); - } - $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $contentEncoding); $cipherText = $encrypted['cipherText']; $salt = $encrypted['salt']; @@ -204,10 +196,10 @@ protected function prepare(array $notifications): array $headers = [ 'Content-Type' => 'application/octet-stream', - 'Content-Encoding' => $contentEncoding, + 'Content-Encoding' => $contentEncoding->value, ]; - if ($contentEncoding === "aesgcm") { + if ($contentEncoding === ContentEncoding::aesgcm) { $headers['Encryption'] = 'salt='.Base64UrlSafe::encodeUnpadded($salt); $headers['Crypto-Key'] = 'dh='.Base64UrlSafe::encodeUnpadded($localPublicKey); } @@ -234,7 +226,7 @@ protected function prepare(array $notifications): array $headers['Topic'] = $options['topic']; } - if (array_key_exists('VAPID', $auth) && $contentEncoding) { + if (array_key_exists('VAPID', $auth)) { $audience = parse_url($endpoint, PHP_URL_SCHEME).'://'.parse_url($endpoint, PHP_URL_HOST); if (!parse_url($audience)) { throw new \ErrorException('Audience "'.$audience.'"" could not be generated.'); @@ -244,7 +236,7 @@ protected function prepare(array $notifications): array $headers['Authorization'] = $vapidHeaders['Authorization']; - if ($contentEncoding === 'aesgcm') { + if ($contentEncoding === ContentEncoding::aesgcm) { if (array_key_exists('Crypto-Key', $headers)) { $headers['Crypto-Key'] .= ';'.$vapidHeaders['Crypto-Key']; } else { @@ -335,13 +327,13 @@ public function countPendingNotifications(): int /** * @throws \ErrorException */ - protected function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid): ?array + protected function getVAPIDHeaders(string $audience, ContentEncoding $contentEncoding, array $vapid): ?array { $vapidHeaders = null; $cache_key = null; if ($this->reuseVAPIDHeaders) { - $cache_key = implode('#', [$audience, $contentEncoding, crc32(serialize($vapid))]); + $cache_key = implode('#', [$audience, $contentEncoding->value, crc32(serialize($vapid))]); if (array_key_exists($cache_key, $this->vapidHeaders)) { $vapidHeaders = $this->vapidHeaders[$cache_key]; } diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php index c7af076..0ec1547 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -9,6 +9,7 @@ */ use Jose\Component\Core\JWK; +use Minishlink\WebPush\ContentEncoding; use Minishlink\WebPush\Encryption; use Minishlink\WebPush\Utils; use ParagonIE\ConstantTime\Base64UrlSafe; @@ -21,7 +22,7 @@ final class EncryptionTest extends PHPUnit\Framework\TestCase { public function testDeterministicEncrypt(): void { - $contentEncoding = "aes128gcm"; + $contentEncoding = ContentEncoding::aes128gcm; $plaintext = 'When I grow up, I want to be a watermelon'; $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Base64UrlSafe::encodeUnpadded($plaintext)); @@ -68,7 +69,7 @@ public function testGetContentCodingHeader(): void $localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); $salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw'); - $result = Encryption::getContentCodingHeader($salt, $localPublicKey, "aes128gcm"); + $result = Encryption::getContentCodingHeader($salt, $localPublicKey, ContentEncoding::aes128gcm); $expected = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); $this->assertEquals(Utils::safeStrlen($expected), Utils::safeStrlen($result)); @@ -81,7 +82,7 @@ public function testGetContentCodingHeader(): void #[dataProvider('payloadProvider')] public function testPadPayload(string $payload, int $maxLengthToPad, int $expectedResLength): void { - $res = Encryption::padPayload($payload, $maxLengthToPad, "aesgcm"); + $res = Encryption::padPayload($payload, $maxLengthToPad, ContentEncoding::aesgcm); $this->assertStringContainsString('test', $res); $this->assertEquals($expectedResLength, Utils::safeStrlen($res)); diff --git a/tests/SubscriptionTest.php b/tests/SubscriptionTest.php index 3e6b9a2..2fe50e9 100644 --- a/tests/SubscriptionTest.php +++ b/tests/SubscriptionTest.php @@ -1,5 +1,6 @@ expectException(ValueError::class); $subscriptionArray = [ "endpoint" => "http://toto.com", ]; - $subscription = Subscription::create($subscriptionArray); - $this->assertEquals("http://toto.com", $subscription->getEndpoint()); - $this->assertEquals(null, $subscription->getPublicKey()); - $this->assertEquals(null, $subscription->getAuthToken()); - $this->assertEquals(null, $subscription->getContentEncoding()); + Subscription::create($subscriptionArray); } + /** + * Throw exception on outdated call. + */ public function testConstructMinimal(): void { - $subscription = new Subscription("http://toto.com"); - $this->assertEquals("http://toto.com", $subscription->getEndpoint()); - $this->assertEquals(null, $subscription->getPublicKey()); - $this->assertEquals(null, $subscription->getAuthToken()); - $this->assertEquals(null, $subscription->getContentEncoding()); + $this->expectException(ArgumentCountError::class); + new Subscription("http://toto.com"); + } + public function testExceptionEmpty(): void + { + $this->expectException(ValueError::class); + new Subscription("", "", ""); + } + public function testExceptionEmptyKey(): void + { + $this->expectException(ValueError::class); + $subscriptionArray = [ + "endpoint" => "http://toto.com", + "publicKey" => "", + "authToken" => "authToken", + ]; + Subscription::create($subscriptionArray); + } + public function testExceptionEmptyToken(): void + { + $this->expectException(ValueError::class); + $subscriptionArray = [ + "endpoint" => "http://toto.com", + "publicKey" => "publicKey", + "authToken" => "", + ]; + Subscription::create($subscriptionArray); } public function testCreatePartial(): void @@ -39,7 +65,7 @@ public function testCreatePartial(): void $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); - $this->assertEquals("aesgcm", $subscription->getContentEncoding()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); } public function testConstructPartial(): void @@ -48,11 +74,24 @@ public function testConstructPartial(): void $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); - $this->assertEquals("aesgcm", $subscription->getContentEncoding()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); } public function testCreateFull(): void { + $subscriptionArray = [ + "endpoint" => "http://toto.com", + "publicKey" => "publicKey", + "authToken" => "authToken", + "contentEncoding" => ContentEncoding::aes128gcm, + ]; + $subscription = Subscription::create($subscriptionArray); + $this->assertEquals("http://toto.com", $subscription->getEndpoint()); + $this->assertEquals("publicKey", $subscription->getPublicKey()); + $this->assertEquals("authToken", $subscription->getAuthToken()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); + + // Test with type string contentEncoding $subscriptionArray = [ "endpoint" => "http://toto.com", "publicKey" => "publicKey", @@ -63,18 +102,24 @@ public function testCreateFull(): void $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); - $this->assertEquals("aes128gcm", $subscription->getContentEncoding()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); } public function testConstructFull(): void { - $subscription = new Subscription("http://toto.com", "publicKey", "authToken", "aes128gcm"); + $subscription = new Subscription("http://toto.com", "publicKey", "authToken", ContentEncoding::aes128gcm); $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); - $this->assertEquals("aes128gcm", $subscription->getContentEncoding()); - } + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); + // Test with type string contentEncoding + $subscription = new Subscription("http://toto.com", "publicKey", "authToken", "aesgcm"); + $this->assertEquals("http://toto.com", $subscription->getEndpoint()); + $this->assertEquals("publicKey", $subscription->getPublicKey()); + $this->assertEquals("authToken", $subscription->getAuthToken()); + $this->assertEquals(ContentEncoding::aesgcm, $subscription->getContentEncoding()); + } public function testCreatePartialWithNewStructure(): void { $subscription = Subscription::create([ @@ -87,6 +132,7 @@ public function testCreatePartialWithNewStructure(): void $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); } public function testCreatePartialWithNewStructureAndContentEncoding(): void @@ -102,6 +148,6 @@ public function testCreatePartialWithNewStructureAndContentEncoding(): void $this->assertEquals("http://toto.com", $subscription->getEndpoint()); $this->assertEquals("publicKey", $subscription->getPublicKey()); $this->assertEquals("authToken", $subscription->getAuthToken()); - $this->assertEquals("aes128gcm", $subscription->getContentEncoding()); + $this->assertEquals(ContentEncoding::aes128gcm, $subscription->getContentEncoding()); } } diff --git a/tests/VAPIDTest.php b/tests/VAPIDTest.php index 4d53444..40986f8 100644 --- a/tests/VAPIDTest.php +++ b/tests/VAPIDTest.php @@ -8,6 +8,7 @@ * file that was distributed with this source code. */ +use Minishlink\WebPush\ContentEncoding; use Minishlink\WebPush\Utils; use Minishlink\WebPush\VAPID; use PHPUnit\Framework\Attributes\DataProvider; @@ -27,7 +28,7 @@ public static function vapidProvider(): array 'publicKey' => 'BA6jvk34k6YjElHQ6S0oZwmrsqHdCNajxcod6KJnI77Dagikfb--O_kYXcR2eflRz6l3PcI2r8fPCH3BElLQHDk', 'privateKey' => '-3CdhFOqjzixgAbUSa0Zv9zi-dwDVmWO7672aBxSFPQ', ], - "aesgcm", + ContentEncoding::aesgcm, 1475452165, 'WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwOi8vcHVzaC5jb20iLCJleHAiOjE0NzU0NTIxNjUsInN1YiI6Imh0dHA6Ly90ZXN0LmNvbSJ9.4F3ZKjeru4P9XM20rHPNvGBcr9zxhz8_ViyNfe11_xcuy7A9y7KfEPt6yuNikyW7eT9zYYD5mQZubDGa-5H2cA', 'p256ecdsa=BA6jvk34k6YjElHQ6S0oZwmrsqHdCNajxcod6KJnI77Dagikfb--O_kYXcR2eflRz6l3PcI2r8fPCH3BElLQHDk', @@ -38,7 +39,7 @@ public static function vapidProvider(): array 'publicKey' => 'BA6jvk34k6YjElHQ6S0oZwmrsqHdCNajxcod6KJnI77Dagikfb--O_kYXcR2eflRz6l3PcI2r8fPCH3BElLQHDk', 'privateKey' => '-3CdhFOqjzixgAbUSa0Zv9zi-dwDVmWO7672aBxSFPQ', ], - "aes128gcm", + ContentEncoding::aes128gcm, 1475452165, 'vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwOi8vcHVzaC5jb20iLCJleHAiOjE0NzU0NTIxNjUsInN1YiI6Imh0dHA6Ly90ZXN0LmNvbSJ9.4F3ZKjeru4P9XM20rHPNvGBcr9zxhz8_ViyNfe11_xcuy7A9y7KfEPt6yuNikyW7eT9zYYD5mQZubDGa-5H2cA, k=BA6jvk34k6YjElHQ6S0oZwmrsqHdCNajxcod6KJnI77Dagikfb--O_kYXcR2eflRz6l3PcI2r8fPCH3BElLQHDk', null, @@ -50,7 +51,7 @@ public static function vapidProvider(): array * @throws ErrorException */ #[dataProvider('vapidProvider')] - public function testGetVapidHeaders(string $audience, array $vapid, string $contentEncoding, int $expiration, string $expectedAuthorization, ?string $expectedCryptoKey): void + public function testGetVapidHeaders(string $audience, array $vapid, ContentEncoding $contentEncoding, int $expiration, string $expectedAuthorization, ?string $expectedCryptoKey): void { $vapid = VAPID::validate($vapid); $headers = VAPID::getVapidHeaders( @@ -74,10 +75,6 @@ public function testGetVapidHeaders(string $audience, array $vapid, string $cont } } - /** - * @param string $auth - * @return array - */ private function explodeAuthorization(string $auth): array { $auth = explode('.', $auth); diff --git a/tests/WebPushTest.php b/tests/WebPushTest.php index ef3d1d8..795839b 100644 --- a/tests/WebPushTest.php +++ b/tests/WebPushTest.php @@ -12,10 +12,12 @@ use Minishlink\WebPush\SubscriptionInterface; use Minishlink\WebPush\WebPush; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; /** * @covers \Minishlink\WebPush\WebPush */ +#[group('online')] final class WebPushTest extends PHPUnit\Framework\TestCase { private static array $endpoints; @@ -122,23 +124,15 @@ private static function setCiEnvironment(): void self::$keys['standard'] = $keys->{'p256dh'}; } - /** - * @throws ErrorException - */ public static function notificationProvider(): array { self::setUpBeforeClass(); // dirty hack of PHPUnit limitation return [ - [new Subscription(self::$endpoints['standard'] ?: '', self::$keys['standard'] ?: '', self::$tokens['standard'] ?: ''), '{"message":"Comment ça va ?","tag":"general"}'], + [new Subscription(self::$endpoints['standard'] ?: 'endpoint', self::$keys['standard'] ?: 'publicKey', self::$tokens['standard'] ?: 'authToken'), '{"message":"Comment ça va ?","tag":"general"}'], ]; } - /** - * @param SubscriptionInterface $subscription - * @param string $payload - * @throws ErrorException - */ #[dataProvider('notificationProvider')] public function testSendOneNotification(SubscriptionInterface $subscription, string $payload): void { @@ -146,9 +140,6 @@ public function testSendOneNotification(SubscriptionInterface $subscription, str $this->assertTrue($report->isSuccess()); } - /** - * @throws ErrorException - */ public function testSendNotificationBatch(): void { $batchSize = 10; @@ -168,29 +159,21 @@ public function testSendNotificationBatch(): void } } - /** - * @throws ErrorException - */ - public function testSendOneNotificationWithTooBigPayload(): void + #[dataProvider('notificationProvider')] + public function testSendOneNotificationWithTooBigPayload(SubscriptionInterface $subscription): void { $this->expectException(ErrorException::class); $this->expectExceptionMessage('Size of payload must not be greater than 4078 octets.'); - $subscription = new Subscription(self::$endpoints['standard'], self::$keys['standard']); $this->webPush->sendOneNotification( $subscription, str_repeat('test', 1020) ); } - /** - * @throws \ErrorException - * @throws \JsonException - */ - public function testFlush(): void + #[dataProvider('notificationProvider')] + public function testFlush(SubscriptionInterface $subscription): void { - $subscription = new Subscription(self::$endpoints['standard']); - $report = $this->webPush->sendOneNotification($subscription); $this->assertFalse($report->isSuccess()); // it doesn't have VAPID @@ -227,13 +210,9 @@ public function testFlushEmpty(): void $this->assertEmpty(iterator_to_array($this->webPush->flush(300))); } - /** - * @throws ErrorException - */ - public function testCount(): void + #[dataProvider('notificationProvider')] + public function testCount(SubscriptionInterface $subscription): void { - $subscription = new Subscription(self::$endpoints['standard']); - $this->webPush->queueNotification($subscription); $this->webPush->queueNotification($subscription); $this->webPush->queueNotification($subscription);