Skip to content

Commit 5817bc8

Browse files
authored
Merge pull request #108 from amazon-ospo/fixInstallationTracker
fix: add pagination when get GitHub App's installations
2 parents e8dc152 + c17b786 commit 5817bc8

File tree

3 files changed

+723
-691
lines changed

3 files changed

+723
-691
lines changed

src/packages/app-framework/src/gitHubService.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,46 @@ export class GitHubAPIService {
4141
}
4242

4343
async getInstallations({
44-
ocktokitClient = this.getOctokitClient.bind(this),
44+
octokitClient = this.getOctokitClient.bind(this),
4545
}: {
46-
ocktokitClient?: () => Octokit;
46+
octokitClient?: () => Octokit;
4747
}): Promise<AppInstallationsResponseType> {
48-
const octokit = ocktokitClient();
48+
const octokit = octokitClient();
4949

50-
const response = await octokit.rest.apps.listInstallations();
50+
try {
51+
const installations = await octokit.paginate(
52+
octokit.rest.apps.listInstallations,
53+
{ per_page: 100 },
54+
);
55+
56+
return installations as AppInstallationsResponseType;
57+
} catch (err: any) {
58+
const status = err?.status ?? err?.response?.status;
59+
const headers = err?.response?.headers;
60+
const data = err?.response?.data;
5161

52-
if (response.status >= 400) {
5362
throw new GitHubError(
54-
`GitHub API Error: status: ${response.status}, headers: ${response.headers}, error: ${response.data}`,
63+
`GitHub API Error: status: ${status}, headers: ${JSON.stringify(
64+
headers,
65+
)}, error: ${JSON.stringify(data)}`,
5566
);
5667
}
57-
58-
return response.data;
5968
}
6069

6170
async getInstallationToken({
6271
installationId,
6372
repositoryIds,
6473
repositoryNames,
6574
permissions,
66-
ocktokitClient: octokitClient = this.getOctokitClient.bind(this),
75+
octokitClient: octokitClient = this.getOctokitClient.bind(this),
6776
}: {
6877
installationId: number;
6978
repositoryIds?: number[];
7079
repositoryNames?: string[];
7180
permissions?: {
7281
[key: string]: string;
7382
};
74-
ocktokitClient?: () => Octokit;
83+
octokitClient?: () => Octokit;
7584
}): Promise<GetInstallationAccessTokenResponseType> {
7685
const octokit = octokitClient();
7786
try {
@@ -103,11 +112,11 @@ export class GitHubAPIService {
103112
}
104113

105114
async getAuthenticatedApp({
106-
ocktokitClient = this.getOctokitClient.bind(this),
115+
octokitClient: octokitClient = this.getOctokitClient.bind(this),
107116
}: {
108-
ocktokitClient?: () => Octokit;
117+
octokitClient?: () => Octokit;
109118
}): Promise<AppAuthenticationResponseType> {
110-
const octokit = ocktokitClient();
119+
const octokit = octokitClient();
111120
try {
112121
const response = await octokit.rest.apps.getAuthenticated();
113122

@@ -131,11 +140,11 @@ export class GitHubAPIService {
131140
}
132141

133142
async getRateLimit({
134-
ocktokitClient = this.getOctokitClient.bind(this),
143+
octokitClient: octokitClient = this.getOctokitClient.bind(this),
135144
}: {
136-
ocktokitClient?: () => Octokit;
145+
octokitClient?: () => Octokit;
137146
}): Promise<GetRateLimitResponseType> {
138-
const octokit = ocktokitClient();
147+
const octokit = octokitClient();
139148
const response = await octokit.rest.rateLimit.get();
140149
if (response.status >= 400) {
141150
throw new GitHubError(

src/packages/app-framework/test/gitHubService.test.ts

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ const mockOctokitRest = {
2222
},
2323
};
2424

25+
const mockPaginate = jest.fn();
26+
2527
jest.mock('@octokit/rest', () => ({
2628
Octokit: jest.fn().mockImplementation(() => ({
2729
rest: mockOctokitRest,
30+
paginate: mockPaginate,
2831
})),
2932
}));
3033

@@ -38,7 +41,7 @@ describe('GitHubAPIService', () => {
3841

3942
describe('getInstallations', () => {
4043
it('should return list of installations if able to find in output', async () => {
41-
const listInstallations = [
44+
const expectedInstallations = [
4245
{
4346
id: 788,
4447
account: {
@@ -55,46 +58,79 @@ describe('GitHubAPIService', () => {
5558
},
5659
];
5760

58-
mockOctokitRest.apps.listInstallations.mockResolvedValue({
59-
status: 200,
60-
data: listInstallations,
61-
headers: {},
62-
url: '',
63-
});
61+
mockPaginate.mockResolvedValue(expectedInstallations);
6462

6563
const result = await service.getInstallations({
66-
ocktokitClient: () => new Octokit() as any,
64+
octokitClient: () => new Octokit() as any,
6765
});
68-
expect(result).toEqual(listInstallations);
66+
67+
expect(mockPaginate).toHaveBeenCalledWith(
68+
mockOctokitRest.apps.listInstallations,
69+
{ per_page: 100 },
70+
);
71+
expect(result).toEqual(expectedInstallations);
6972
});
7073

71-
it('should return empty list of installations if able to find in output', async () => {
72-
mockOctokitRest.apps.listInstallations.mockResolvedValue({
73-
status: 200,
74-
data: [{}],
75-
headers: {},
76-
url: '',
74+
it('should return empty array when no installations found', async () => {
75+
mockPaginate.mockResolvedValue([]);
76+
77+
const result = await service.getInstallations({
78+
octokitClient: () => new Octokit() as any,
7779
});
7880

81+
expect(mockPaginate).toHaveBeenCalledWith(
82+
mockOctokitRest.apps.listInstallations,
83+
{ per_page: 100 },
84+
);
85+
expect(result).toEqual([]);
86+
expect(result).toHaveLength(0);
87+
});
88+
89+
it('should handle multiple pages automatically with octokit.paginate', async () => {
90+
const expectedInstallations = Array.from({ length: 250 }, (_, i) => ({
91+
id: i + 1,
92+
account: { node_id: `node_${i + 1}` },
93+
app_id: 1010,
94+
}));
95+
96+
mockPaginate.mockResolvedValue(expectedInstallations);
97+
7998
const result = await service.getInstallations({
80-
ocktokitClient: () => new Octokit() as any,
99+
octokitClient: () => new Octokit() as any,
81100
});
82-
expect(result).toEqual([{}]);
101+
102+
expect(mockPaginate).toHaveBeenCalledWith(
103+
mockOctokitRest.apps.listInstallations,
104+
{ per_page: 100 },
105+
);
106+
expect(result).toEqual(expectedInstallations);
107+
expect(result).toHaveLength(250);
83108
});
84109

85-
it('should throw error if GitHub API has an error', async () => {
86-
mockOctokitRest.apps.listInstallations.mockResolvedValue({
110+
it('should throw GitHubError when paginate throws HTTP error', async () => {
111+
const httpError = {
87112
status: 401,
88-
data: 'Invalid token',
89-
headers: { 'content-type': 'text/plain' },
90-
url: '',
91-
});
113+
response: {
114+
status: 401,
115+
headers: { 'content-type': 'application/json' },
116+
data: { message: 'Bad credentials' },
117+
},
118+
message: 'Bad credentials',
119+
};
120+
121+
mockPaginate.mockRejectedValue(httpError);
92122

93123
await expect(
94124
service.getInstallations({
95-
ocktokitClient: () => new Octokit() as any,
125+
octokitClient: () => new Octokit() as any,
96126
}),
97127
).rejects.toThrow(GitHubError);
128+
129+
await expect(
130+
service.getInstallations({
131+
octokitClient: () => new Octokit() as any,
132+
}),
133+
).rejects.toThrow('GitHub API Error: status: 401');
98134
});
99135
});
100136

@@ -118,7 +154,7 @@ describe('GitHubAPIService', () => {
118154

119155
const result = await service.getInstallationToken({
120156
installationId,
121-
ocktokitClient: () => new Octokit() as any,
157+
octokitClient: () => new Octokit() as any,
122158
});
123159
expect(result).toEqual(data);
124160
});
@@ -138,7 +174,7 @@ describe('GitHubAPIService', () => {
138174
await expect(
139175
service.getInstallationToken({
140176
installationId,
141-
ocktokitClient: () => new Octokit() as any,
177+
octokitClient: () => new Octokit() as any,
142178
}),
143179
).rejects.toThrow(DataError);
144180
});
@@ -154,7 +190,7 @@ describe('GitHubAPIService', () => {
154190
await expect(
155191
service.getInstallationToken({
156192
installationId,
157-
ocktokitClient: () => new Octokit() as any,
193+
octokitClient: () => new Octokit() as any,
158194
}),
159195
).rejects.toThrow(GitHubError);
160196
});
@@ -174,7 +210,7 @@ describe('GitHubAPIService', () => {
174210
});
175211

176212
const result = await service.getAuthenticatedApp({
177-
ocktokitClient: () => new Octokit() as any,
213+
octokitClient: () => new Octokit() as any,
178214
});
179215
expect(result).toEqual(output);
180216
});
@@ -193,7 +229,7 @@ describe('GitHubAPIService', () => {
193229

194230
await expect(
195231
service.getAuthenticatedApp({
196-
ocktokitClient: () => new Octokit() as any,
232+
octokitClient: () => new Octokit() as any,
197233
}),
198234
).rejects.toThrow(DataError);
199235
});
@@ -208,7 +244,7 @@ describe('GitHubAPIService', () => {
208244

209245
await expect(
210246
service.getAuthenticatedApp({
211-
ocktokitClient: () => new Octokit() as any,
247+
octokitClient: () => new Octokit() as any,
212248
}),
213249
).rejects.toThrow(GitHubError);
214250
});
@@ -253,7 +289,7 @@ describe('GitHubAPIService', () => {
253289
});
254290

255291
const result = await service.getRateLimit({
256-
ocktokitClient: () => new Octokit() as any,
292+
octokitClient: () => new Octokit() as any,
257293
});
258294
expect(result).toEqual(output);
259295
});
@@ -268,7 +304,7 @@ describe('GitHubAPIService', () => {
268304

269305
await expect(
270306
service.getRateLimit({
271-
ocktokitClient: () => new Octokit() as any,
307+
octokitClient: () => new Octokit() as any,
272308
}),
273309
).rejects.toThrow(GitHubError);
274310
});
@@ -283,7 +319,7 @@ describe('GitHubAPIService', () => {
283319

284320
await expect(
285321
service.getRateLimit({
286-
ocktokitClient: () => new Octokit() as any,
322+
octokitClient: () => new Octokit() as any,
287323
}),
288324
).rejects.toThrow(DataError);
289325
});

0 commit comments

Comments
 (0)