feat: paginate RepositoryService.getTeams using PaginatedRequest#36
feat: paginate RepositoryService.getTeams using PaginatedRequest#36Hayena wants to merge 2 commits into
Conversation
Hayena
left a comment
There was a problem hiding this comment.
Kotlin Code Review
🟡 Improvement (missed idioms / non-idiomatic patterns)
Two minor improvements noted below as inline comments — see specific lines for details.
🟢 Looks Good
The core refactor is clean and correct. Delegating to PaginatedRequest via the companion invoke(uri) is the right pattern, consistent with how getAllContributors uses the full constructor. Error behaviour is preserved (non-200 → GitHubApiException), and the pagination tests are well-structured and meaningful.
| every { httpClient.invoke(matchUri("/repos/${repo.owner}/${repo.name}/teams?page=2&per_page=100")) } | ||
| .returns(Response(Status.OK).body("[$secondTeam]")) | ||
|
|
||
| val result = client.repositories.getTeams(repo) |
There was a problem hiding this comment.
The multi-page test duplicates what a Defaults.repositoryTeam() helper could centralise. Defaults already provides contributor() for its equivalent test, and a repositoryTeam() factory (analogous to Defaults.team() for GitHubTeam) would:
- Cut the 60-line inline JSON + full constructor assertion down to a couple of named calls.
- Keep fixture data in one place — if a field is added to
GitHubRepositoryTeamthe helper breaks the build instead of silently passing with stale JSON.
GetTeamResponse is internal, so raw JSON for the mock response body is unavoidable, but the expected GitHubRepositoryTeam values in the assertion can be expressed through a helper:
// Defaults.kt
fun repositoryTeam(
organization: String = OWNER,
id: Int = 1,
name: String = "Justice League",
slug: String = "justice-league",
// …sensible defaults for other fields
) = GitHubRepositoryTeam(organization = organization, id = id, name = name, slug = slug, …)Then the assertion becomes:
result.getOrThrow() shouldBe listOf(
Defaults.repositoryTeam(id = 1, name = "Justice League", …),
Defaults.repositoryTeam(id = 2, name = "Avengers", …),
)| result.getOrThrow() shouldBe emptyList() | ||
| } | ||
|
|
||
| "follow Link rel=next headers across multiple pages" { |
There was a problem hiding this comment.
The test suite for getTeams covers empty list, multi-page, and error — but is missing a single non-empty page with no Link header. The getAllContributors suite has this as its first case: "return Found with contributors when a single page is returned".
Without it, the scenario where defaultPageResult produces hasNext = false from the very first request (because no Link: rel="next" header is present) is not exercised as a standalone path. The multi-page test happens to cover page-2 termination, but a dedicated case like:
"return Found with teams when a single page is returned" {
every { httpClient.invoke(matchUri(".../teams?page=1&per_page=100")) }
.returns(Response(Status.OK).body("[$firstTeam]")) // no Link header
val result = client.repositories.getTeams(repo)
result.shouldBeInstanceOf<ApiResult.Found<List<GitHubRepositoryTeam>>>()
result.getOrThrow() shouldHaveSize 1
}would make the intent explicit and guard against regressions in the hasNext logic.
Summary
Implements #19
RepositoryService.getTeams()previously used a plainGETand deserialised only the first page, silently truncating repositories with many teams. This PR replaces it with aPaginatedRequest-based implementation, consistent with all other paginated list endpoints in the library.Changes
GitHubRepositoryClient.getTeams()replaced with aPaginatedRequest-based implementationLinkheader pagination