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