diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 419ba79..cda4795 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,8 +23,16 @@ jobs: uses: php-actions/phpunit@v4 with: php_version: ${{matrix.php-version}} - php_extensions: json zip xdebug + php_extensions: json zlib xdebug configuration: phpunit.xml.dist coverage_text: true + coverage_clover: coverage.xml env: - XDEBUG_MODE: coverage \ No newline at end of file + XDEBUG_MODE: coverage + + - name: Enforce coverage threshold + if: matrix.php-version == '8.2' + uses: ericsizemore/phpunit-coverage-check-action@2.0.0 + with: + clover_file: 'coverage.xml' + threshold: '100' diff --git a/composer.json b/composer.json index 00d77d2..2df497e 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "@analyse" ], "test": "vendor/bin/phpunit --testdox", - "test-coverage": "@test --coverage-text coverage" + "test-coverage": "@test --coverage-text" }, "autoload": { "psr-4": { diff --git a/tests/Feature/SendNotificationsTest.php b/tests/Feature/SendNotificationsTest.php index 90642b0..1e69547 100644 --- a/tests/Feature/SendNotificationsTest.php +++ b/tests/Feature/SendNotificationsTest.php @@ -355,6 +355,38 @@ public function send_notifications_exposes_push_errors_for_each_request_error(): $this->assertEquals(PushErrorCode::PushTooManyExperienceIds, $result->errors->get(0)->code); } + #[Test] + public function send_notifications_exposes_unauthorized_push_error_for_invalid_credentials(): void + { + $this->mockClient->addResponse( + MockResponse::make( + body: [ + 'errors' => [ + [ + 'code' => 'UNAUTHORIZED', + 'message' => 'Invalid credentials', + ], + ], + ], + status: 401, + headers: ['Content-Type' => 'application/json'], + ), + ); + + $messages = new PushMessageCollection( + new PushMessage(to: new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'), title: 'Test Notification'), + ); + + $result = $this->service->sendNotifications($messages); + + $this->mockClient->assertSentCount(1, SendNotificationsRequest::class); + + $this->assertTrue($result->hasErrors()); + $this->assertCount(1, $result->errors); + $this->assertEquals(PushErrorCode::Unauthorized, $result->errors->get(0)->code); + $this->assertCount(0, $result->tickets); + } + // Helpers ---- protected function generatePushMessage(): PushMessage diff --git a/tests/Unit/PushMessage/PushMessageTest.php b/tests/Unit/PushMessage/PushMessageTest.php index 116ecb8..b508b2c 100644 --- a/tests/Unit/PushMessage/PushMessageTest.php +++ b/tests/Unit/PushMessage/PushMessageTest.php @@ -8,6 +8,7 @@ use Dru1x\ExpoPush\PushMessage\RichContent; use Dru1x\ExpoPush\PushToken\PushToken; use Dru1x\ExpoPush\PushToken\PushTokenCollection; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use stdClass; @@ -210,6 +211,81 @@ public function from_array_with_dictionary_returns_instance(): void $this->assertSame('test-tag', $message->tag); } + #[Test] + public function from_array_throws_exception_when_to_is_missing(): void + { + $this->expectException(InvalidArgumentException::class); + + PushMessage::fromArray([ + 'title' => 'Test Notification', + ]); + } + + #[Test] + public function from_array_throws_exception_when_to_is_not_array_or_string(): void + { + $this->expectException(InvalidArgumentException::class); + + PushMessage::fromArray([ + 'to' => 123, + ]); + } + + #[Test] + public function from_array_throws_exception_when_rich_content_is_not_array(): void + { + $this->expectException(TypeError::class); + + PushMessage::fromArray([ + 'to' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', + 'richContent' => 'not-an-array', + ]); + } + + #[Test] + public function from_array_converts_priority_to_enum(): void + { + $message = PushMessage::fromArray([ + 'to' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', + 'priority' => 'high', + ]); + + $this->assertSame(Priority::High, $message->priority); + } + + #[Test] + public function from_array_throws_exception_when_priority_is_not_valid(): void + { + $this->expectException(TypeError::class); + + PushMessage::fromArray([ + 'to' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', + 'priority' => [], + ]); + } + + #[Test] + public function from_array_converts_interruption_level_to_enum(): void + { + $message = PushMessage::fromArray([ + 'to' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', + 'interruptionLevel' => 'critical', + ]); + + $this->assertSame(InterruptionLevel::Critical, $message->interruptionLevel); + } + + #[Test] + public function from_array_throws_exception_when_interruption_level_is_not_valid(): void + { + $this->expectException(TypeError::class); + + PushMessage::fromArray([ + 'to' => 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]', + 'interruptionLevel' => [], + ]); + } + #[Test] public function from_json_returns_instance(): void { diff --git a/tests/Unit/PushTicket/SuccessfulPushReceiptTest.php b/tests/Unit/PushReceipt/SuccessfulPushReceiptTest.php similarity index 96% rename from tests/Unit/PushTicket/SuccessfulPushReceiptTest.php rename to tests/Unit/PushReceipt/SuccessfulPushReceiptTest.php index 915b13a..96fec69 100644 --- a/tests/Unit/PushTicket/SuccessfulPushReceiptTest.php +++ b/tests/Unit/PushReceipt/SuccessfulPushReceiptTest.php @@ -1,6 +1,6 @@ assertCount(3, $collection); } + #[Test] + public function shift_with_negative_count_throws_exception(): void + { + $collection = new PushTokenCollection( + new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'), + ); + + $this->expectException(InvalidArgumentException::class); + + $collection->shift(-1); + } + + #[Test] + public function shift_with_zero_count_returns_empty_collection_without_mutating_source(): void + { + $token1 = new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'); + $token2 = new PushToken('ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'); + $token3 = new PushToken('ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]'); + + $collection = new PushTokenCollection($token1, $token2, $token3); + + $shifted = $collection->shift(0); + + $this->assertTrue($shifted->isEmpty()); + + $this->assertCount(3, $collection); + $this->assertEquals($token1, $collection->get(0)); + $this->assertEquals($token2, $collection->get(1)); + $this->assertEquals($token3, $collection->get(2)); + } + + #[Test] + public function shift_from_empty_collection_returns_empty_collection(): void + { + $collection = new PushTokenCollection(); + + $shifted = $collection->shift(); + + $this->assertTrue($shifted->isEmpty()); + } + + #[Test] + public function shift_with_count_greater_than_available_returns_all_items(): void + { + $token1 = new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'); + $token2 = new PushToken('ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'); + + $collection = new PushTokenCollection($token1, $token2); + + $shifted = $collection->shift(5); + + $this->assertCount(2, $shifted); + $this->assertEquals($token1, $shifted->get(0)); + $this->assertEquals($token2, $shifted->get(1)); + + $this->assertTrue($collection->isEmpty()); + } + + #[Test] + public function shift_removes_and_returns_leading_items_leaving_remainder_in_source(): void + { + $token1 = new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'); + $token2 = new PushToken('ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'); + $token3 = new PushToken('ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]'); + $token4 = new PushToken('ExponentPushToken[aaaaaaaaaaaaaaaaaaaaaa]'); + + $collection = new PushTokenCollection($token1, $token2, $token3, $token4); + + $shifted = $collection->shift(2); + + $this->assertCount(2, $shifted); + $this->assertEquals($token1, $shifted->get(0)); + $this->assertEquals($token2, $shifted->get(1)); + + $this->assertCount(2, $collection); + $this->assertEquals($token3, $collection->get(0)); + $this->assertEquals($token4, $collection->get(1)); + } + + #[Test] + public function shift_with_default_count_removes_single_item(): void + { + $token1 = new PushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'); + $token2 = new PushToken('ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'); + + $collection = new PushTokenCollection($token1, $token2); + + $shifted = $collection->shift(); + + $this->assertCount(1, $shifted); + $this->assertEquals($token1, $shifted->get(0)); + + $this->assertCount(1, $collection); + $this->assertEquals($token2, $collection->get(0)); + } + #[Test] public function chunk_returns_correctly_sized_chunks(): void { diff --git a/tests/Unit/Request/GetReceiptsRequestTest.php b/tests/Unit/Request/GetReceiptsRequestTest.php index cbe5cb0..9f4a9bf 100644 --- a/tests/Unit/Request/GetReceiptsRequestTest.php +++ b/tests/Unit/Request/GetReceiptsRequestTest.php @@ -10,6 +10,7 @@ use Dru1x\ExpoPush\PushReceipt\PushReceiptIdCollection; use Dru1x\ExpoPush\Request\GetReceiptsRequest; use OverflowException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Saloon\Config; @@ -167,6 +168,52 @@ public function create_dto_from_response_maps_unrecognized_error_code_to_unknown $this->assertEquals(PushReceiptErrorCode::Unknown, $receipt->details->error); } + #[Test] + #[DataProvider('knownErrorCodeProvider')] + public function create_dto_from_response_maps_known_error_code(string $expoErrorString, PushReceiptErrorCode $expectedCode): void + { + $request = new GetReceiptsRequest( + new PushReceiptIdCollection(), + ); + + $this->mockClient->addResponses([ + GetReceiptsRequest::class => MockResponse::make( + body: [ + 'data' => [ + 'ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ' => [ + 'status' => 'error', + 'message' => '"ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]" is not a registered push notification recipient', + 'details' => [ + 'error' => $expoErrorString, + 'expoPushToken' => 'ExponentPushToken[zzzzzzzzzzzzzzzzzzzzzz]', + ], + ], + ], + ], + headers: ['Content-Type' => 'application/json'], + ), + ]); + + $dto = $request->createDtoFromResponse( + $this->connector->send($request), + ); + + $receipt = $dto->get(0); + $this->assertInstanceOf(FailedPushReceipt::class, $receipt); + $this->assertEquals($expectedCode, $receipt->details->error); + } + + public static function knownErrorCodeProvider(): array + { + return [ + 'DeviceNotRegistered' => ['DeviceNotRegistered', PushReceiptErrorCode::DeviceNotRegistered], + 'InvalidCredentials' => ['InvalidCredentials', PushReceiptErrorCode::InvalidCredentials], + 'MessageRateExceeded' => ['MessageRateExceeded', PushReceiptErrorCode::MessageRateExceeded], + 'MessageTooBig' => ['MessageTooBig', PushReceiptErrorCode::MessageTooBig], + 'MismatchSenderId' => ['MismatchSenderId', PushReceiptErrorCode::MismatchSenderId], + ]; + } + #[Test] public function create_dto_from_response_throws_exception_when_body_cannot_be_decoded(): void { diff --git a/tests/Unit/Support/CollectionTest.php b/tests/Unit/Support/CollectionTest.php index cc7c4e5..deb3a4e 100644 --- a/tests/Unit/Support/CollectionTest.php +++ b/tests/Unit/Support/CollectionTest.php @@ -42,6 +42,16 @@ public function can_check_if_a_collection_contains_a_given_value(mixed $item, bo ); } + #[Test] + #[DataProvider('doesntContainProvider')] + public function can_check_if_a_collection_doesnt_contain_a_given_value(mixed $item, bool $expected): void + { + $this->assertSame( + $expected, + Collection::make(1, 2)->doesntContain($item), + ); + } + #[Test] #[DataProvider('getProvider')] public function can_retrieve_a_collection_value_by_its_key(int|string $key, mixed $value): void @@ -226,6 +236,15 @@ public static function containsProvider(): array ]; } + public static function doesntContainProvider(): array + { + return [ + 'does' => [2, false], + 'doesnt' => [3, true], + 'doesnt loose' => ['2', true], + ]; + } + public static function getProvider(): array { return [