Skip to content

Commit 70dafc9

Browse files
Merge pull request #69 from leanix/feature/CID-3368/Paginate-getInstallations
CID-3368: Paginate getting installations & organisations
2 parents 0bd5100 + 394b64f commit 70dafc9

File tree

8 files changed

+159
-19
lines changed

8 files changed

+159
-19
lines changed

src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ interface GitHubClient {
2828
): GitHubAppResponse
2929

3030
@GetMapping("/api/v3/app/installations")
31-
fun getInstallations(@RequestHeader("Authorization") jwt: String): List<Installation>
31+
fun getInstallations(
32+
@RequestHeader("Authorization") jwt: String,
33+
@RequestParam("per_page", defaultValue = "30") perPage: Int,
34+
@RequestParam("page", defaultValue = "1") page: Int
35+
): List<Installation>
3236

3337
@GetMapping("/api/v3/app/installations/{installationId}")
3438
fun getInstallation(
@@ -43,7 +47,11 @@ interface GitHubClient {
4347
): InstallationTokenResponse
4448

4549
@GetMapping("/api/v3/organizations")
46-
fun getOrganizations(@RequestHeader("Authorization") token: String): List<Organization>
50+
fun getOrganizations(
51+
@RequestHeader("Authorization") jwt: String,
52+
@RequestParam("per_page", defaultValue = "30") perPage: Int,
53+
@RequestParam("since", defaultValue = "1") since: Int
54+
): List<Organization>
4755

4856
@GetMapping("/api/v3/orgs/{org}/repos")
4957
fun getRepositories(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.leanix.githubagent.services
2+
3+
import net.leanix.githubagent.client.GitHubClient
4+
import net.leanix.githubagent.dto.Installation
5+
import net.leanix.githubagent.dto.Organization
6+
import org.springframework.stereotype.Service
7+
8+
@Service
9+
class GitHubAPIService(
10+
private val gitHubClient: GitHubClient,
11+
) {
12+
13+
companion object {
14+
private const val PAGE_SIZE = 30 // Maximum allowed by GitHub API is 100
15+
}
16+
17+
fun getPaginatedInstallations(jwtToken: String): List<Installation> {
18+
val installations = mutableListOf<Installation>()
19+
var page = 1
20+
var currentInstallations: List<Installation>
21+
22+
do {
23+
currentInstallations = gitHubClient.getInstallations("Bearer $jwtToken", PAGE_SIZE, page)
24+
if (currentInstallations.isNotEmpty()) installations.addAll(currentInstallations) else break
25+
page++
26+
} while (currentInstallations.size == PAGE_SIZE)
27+
return installations
28+
}
29+
30+
fun getPaginatedOrganizations(installationToken: String): List<Organization> {
31+
val organizations = mutableListOf<Organization>()
32+
var since = 1
33+
var currentOrganizations: List<Organization>
34+
35+
do {
36+
currentOrganizations = gitHubClient.getOrganizations("Bearer $installationToken", PAGE_SIZE, since)
37+
if (currentOrganizations.isNotEmpty()) organizations.addAll(currentOrganizations) else break
38+
since = currentOrganizations.last().id
39+
} while (currentOrganizations.size == PAGE_SIZE)
40+
return organizations
41+
}
42+
}

src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import net.leanix.githubagent.client.GitHubClient
66
import net.leanix.githubagent.config.GitHubEnterpriseProperties
77
import net.leanix.githubagent.dto.Installation
88
import net.leanix.githubagent.exceptions.FailedToCreateJWTException
9+
import net.leanix.githubagent.exceptions.JwtTokenNotFound
910
import org.bouncycastle.jce.provider.BouncyCastleProvider
1011
import org.slf4j.LoggerFactory
1112
import org.springframework.core.io.ResourceLoader
@@ -26,7 +27,8 @@ class GitHubAuthenticationService(
2627
private val githubEnterpriseProperties: GitHubEnterpriseProperties,
2728
private val resourceLoader: ResourceLoader,
2829
private val gitHubEnterpriseService: GitHubEnterpriseService,
29-
private val gitHubClient: GitHubClient
30+
private val gitHubClient: GitHubClient,
31+
private val gitHubAPIService: GitHubAPIService,
3032
) {
3133

3234
companion object {
@@ -38,11 +40,9 @@ class GitHubAuthenticationService(
3840

3941
fun refreshTokens() {
4042
generateAndCacheJwtToken()
41-
val jwtToken = cachingService.get("jwtToken")
42-
generateAndCacheInstallationTokens(
43-
gitHubClient.getInstallations("Bearer $jwtToken"),
44-
jwtToken.toString()
45-
)
43+
val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound()
44+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken.toString())
45+
generateAndCacheInstallationTokens(installations, jwtToken.toString())
4646
}
4747

4848
fun generateAndCacheJwtToken() {

src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class GitHubScanningService(
2929
private val syncLogService: SyncLogService,
3030
private val rateLimitHandler: RateLimitHandler,
3131
private val gitHubEnterpriseService: GitHubEnterpriseService,
32+
private val gitHubAPIService: GitHubAPIService,
3233
) {
3334

3435
private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java)
@@ -67,7 +68,7 @@ class GitHubScanningService(
6768
}
6869

6970
private fun getInstallations(jwtToken: String): List<Installation> {
70-
val installations = gitHubClient.getInstallations("Bearer $jwtToken")
71+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken)
7172
gitHubAuthenticationService.generateAndCacheInstallationTokens(installations, jwtToken)
7273
return installations
7374
}
@@ -81,7 +82,7 @@ class GitHubScanningService(
8182
return
8283
}
8384
val installationToken = cachingService.get("installationToken:${installations.first().id}")
84-
val organizations = gitHubClient.getOrganizations("Bearer $installationToken")
85+
val organizations = gitHubAPIService.getPaginatedOrganizations(installationToken.toString())
8586
.map { organization ->
8687
if (installations.find { it.account.login == organization.login } != null) {
8788
OrganizationDto(organization.id, organization.login, true)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package net.leanix.githubagent.services
2+
3+
import io.mockk.every
4+
import io.mockk.mockk
5+
import net.leanix.githubagent.client.GitHubClient
6+
import net.leanix.githubagent.dto.Account
7+
import net.leanix.githubagent.dto.Installation
8+
import net.leanix.githubagent.dto.Organization
9+
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Test
11+
12+
class GitHubAPIServiceTest {
13+
14+
private val gitHubClient = mockk<GitHubClient>()
15+
private val gitHubAPIService = GitHubAPIService(gitHubClient)
16+
17+
private val permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read")
18+
private val events = listOf("label", "public", "repository", "push")
19+
20+
@Test
21+
fun `test getPaginatedInstallations with one page`() {
22+
val jwtToken = "test-jwt-token"
23+
val installationsPage1 = listOf(
24+
Installation(1, Account("test-account"), permissions, events),
25+
Installation(2, Account("test-account"), permissions, events)
26+
)
27+
28+
every { gitHubClient.getInstallations(any(), any(), any()) } returns installationsPage1
29+
30+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken)
31+
assertEquals(2, installations.size)
32+
assertEquals(installationsPage1, installations)
33+
}
34+
35+
@Test
36+
fun `test getPaginatedInstallations with multiple pages`() {
37+
val jwtToken = "test-jwt-token"
38+
val perPage = 30
39+
val totalInstallations = 100
40+
val installations = (1..totalInstallations).map {
41+
Installation(it.toLong(), Account("test-account-$it"), permissions, events)
42+
}
43+
val pages = installations.chunked(perPage)
44+
45+
every { gitHubClient.getInstallations(any(), any(), any()) } returnsMany pages + listOf(emptyList())
46+
47+
val result = gitHubAPIService.getPaginatedInstallations(jwtToken)
48+
assertEquals(totalInstallations, result.size)
49+
assertEquals(installations, result)
50+
}
51+
52+
@Test
53+
fun `test getPaginatedOrganizations with one page`() {
54+
val installationToken = "test-installation-token"
55+
val organizationsPage1 = listOf(
56+
Organization("org-1", 1),
57+
Organization("org-2", 2)
58+
)
59+
60+
every { gitHubClient.getOrganizations(any(), any(), any()) } returns organizationsPage1
61+
62+
val organizations = gitHubAPIService.getPaginatedOrganizations(installationToken)
63+
assertEquals(2, organizations.size)
64+
assertEquals(organizationsPage1, organizations)
65+
}
66+
67+
@Test
68+
fun `test getPaginatedOrganizations with multiple pages`() {
69+
val installationToken = "test-installation-token"
70+
val perPage = 30
71+
val totalOrganizations = 100
72+
val organizations = (1..totalOrganizations).map { Organization("org-$it", it) }
73+
val pages = organizations.chunked(perPage)
74+
75+
every { gitHubClient.getOrganizations(any(), any(), any()) } returnsMany pages + listOf(emptyList())
76+
77+
val result = gitHubAPIService.getPaginatedOrganizations(installationToken)
78+
assertEquals(totalOrganizations, result.size)
79+
assertEquals(organizations, result)
80+
}
81+
}

src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ class GitHubAuthenticationServiceTest {
2121
private val gitHubEnterpriseService = mockk<GitHubEnterpriseService>()
2222
private val gitHubClient = mockk<GitHubClient>()
2323
private val syncLogService = mockk<SyncLogService>()
24+
private val gitHubAPIService = mockk<GitHubAPIService>()
2425
private val githubAuthenticationService = GitHubAuthenticationService(
2526
cachingService,
2627
githubEnterpriseProperties,
2728
resourceLoader,
2829
gitHubEnterpriseService,
29-
gitHubClient
30+
gitHubClient,
31+
gitHubAPIService
3032
)
3133

3234
@BeforeEach

src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class GitHubScanningServiceTest {
3535
private val gitHubAuthenticationService = mockk<GitHubAuthenticationService>()
3636
private val syncLogService = mockk<SyncLogService>(relaxUnitFun = true)
3737
private val rateLimitHandler = mockk<RateLimitHandler>(relaxUnitFun = true)
38+
private val gitHubAPIService = mockk<GitHubAPIService>()
3839
private val gitHubEnterpriseService = GitHubEnterpriseService(gitHubClient, syncLogService)
3940
private val gitHubScanningService = GitHubScanningService(
4041
gitHubClient,
@@ -45,6 +46,7 @@ class GitHubScanningServiceTest {
4546
syncLogService,
4647
rateLimitHandler,
4748
gitHubEnterpriseService,
49+
gitHubAPIService
4850
)
4951
private val runId = UUID.randomUUID()
5052

@@ -54,13 +56,13 @@ class GitHubScanningServiceTest {
5456
@BeforeEach
5557
fun setup() {
5658
every { cachingService.get(any()) } returns "value"
57-
every { gitHubClient.getInstallations(any()) } returns listOf(
59+
every { gitHubAPIService.getPaginatedInstallations(any()) } returns listOf(
5860
Installation(1, Account("testInstallation"), permissions, events)
5961
)
6062
every { gitHubClient.createInstallationToken(1, any()) } returns
6163
InstallationTokenResponse("testToken", "2024-01-01T00:00:00Z", mapOf(), "all")
6264
every { cachingService.set(any(), any(), any()) } returns Unit
63-
every { gitHubClient.getOrganizations(any()) } returns listOf(Organization("testOrganization", 1))
65+
every { gitHubAPIService.getPaginatedOrganizations(any()) } returns listOf(Organization("testOrganization", 1))
6466
every { gitHubGraphQLService.getRepositories(any(), any()) } returns PagedRepositories(
6567
repositories = emptyList(),
6668
hasNextPage = false,
@@ -92,7 +94,7 @@ class GitHubScanningServiceTest {
9294
val runId = UUID.randomUUID()
9395

9496
every { cachingService.get("runId") } returns runId
95-
every { gitHubClient.getInstallations(any()) } returns emptyList()
97+
every { gitHubAPIService.getPaginatedInstallations(any()) } returns emptyList()
9698

9799
gitHubScanningService.scanGitHubResources()
98100

@@ -236,7 +238,7 @@ class GitHubScanningServiceTest {
236238
@Test
237239
fun `scanGitHubResources should skip organizations without correct permissions and events`() {
238240
every { cachingService.get("runId") } returns runId
239-
every { gitHubClient.getInstallations(any()) } returns listOf(
241+
every { gitHubAPIService.getPaginatedInstallations(any()) } returns listOf(
240242
Installation(1, Account("testInstallation1"), mapOf(), listOf()),
241243
Installation(2, Account("testInstallation2"), permissions, events),
242244
Installation(3, Account("testInstallation3"), permissions, events)

src/test/kotlin/net/leanix/githubagent/services/WebhookEventServiceTest.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ class WebhookEventServiceTest {
3737
@MockkBean
3838
private lateinit var gitHubAuthenticationService: GitHubAuthenticationService
3939

40+
@Autowired
41+
private lateinit var webhookEventService: WebhookEventService
42+
4043
@MockkBean
4144
private lateinit var gitHubClient: GitHubClient
4245

43-
@Autowired
44-
private lateinit var webhookEventService: WebhookEventService
46+
@MockkBean
47+
private lateinit var gitHubAPIService: GitHubAPIService
4548

4649
private val permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read")
4750
private val events = listOf("label", "public", "repository", "push")
@@ -53,7 +56,7 @@ class WebhookEventServiceTest {
5356
every { webSocketService.sendMessage(any(), any()) } returns Unit
5457
every { cachingService.get(any()) } returns "token"
5558
every { gitHubGraphQLService.getManifestFileContent(any(), any(), any(), any()) } returns "content"
56-
every { gitHubClient.getInstallations(any()) } returns listOf(installation)
59+
every { gitHubAPIService.getPaginatedInstallations(any()) } returns listOf(installation)
5760
every { gitHubClient.getInstallation(any(), any()) } returns installation
5861
}
5962

@@ -379,7 +382,8 @@ class WebhookEventServiceTest {
379382
every { cachingService.get("runId") } returnsMany listOf("value", null, runId)
380383
every { cachingService.set("runId", any(), any()) } just runs
381384
every { cachingService.remove("runId") } just runs
382-
every { gitHubClient.getOrganizations(any()) } returns listOf(Organization("testOrganization", 1))
385+
every { gitHubAPIService.getPaginatedOrganizations(any()) } returns
386+
listOf(Organization("testOrganization", 1))
383387

384388
val eventType = "INSTALLATION"
385389
val payload = """{

0 commit comments

Comments
 (0)