diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3ae9fea..5c71aca 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,6 +25,9 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} + extensions: mbstring, + coverage: xdebug, pcov + tools: composer:v2 - name: Set composer cache directory id: composer-cache @@ -40,5 +43,11 @@ jobs: - name: Install composer dependencies run: composer install --no-interaction --prefer-dist --no-progress - - name: Run phpunit unit tests - run: vendor/bin/phpunit --testsuite=unit --colors=always --testdox \ No newline at end of file + - name: Run phpunit unit tests with coverage + run: vendor/bin/phpunit --testsuite=unit --colors=always --testdox --coverage-clover=coverage.xml + + - name: Coverage Check + uses: ericsizemore/phpunit-coverage-check-action@2.0.0 + with: + clover_file: 'coverage.xml' + threshold: 75 \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2cd4d0f..02d4887 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,7 +18,7 @@ - src + src/Client diff --git a/tests/Unit/Client/List/PaginatedAuthorProjectListTest.php b/tests/Unit/Client/List/PaginatedAuthorProjectListTest.php new file mode 100644 index 0000000..a6bdf48 --- /dev/null +++ b/tests/Unit/Client/List/PaginatedAuthorProjectListTest.php @@ -0,0 +1,134 @@ + $id, + 'author' => new ResourceAuthor(['id' => $authorId, 'username' => 'author-' . $authorId]), + ]); + } + + public function testConstructionTransformsResourcesToProjects(): void + { + $resources = [ + $this->createResource(1), + $this->createResource(2), + $this->createResource(3), + ]; + + $list = new PaginatedAuthorProjectList($this->apiClient, 123, 1, $resources); + + $this->assertCount(3, $list); + $this->assertSame(3, $list->getHits()); + $this->assertInstanceOf(Project::class, $list[0]); + $this->assertInstanceOf(Project::class, $list[1]); + $this->assertInstanceOf(Project::class, $list[2]); + } + + public function testArrayAccessAndIteration(): void + { + $resources = [ + $this->createResource(10), + $this->createResource(11), + ]; + + $list = new PaginatedAuthorProjectList($this->apiClient, 5, 2, $resources); + + // ArrayAccess + $this->assertTrue(isset($list[0])); + $this->assertTrue(isset($list[1])); + $this->assertFalse(isset($list[2])); + + // Iteration + $collected = []; + foreach ($list as $project) { + $collected[] = $project; + } + $this->assertCount(2, $collected); + $this->assertEquals($list[0], $collected[0]); + $this->assertEquals($list[1], $collected[1]); + + // Rewind + key/valid + $list->rewind(); + $this->assertTrue($list->valid()); + $this->assertSame(0, $list->key()); + $list->next(); + $this->assertSame(1, $list->key()); + } + + public function testGetNextPageUsesClient(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + $authorId = 55; + + $page1 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 1, [ + $this->createResource(1, $authorId) + ]); + $page2 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 2, [ + $this->createResource(2, $authorId) + ]); + + $mockedApiClient->expects($this->once()) + ->method('getAuthorProjects') + ->with($authorId, 2) + ->willReturn($page2); + + $next = $page1->getNextPage(); + $this->assertSame($page2, $next); + } + + public function testGetPreviousPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + $authorId = 77; + + $page1 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 1, [$this->createResource(1, $authorId)]); + $this->assertFalse($page1->hasPreviousPage()); + $this->assertNull($page1->getPreviousPage()); + + $page2 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 2, [$this->createResource(2, $authorId)]); + + $mockedApiClient->expects($this->once()) + ->method('getAuthorProjects') + ->with($authorId, 1) + ->willReturn($page1); + + $previous = $page2->getPreviousPage(); + $this->assertSame($page1, $previous); + } + + public function testGetResultsFromFollowingPagesAggregatesUntilEmptyPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + $authorId = 999; + + $page1 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 1, [ + $this->createResource(10, $authorId), + ]); + $page2 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 2, [ + $this->createResource(20, $authorId), + $this->createResource(21, $authorId), + ]); + // Empty page (hits = 0) stops the aggregation loop + $page3 = new PaginatedAuthorProjectList($mockedApiClient, $authorId, 3, []); + + $mockedApiClient->expects($this->exactly(2)) + ->method('getAuthorProjects') + ->willReturnOnConsecutiveCalls($page2, $page3); + + $all = $page1->getResultsFromFollowingPages(); + $this->assertCount(3, $all); // 1 from page1 + 2 from page2 + $this->assertContainsOnlyInstancesOf(Project::class, $all); + } +} \ No newline at end of file diff --git a/tests/Unit/Client/List/PaginatedCategoryProjectListTest.php b/tests/Unit/Client/List/PaginatedCategoryProjectListTest.php new file mode 100644 index 0000000..2010f20 --- /dev/null +++ b/tests/Unit/Client/List/PaginatedCategoryProjectListTest.php @@ -0,0 +1,136 @@ + $id, + 'category' => + new ResourceCategory([ + 'id' => $category->value, + 'title' => $category->name + ]) + ]); + } + + public function testConstructionTransformsResourcesToProjects(): void + { + $resources = [ + $this->createResource(1), + $this->createResource(2), + $this->createResource(3), + ]; + + $list = new PaginatedCategoryProjectList($this->apiClient, Category::SPIGOT, 1, $resources); + + $this->assertCount(3, $list); + $this->assertSame(3, $list->getHits()); + $this->assertInstanceOf(Project::class, $list[0]); + $this->assertInstanceOf(Project::class, $list[1]); + $this->assertInstanceOf(Project::class, $list[2]); + } + + public function testArrayAccessAndIteration(): void + { + $resources = [ + $this->createResource(10), + $this->createResource(11), + ]; + + $list = new PaginatedCategoryProjectList($this->apiClient, Category::SPIGOT, 2, $resources); + + // ArrayAccess + $this->assertTrue(isset($list[0])); + $this->assertTrue(isset($list[1])); + $this->assertFalse(isset($list[2])); + + // Iteration + $collected = []; + foreach ($list as $project) { + $collected[] = $project; + } + $this->assertCount(2, $collected); + $this->assertEquals($list[0], $collected[0]); + $this->assertEquals($list[1], $collected[1]); + + // Rewind + key/valid + $list->rewind(); + $this->assertTrue($list->valid()); + $this->assertSame(0, $list->key()); + $list->next(); + $this->assertSame(1, $list->key()); + } + + public function testGetNextPageUsesClient(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 1, [ + $this->createResource(1, Category::SPIGOT) + ]); + $page2 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 2, [ + $this->createResource(2, Category::SPIGOT) + ]); + + $mockedApiClient->expects($this->once()) + ->method('listProjectsForCategory') + ->with(Category::SPIGOT, 2) + ->willReturn($page2); + + $next = $page1->getNextPage(); + $this->assertSame($page2, $next); + } + + public function testGetPreviousPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 1, [$this->createResource(1, Category::SPIGOT)]); + $this->assertFalse($page1->hasPreviousPage()); + $this->assertNull($page1->getPreviousPage()); + + $page2 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 2, [$this->createResource(2, Category::SPIGOT)]); + + $mockedApiClient->expects($this->once()) + ->method('listProjectsForCategory') + ->with(Category::SPIGOT, 1) + ->willReturn($page1); + + $previous = $page2->getPreviousPage(); + $this->assertSame($page1, $previous); + } + + public function testGetResultsFromFollowingPagesAggregatesUntilEmptyPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 1, [ + $this->createResource(10, Category::SPIGOT), + ]); + $page2 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 2, [ + $this->createResource(20, Category::SPIGOT), + $this->createResource(21, Category::SPIGOT), + ]); + // Empty page (hits = 0) stops the aggregation loop + $page3 = new PaginatedCategoryProjectList($mockedApiClient, Category::SPIGOT, 3, []); + + $mockedApiClient->expects($this->exactly(2)) + ->method('listProjectsForCategory') + ->willReturnOnConsecutiveCalls($page2, $page3); + + $all = $page1->getResultsFromFollowingPages(); + $this->assertCount(3, $all); // 1 from page1 + 2 from page2 + $this->assertContainsOnlyInstancesOf(Project::class, $all); + } +} \ No newline at end of file diff --git a/tests/Unit/Client/List/PaginatedProjectListTest.php b/tests/Unit/Client/List/PaginatedProjectListTest.php new file mode 100644 index 0000000..9a6ae70 --- /dev/null +++ b/tests/Unit/Client/List/PaginatedProjectListTest.php @@ -0,0 +1,116 @@ + 1]), + new Resource(['id' => 2]), + new Resource(['id' => 3]), + ]; + + $list = new PaginatedProjectList($this->apiClient, 1, $resources); + + $this->assertCount(3, $list); + $this->assertSame(3, $list->getHits()); + $this->assertInstanceOf(Project::class, $list[0]); + $this->assertInstanceOf(Project::class, $list[1]); + $this->assertInstanceOf(Project::class, $list[2]); + } + + public function testArrayAccessAndIteration(): void + { + $resources = [ + new Resource(['id' => 10]), + new Resource(['id' => 11]), + ]; + + $list = new PaginatedProjectList($this->apiClient, 2, $resources); + + // ArrayAccess + $this->assertTrue(isset($list[0])); + $this->assertTrue(isset($list[1])); + $this->assertFalse(isset($list[2])); + + // Iteration + $collected = []; + foreach ($list as $project) { + $collected[] = $project; + } + $this->assertCount(2, $collected); + $this->assertEquals($list[0], $collected[0]); + $this->assertEquals($list[1], $collected[1]); + + // Rewind + key/valid + $list->rewind(); + $this->assertTrue($list->valid()); + $this->assertSame(0, $list->key()); + $list->next(); + $this->assertSame(1, $list->key()); + } + + public function testGetNextPageUsesClient(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedProjectList($mockedApiClient, 1, [new Resource(['id' => 1])]); + $page2 = new PaginatedProjectList($mockedApiClient, 2, [new Resource(['id' => 2])]); + + $mockedApiClient->expects($this->once()) + ->method('listProjects') + ->with(2) + ->willReturn($page2); + + $next = $page1->getNextPage(); + $this->assertSame($page2, $next); + } + + public function testGetPreviousPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedProjectList($mockedApiClient, 1, [new Resource(['id' => 1])]); + $this->assertFalse($page1->hasPreviousPage()); + $this->assertNull($page1->getPreviousPage()); + + $page2 = new PaginatedProjectList($mockedApiClient, 2, [new Resource(['id' => 2])]); + + $mockedApiClient->expects($this->once()) + ->method('listProjects') + ->with(1) + ->willReturn($page1); + + $previous = $page2->getPreviousPage(); + $this->assertSame($page1, $previous); + } + + public function testGetResultsFromFollowingPagesAggregatesUntilEmptyPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedProjectList($mockedApiClient, 1, [new Resource(['id' => 10])]); + $page2 = new PaginatedProjectList($mockedApiClient, 2, [ + new Resource(['id' => 20]), + new Resource(['id' => 21]), + ]); + // Empty page (hits = 0) stops the aggregation loop + $page3 = new PaginatedProjectList($mockedApiClient, 3, []); + + $mockedApiClient->expects($this->exactly(2)) + ->method('listProjects') + ->willReturnOnConsecutiveCalls($page2, $page3); + + $all = $page1->getResultsFromFollowingPages(); + $this->assertCount(3, $all); // 1 from page1 + 2 from page2 + $this->assertContainsOnlyInstancesOf(Project::class, $all); + } +} \ No newline at end of file diff --git a/tests/Unit/Client/List/PaginatedVersionListTest.php b/tests/Unit/Client/List/PaginatedVersionListTest.php new file mode 100644 index 0000000..96a7f00 --- /dev/null +++ b/tests/Unit/Client/List/PaginatedVersionListTest.php @@ -0,0 +1,117 @@ + 1, 'resource_id' => 100]), + new ResourceUpdate(['id' => 2, 'resource_id' => 100]), + new ResourceUpdate(['id' => 3, 'resource_id' => 100]), + ]; + + $list = new PaginatedVersionList($this->apiClient, 100, 1, $resourceUpdates); + + $this->assertCount(3, $list); + $this->assertSame(3, $list->getHits()); + $this->assertInstanceOf(Version::class, $list[0]); + $this->assertInstanceOf(Version::class, $list[1]); + $this->assertInstanceOf(Version::class, $list[2]); + } + + public function testArrayAccessAndIteration(): void + { + $resourceUpdates = [ + new ResourceUpdate(['id' => 1, 'resource_id' => 100]), + new ResourceUpdate(['id' => 2, 'resource_id' => 100]), + ]; + + $list = new PaginatedVersionList($this->apiClient, 100, 2, $resourceUpdates); + + // ArrayAccess + $this->assertTrue(isset($list[0])); + $this->assertTrue(isset($list[1])); + $this->assertFalse(isset($list[2])); + + // Iteration + $collected = []; + foreach ($list as $version) { + $collected[] = $version; + } + $this->assertCount(2, $collected); + $this->assertEquals($list[0], $collected[0]); + $this->assertEquals($list[1], $collected[1]); + + // Rewind + key/valid + $list->rewind(); + $this->assertTrue($list->valid()); + $this->assertSame(0, $list->key()); + $list->next(); + $this->assertSame(1, $list->key()); + } + + public function testGetNextPageUsesClient(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedVersionList($mockedApiClient, 100, 1, [new ResourceUpdate(['id' => 1, 'resource_id' => 100])]); + $page2 = new PaginatedVersionList($mockedApiClient, 100, 2, [new ResourceUpdate(['id' => 2, 'resource_id' => 100])]); + + $mockedApiClient->expects($this->once()) + ->method('getProjectVersions') + ->with(100, 2) + ->willReturn($page2); + + $next = $page1->getNextPage(); + $this->assertSame($page2, $next); + } + + public function testGetPreviousPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedVersionList($mockedApiClient, 100, 1, [new ResourceUpdate(['id' => 1, 'resource_id' => 100])]); + $this->assertFalse($page1->hasPreviousPage()); + $this->assertNull($page1->getPreviousPage()); + + $page2 = new PaginatedVersionList($mockedApiClient, 100, 2, [new ResourceUpdate(['id' => 2, 'resource_id' => 100])]); + + $mockedApiClient->expects($this->once()) + ->method('getProjectVersions') + ->with(100, 1) + ->willReturn($page1); + + $previous = $page2->getPreviousPage(); + $this->assertSame($page1, $previous); + } + + public function testGetResultsFromFollowingPagesAggregatesUntilEmptyPage(): void + { + $mockedApiClient = $this->createMock(SpigotAPIClient::class); + + $page1 = new PaginatedVersionList($mockedApiClient, 100, 1, [new ResourceUpdate(['id' => 10, 'resource_id' => 100])]); + $page2 = new PaginatedVersionList($mockedApiClient, 100, 2, [ + new ResourceUpdate(['id' => 20, 'resource_id' => 100]), + new ResourceUpdate(['id' => 21, 'resource_id' => 100]), + ]); + // Empty page (hits = 0) stops the aggregation loop + $page3 = new PaginatedVersionList($mockedApiClient, 100, 3, []); + + $mockedApiClient->expects($this->exactly(2)) + ->method('getProjectVersions') + ->willReturnOnConsecutiveCalls($page2, $page3); + + $all = $page1->getResultsFromFollowingPages(); + $this->assertCount(3, $all); // 1 from page1 + 2 from page2 + $this->assertContainsOnlyInstancesOf(Version::class, $all); + } +} \ No newline at end of file