Skip to content

Commit 0a3da44

Browse files
authored
add support for listing repositories accessible to an app installation (#42)
* add method to list repos accessible to a Github App this change implements support for https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-repositories-accessible-to-the-app-installation I believe though, that in this initial form, the response is limited to only the first page of results returned by the Github API: #41 * add GithubAppClientTest#listAccessibleRepositories and refactor other tests This commit refactors GithubAppClientTest so that it tests the actual methods in GithubAppClient and adds a test of the new method `listAccessibleRepositories()`. Previously the test did not actually test any logic in GithubAppClient but rather the tests were of the deserialization logic for some of the types used in the class. I moved the test related to deserializing an AccessToken to a new AccessTokenTest, and replaced the other tests so that they use a MockWebServer to actually be able to test the HTTP requests/responses. * minor: unnecessary type arguments * forgot to teardown MockWebServer use it as a Rule instead to make things a little simpler * squash: remove redundant toCompletableFuture() calls
1 parent 32b0bf5 commit 0a3da44

File tree

11 files changed

+491
-118
lines changed

11 files changed

+491
-118
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@
194194
<version>0.2</version>
195195
<scope>test</scope>
196196
</dependency>
197+
<dependency>
198+
<groupId>org.hamcrest</groupId>
199+
<artifactId>hamcrest-core</artifactId>
200+
<version>2.2</version>
201+
<scope>test</scope>
202+
</dependency>
197203
<dependency>
198204
<groupId>junit</groupId>
199205
<artifactId>junit</artifactId>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*-
2+
* -\-\-
3+
* github-client
4+
* --
5+
* Copyright (C) 2016 - 2020 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
21+
package com.spotify.github.v3.apps;
22+
23+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
24+
import com.spotify.github.GithubStyle;
25+
import com.spotify.github.v3.repos.Repository;
26+
import java.util.List;
27+
import org.immutables.value.Value;
28+
29+
/**
30+
* Response for requests to "List repositories accessible to the app installation"
31+
*
32+
* https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-repositories-accessible-to-the-app-installation
33+
*/
34+
@Value.Immutable
35+
@GithubStyle
36+
@JsonDeserialize(as = ImmutableInstallationRepositoriesResponse.class)
37+
public interface InstallationRepositoriesResponse {
38+
int totalCount();
39+
40+
List<Repository> repositories();
41+
}

src/main/java/com/spotify/github/v3/clients/GitHubClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525

2626
import com.fasterxml.jackson.core.type.TypeReference;
2727
import com.spotify.github.jackson.Json;
28-
import com.spotify.github.v3.prs.Review;
2928
import com.spotify.github.v3.checks.AccessToken;
3029
import com.spotify.github.v3.comment.Comment;
3130
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
3231
import com.spotify.github.v3.exceptions.RequestNotOkException;
3332
import com.spotify.github.v3.git.Reference;
3433
import com.spotify.github.v3.prs.PullRequestItem;
34+
import com.spotify.github.v3.prs.Review;
3535
import com.spotify.github.v3.prs.ReviewRequests;
3636
import com.spotify.github.v3.repos.Branch;
3737
import com.spotify.github.v3.repos.CommitItem;

src/main/java/com/spotify/github/v3/clients/GithubAppClient.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.fasterxml.jackson.core.type.TypeReference;
2424
import com.google.common.collect.ImmutableMap;
25+
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
2526
import com.spotify.github.v3.checks.AccessToken;
2627
import com.spotify.github.v3.checks.Installation;
2728
import java.util.List;
@@ -35,6 +36,7 @@ public class GithubAppClient {
3536
private static final String GET_ACCESS_TOKEN_URL = "/app/installations/%s/access_tokens";
3637
private static final String GET_INSTALLATIONS_URL = "/app/installations?per_page=100";
3738
private static final String GET_INSTALLATION_REPO_URL = "/repos/%s/%s/installation";
39+
private static final String LIST_ACCESSIBLE_REPOS_URL = "/installation/repositories";
3840

3941
private final GitHubClient github;
4042
private final String owner;
@@ -44,7 +46,7 @@ public class GithubAppClient {
4446
ImmutableMap.of(HttpHeaders.ACCEPT, "application/vnd.github.machine-man-preview+json");
4547

4648
private static final TypeReference<List<Installation>> INSTALLATION_LIST_TYPE_REFERENCE =
47-
new TypeReference<List<Installation>>() {};
49+
new TypeReference<>() {};
4850

4951
GithubAppClient(final GitHubClient github, final String owner, final String repo) {
5052
this.github = github;
@@ -62,7 +64,7 @@ public CompletableFuture<List<Installation>> getInstallations() {
6264
}
6365

6466
/**
65-
* Get Installation of a.repo
67+
* Get Installation of a repo
6668
*
6769
* @return a list of Installation
6870
*/
@@ -80,4 +82,17 @@ public CompletableFuture<AccessToken> getAccessToken(final Integer installationI
8082
final String path = String.format(GET_ACCESS_TOKEN_URL, installationId);
8183
return github.post(path, "", AccessToken.class, extraHeaders);
8284
}
85+
86+
/**
87+
* Lists the repositories that an app installation can access.
88+
*
89+
* <p>see
90+
* https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-repositories-accessible-to-the-app-installation
91+
*/
92+
public CompletableFuture<InstallationRepositoriesResponse> listAccessibleRepositories(
93+
final int installationId) {
94+
95+
return GitHubClient.scopeForInstallationId(github, installationId)
96+
.request(LIST_ACCESSIBLE_REPOS_URL, InstallationRepositoriesResponse.class, extraHeaders);
97+
}
8398
}

src/test/java/com/spotify/github/FixtureHelper.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import static java.nio.charset.Charset.defaultCharset;
2525

2626
import com.google.common.io.Resources;
27+
import java.io.File;
2728
import java.io.IOException;
2829
import java.io.UncheckedIOException;
30+
import java.net.URISyntaxException;
31+
import java.net.URL;
2932

3033
public class FixtureHelper {
3134

@@ -38,4 +41,14 @@ public static String loadFixture(final String path) {
3841
throw new UncheckedIOException(e);
3942
}
4043
}
44+
45+
/** Return a File pointing to the resource on the classpath */
46+
public static File loadFile(final String path) {
47+
URL resource = getResource(FIXTURE_ROOT + path);
48+
try {
49+
return new File(resource.toURI());
50+
} catch (URISyntaxException e) {
51+
throw new RuntimeException(e);
52+
}
53+
}
4154
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*-
2+
* -\-\-
3+
* github-client
4+
* --
5+
* Copyright (C) 2016 - 2020 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
21+
package com.spotify.github.v3.checks;
22+
23+
import static org.hamcrest.CoreMatchers.is;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
26+
import com.spotify.github.FixtureHelper;
27+
import com.spotify.github.jackson.Json;
28+
import java.io.IOException;
29+
import java.time.ZonedDateTime;
30+
import org.junit.Test;
31+
32+
public class AccessTokenTest {
33+
private final Json json = Json.create();
34+
35+
@Test
36+
public void canDeserializeToken() throws IOException {
37+
final AccessToken accessToken =
38+
json.fromJson(FixtureHelper.loadFixture("checks/access-token.json"), AccessToken.class);
39+
assertThat(accessToken.token(), is("v1.1f699f1069f60xxx"));
40+
assertThat(accessToken.expiresAt(), is(ZonedDateTime.parse("2016-07-11T22:14:10Z")));
41+
}
42+
}

src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,61 +20,106 @@
2020

2121
package com.spotify.github.v3.clients;
2222

23-
import static java.nio.charset.StandardCharsets.UTF_8;
2423
import static org.hamcrest.MatcherAssert.assertThat;
24+
import static org.hamcrest.Matchers.containsInAnyOrder;
2525
import static org.hamcrest.core.Is.is;
26-
import static org.mockito.Mockito.mock;
27-
import static org.mockito.Mockito.when;
28-
29-
import com.google.common.io.Resources;
30-
import com.spotify.github.jackson.Json;
31-
import com.spotify.github.v3.checks.AccessToken;
32-
import com.spotify.github.v3.checks.InstallationList;
33-
import java.io.IOException;
34-
import java.io.UncheckedIOException;
26+
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.spotify.github.FixtureHelper;
29+
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
30+
import com.spotify.github.v3.checks.Installation;
31+
import java.io.File;
32+
import java.net.URI;
3533
import java.time.ZonedDateTime;
34+
import java.util.List;
35+
import java.util.concurrent.TimeUnit;
36+
import okhttp3.mockwebserver.MockResponse;
37+
import okhttp3.mockwebserver.MockWebServer;
38+
import okhttp3.mockwebserver.RecordedRequest;
3639
import org.junit.Before;
40+
import org.junit.Rule;
3741
import org.junit.Test;
3842

3943
public class GithubAppClientTest {
4044

41-
private static final String FIXTURES_PATH = "com/spotify/github/v3/githubapp/";
42-
private Json json;
45+
@Rule
46+
public final MockWebServer mockServer = new MockWebServer();
4347

44-
public static String loadResource(final String path) {
45-
try {
46-
return Resources.toString(Resources.getResource(path), UTF_8);
47-
} catch (IOException e) {
48-
throw new UncheckedIOException(e);
49-
}
50-
}
48+
private final ObjectMapper objectMapper = new ObjectMapper();
49+
private final int appId = 42;
50+
private GithubAppClient client;
5151

5252
@Before
53-
public void setUp() {
54-
final GitHubClient github = mock(GitHubClient.class);
55-
final GithubAppClient client = new GithubAppClient(github, "org", "repo");
56-
json = Json.create();
57-
when(github.json()).thenReturn(json);
53+
public void setUp() throws Exception {
54+
URI uri = mockServer.url("").uri();
55+
File key = FixtureHelper.loadFile("githubapp/key.pem");
56+
57+
GitHubClient rootclient = GitHubClient.create(uri, key, appId);
58+
client = rootclient.createRepositoryClient("", "").createGithubAppClient();
5859
}
5960

6061
@Test
6162
public void getInstallationsList() throws Exception {
62-
final InstallationList installations =
63-
json.fromJson(
64-
loadResource(FIXTURES_PATH + "installations-list.json"), InstallationList.class);
65-
66-
assertThat(installations.totalCount(), is(2));
67-
assertThat(installations.installations().get(0).account().login(), is("github"));
68-
assertThat(installations.installations().get(0).id(), is(1));
69-
assertThat(installations.installations().get(1).account().login(), is("octocat"));
70-
assertThat(installations.installations().get(1).id(), is(3));
63+
mockServer.enqueue(
64+
new MockResponse()
65+
.setResponseCode(200)
66+
.setBody(FixtureHelper.loadFixture("githubapp/installations-list.json")));
67+
68+
List<Installation> installations = client.getInstallations().join();
69+
70+
assertThat(installations.size(), is(2));
71+
assertThat(installations.get(0).account().login(), is("github"));
72+
assertThat(installations.get(0).id(), is(1));
73+
assertThat(installations.get(1).account().login(), is("octocat"));
74+
assertThat(installations.get(1).id(), is(3));
75+
76+
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
77+
assertThat(recordedRequest.getRequestUrl().encodedPath(), is("/app/installations"));
78+
assertThat(recordedRequest.getRequestUrl().queryParameter("per_page"), is("100"));
79+
80+
assertThat(
81+
recordedRequest.getHeaders().values("Accept"),
82+
containsInAnyOrder("application/json", "application/vnd.github.machine-man-preview+json"));
7183
}
7284

7385
@Test
74-
public void canDeserializeToken() throws IOException {
75-
final AccessToken accessToken =
76-
json.fromJson(loadResource(FIXTURES_PATH + "access-token.json"), AccessToken.class);
77-
assertThat(accessToken.token(), is("v1.1f699f1069f60xxx"));
78-
assertThat(accessToken.expiresAt(), is(ZonedDateTime.parse("2016-07-11T22:14:10Z")));
86+
public void listAccessibleRepositories() throws Exception {
87+
// response for POST /app/installations/:id/access_tokens
88+
final String installationAccessToken = "abc123-secret";
89+
mockServer.enqueue(
90+
new MockResponse()
91+
.setResponseCode(201)
92+
// this might not serialize 100% the same as the Json class's ObjectMapper but should be
93+
// fine for this test
94+
.setBody(
95+
objectMapper
96+
.createObjectNode()
97+
.put("token", installationAccessToken)
98+
.put("expires_at", ZonedDateTime.now().plusHours(1).toString())
99+
.toString()));
100+
101+
// response for GET /installation/repositories
102+
mockServer.enqueue(
103+
new MockResponse()
104+
.setResponseCode(200)
105+
.setBody(FixtureHelper.loadFixture("githubapp/accessible-repositories.json")));
106+
107+
InstallationRepositoriesResponse response =
108+
client.listAccessibleRepositories(1234).join();
109+
110+
assertThat(response.totalCount(), is(2));
111+
assertThat(response.repositories().size(), is(2));
112+
assertThat(response.repositories().get(0).id(), is(1));
113+
assertThat(response.repositories().get(1).id(), is(2));
114+
115+
RecordedRequest accessTokenRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
116+
assertThat(accessTokenRequest.getMethod(), is("POST"));
117+
assertThat(
118+
accessTokenRequest.getRequestUrl().encodedPath(),
119+
is("/app/installations/1234/access_tokens"));
120+
121+
RecordedRequest listReposRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
122+
assertThat(listReposRequest.getMethod(), is("GET"));
123+
assertThat(listReposRequest.getRequestUrl().encodedPath(), is("/installation/repositories"));
79124
}
80125
}
File renamed without changes.

0 commit comments

Comments
 (0)