Skip to content

Commit d62279a

Browse files
Add asAppScopedClient() function for convenience (#196)
* Add `asAppScopedClient()` function for convenience * Add tests, fix missing `getCause()`
1 parent 7cf19e5 commit d62279a

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

src/main/java/com/spotify/github/async/Async.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
package com.spotify.github.async;
2222

23+
import java.util.concurrent.CompletableFuture;
24+
import java.util.function.Function;
2325
import java.util.stream.Stream;
2426

2527
import static java.util.stream.StreamSupport.stream;
@@ -34,4 +36,19 @@ public static <T> Stream<T> streamFromPaginatingIterable(final Iterable<AsyncPag
3436
return stream(iterable.spliterator(), false)
3537
.flatMap(page -> stream(page.spliterator(), false));
3638
}
39+
40+
public static <T> CompletableFuture<T> exceptionallyCompose(
41+
final CompletableFuture<T> future, final Function<Throwable, CompletableFuture<T>> handler) {
42+
43+
return future
44+
.handle(
45+
(result, throwable) -> {
46+
if (throwable != null) {
47+
return handler.apply(throwable);
48+
} else {
49+
return CompletableFuture.completedFuture(result);
50+
}
51+
})
52+
.thenCompose(Function.identity());
53+
}
3754
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525

2626
import com.fasterxml.jackson.core.type.TypeReference;
2727
import com.spotify.github.Tracer;
28+
import com.spotify.github.async.Async;
2829
import com.spotify.github.jackson.Json;
2930
import com.spotify.github.v3.Team;
3031
import com.spotify.github.v3.User;
3132
import com.spotify.github.v3.checks.AccessToken;
33+
import com.spotify.github.v3.checks.Installation;
3234
import com.spotify.github.v3.comment.Comment;
3335
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
3436
import com.spotify.github.v3.exceptions.RequestNotOkException;
@@ -53,6 +55,7 @@
5355
import java.util.Objects;
5456
import java.util.Optional;
5557
import java.util.concurrent.CompletableFuture;
58+
import java.util.concurrent.CompletionStage;
5659
import java.util.concurrent.ConcurrentHashMap;
5760
import java.util.concurrent.atomic.AtomicBoolean;
5861
import java.util.function.Consumer;
@@ -71,6 +74,7 @@
7174
public class GitHubClient {
7275

7376
private static final int EXPIRY_MARGIN_IN_MINUTES = 5;
77+
private static final int HTTP_NOT_FOUND = 404;
7478

7579
private Tracer tracer = NoopTracer.INSTANCE;
7680

@@ -367,6 +371,37 @@ public GitHubClient withScopeForInstallationId(final int installationId) {
367371
installationId);
368372
}
369373

374+
/**
375+
* This is for clients authenticated as a GitHub App: when performing operations,
376+
* the "installation" of the App must be specified.
377+
* This returns a {@code GitHubClient} that has been scoped to the
378+
* user's/organization's installation of the app, if any.
379+
*/
380+
public CompletionStage<Optional<GitHubClient>> asAppScopedClient(final String owner) {
381+
return Async.exceptionallyCompose(this
382+
.createOrganisationClient(owner)
383+
.createGithubAppClient()
384+
.getInstallation()
385+
.thenApply(Installation::id), e -> {
386+
if (e.getCause() instanceof RequestNotOkException && ((RequestNotOkException) e.getCause()).statusCode() == HTTP_NOT_FOUND) {
387+
return this
388+
.createUserClient(owner)
389+
.createGithubAppClient()
390+
.getUserInstallation()
391+
.thenApply(Installation::id);
392+
}
393+
return CompletableFuture.failedFuture(e);
394+
})
395+
.thenApply(id -> Optional.of(this.withScopeForInstallationId(id)))
396+
.exceptionally(
397+
e -> {
398+
if (e.getCause() instanceof RequestNotOkException && ((RequestNotOkException) e.getCause()).statusCode() == HTTP_NOT_FOUND) {
399+
return Optional.empty();
400+
}
401+
throw new RuntimeException(e);
402+
});
403+
}
404+
370405
public GitHubClient withTracer(final Tracer tracer) {
371406
this.tracer = tracer;
372407
return this;

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import static com.google.common.io.Resources.getResource;
2424
import static java.nio.charset.Charset.defaultCharset;
25+
import static java.util.concurrent.CompletableFuture.completedFuture;
26+
import static java.util.concurrent.CompletableFuture.failedFuture;
2527
import static org.hamcrest.CoreMatchers.containsString;
2628
import static org.hamcrest.MatcherAssert.assertThat;
2729
import static org.hamcrest.collection.IsMapContaining.hasEntry;
@@ -33,6 +35,7 @@
3335
import com.google.common.io.Resources;
3436
import com.spotify.github.Tracer;
3537
import com.spotify.github.v3.checks.CheckSuiteResponseList;
38+
import com.spotify.github.v3.checks.Installation;
3639
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
3740
import com.spotify.github.v3.exceptions.RequestNotOkException;
3841
import com.spotify.github.v3.repos.CommitItem;
@@ -42,6 +45,7 @@
4245
import java.io.IOException;
4346
import java.net.URI;
4447
import java.net.URISyntaxException;
48+
import java.util.HashMap;
4549
import java.util.List;
4650
import java.util.Optional;
4751
import java.util.concurrent.CompletableFuture;
@@ -228,4 +232,59 @@ public void testGetCheckSuites() throws Throwable {
228232
assertThat(result.checkSuites().get(0).app().get().slug().get(), is("octoapp"));
229233

230234
}
235+
236+
@Test
237+
void asAppScopedClientGetsUserClientIfOrgClientNotFound() {
238+
var appGithub = GitHubClient.create(client, URI.create("http://bogus"), new byte[] {}, 1);
239+
var githubSpy = spy(appGithub);
240+
241+
var orgClientMock = mock(OrganisationClient.class);
242+
when(githubSpy.createOrganisationClient("owner")).thenReturn(orgClientMock);
243+
244+
var appClientMock = mock(GithubAppClient.class);
245+
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
246+
when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
247+
248+
var userClientMock = mock(UserClient.class);
249+
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);
250+
251+
var appClientMock2 = mock(GithubAppClient.class);
252+
when(userClientMock.createGithubAppClient()).thenReturn(appClientMock2);
253+
254+
var installationMock = mock(Installation.class);
255+
when(appClientMock2.getUserInstallation()).thenReturn(completedFuture(installationMock));
256+
when(installationMock.id()).thenReturn(1);
257+
258+
var maybeScopedClient = githubSpy.asAppScopedClient("owner").toCompletableFuture().join();
259+
260+
Assertions.assertTrue(maybeScopedClient.isPresent());
261+
verify(githubSpy, times(1)).createOrganisationClient("owner");
262+
verify(githubSpy, times(1)).createUserClient("owner");
263+
}
264+
265+
@Test
266+
void asAppScopedClientReturnsEmptyIfNoInstallation() {
267+
var appGithub = GitHubClient.create(client, URI.create("http://bogus"), new byte[] {}, 1);
268+
var githubSpy = spy(appGithub);
269+
270+
var orgClientMock = mock(OrganisationClient.class);
271+
when(githubSpy.createOrganisationClient("owner")).thenReturn(orgClientMock);
272+
273+
var appClientMock = mock(GithubAppClient.class);
274+
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
275+
when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
276+
277+
var userClientMock = mock(UserClient.class);
278+
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);
279+
280+
var appClientMock2 = mock(GithubAppClient.class);
281+
when(userClientMock.createGithubAppClient()).thenReturn(appClientMock2);
282+
283+
var installationMock = mock(Installation.class);
284+
when(appClientMock2.getUserInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
285+
when(installationMock.id()).thenReturn(1);
286+
287+
var maybeScopedClient = githubSpy.asAppScopedClient("owner").toCompletableFuture().join();
288+
Assertions.assertTrue(maybeScopedClient.isEmpty());
289+
}
231290
}

0 commit comments

Comments
 (0)