Skip to content

Commit

Permalink
Add asAppScopedClient() function for convenience (#196)
Browse files Browse the repository at this point in the history
* Add `asAppScopedClient()` function for convenience

* Add tests, fix missing `getCause()`
  • Loading branch information
mitchhentgesspotify authored Sep 12, 2024
1 parent 7cf19e5 commit d62279a
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/main/java/com/spotify/github/async/Async.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

package com.spotify.github.async;

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.stream.StreamSupport.stream;
Expand All @@ -34,4 +36,19 @@ public static <T> Stream<T> streamFromPaginatingIterable(final Iterable<AsyncPag
return stream(iterable.spliterator(), false)
.flatMap(page -> stream(page.spliterator(), false));
}

public static <T> CompletableFuture<T> exceptionallyCompose(
final CompletableFuture<T> future, final Function<Throwable, CompletableFuture<T>> handler) {

return future
.handle(
(result, throwable) -> {
if (throwable != null) {
return handler.apply(throwable);
} else {
return CompletableFuture.completedFuture(result);
}
})
.thenCompose(Function.identity());
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/spotify/github/v3/clients/GitHubClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@

import com.fasterxml.jackson.core.type.TypeReference;
import com.spotify.github.Tracer;
import com.spotify.github.async.Async;
import com.spotify.github.jackson.Json;
import com.spotify.github.v3.Team;
import com.spotify.github.v3.User;
import com.spotify.github.v3.checks.AccessToken;
import com.spotify.github.v3.checks.Installation;
import com.spotify.github.v3.comment.Comment;
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
import com.spotify.github.v3.exceptions.RequestNotOkException;
Expand All @@ -53,6 +55,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
Expand All @@ -71,6 +74,7 @@
public class GitHubClient {

private static final int EXPIRY_MARGIN_IN_MINUTES = 5;
private static final int HTTP_NOT_FOUND = 404;

private Tracer tracer = NoopTracer.INSTANCE;

Expand Down Expand Up @@ -367,6 +371,37 @@ public GitHubClient withScopeForInstallationId(final int installationId) {
installationId);
}

/**
* This is for clients authenticated as a GitHub App: when performing operations,
* the "installation" of the App must be specified.
* This returns a {@code GitHubClient} that has been scoped to the
* user's/organization's installation of the app, if any.
*/
public CompletionStage<Optional<GitHubClient>> asAppScopedClient(final String owner) {
return Async.exceptionallyCompose(this
.createOrganisationClient(owner)
.createGithubAppClient()
.getInstallation()
.thenApply(Installation::id), e -> {
if (e.getCause() instanceof RequestNotOkException && ((RequestNotOkException) e.getCause()).statusCode() == HTTP_NOT_FOUND) {
return this
.createUserClient(owner)
.createGithubAppClient()
.getUserInstallation()
.thenApply(Installation::id);
}
return CompletableFuture.failedFuture(e);
})
.thenApply(id -> Optional.of(this.withScopeForInstallationId(id)))
.exceptionally(
e -> {
if (e.getCause() instanceof RequestNotOkException && ((RequestNotOkException) e.getCause()).statusCode() == HTTP_NOT_FOUND) {
return Optional.empty();
}
throw new RuntimeException(e);
});
}

public GitHubClient withTracer(final Tracer tracer) {
this.tracer = tracer;
return this;
Expand Down
59 changes: 59 additions & 0 deletions src/test/java/com/spotify/github/v3/clients/GitHubClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import static com.google.common.io.Resources.getResource;
import static java.nio.charset.Charset.defaultCharset;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.CompletableFuture.failedFuture;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
Expand All @@ -33,6 +35,7 @@
import com.google.common.io.Resources;
import com.spotify.github.Tracer;
import com.spotify.github.v3.checks.CheckSuiteResponseList;
import com.spotify.github.v3.checks.Installation;
import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException;
import com.spotify.github.v3.exceptions.RequestNotOkException;
import com.spotify.github.v3.repos.CommitItem;
Expand All @@ -42,6 +45,7 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -228,4 +232,59 @@ public void testGetCheckSuites() throws Throwable {
assertThat(result.checkSuites().get(0).app().get().slug().get(), is("octoapp"));

}

@Test
void asAppScopedClientGetsUserClientIfOrgClientNotFound() {
var appGithub = GitHubClient.create(client, URI.create("http://bogus"), new byte[] {}, 1);
var githubSpy = spy(appGithub);

var orgClientMock = mock(OrganisationClient.class);
when(githubSpy.createOrganisationClient("owner")).thenReturn(orgClientMock);

var appClientMock = mock(GithubAppClient.class);
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));

var userClientMock = mock(UserClient.class);
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);

var appClientMock2 = mock(GithubAppClient.class);
when(userClientMock.createGithubAppClient()).thenReturn(appClientMock2);

var installationMock = mock(Installation.class);
when(appClientMock2.getUserInstallation()).thenReturn(completedFuture(installationMock));
when(installationMock.id()).thenReturn(1);

var maybeScopedClient = githubSpy.asAppScopedClient("owner").toCompletableFuture().join();

Assertions.assertTrue(maybeScopedClient.isPresent());
verify(githubSpy, times(1)).createOrganisationClient("owner");
verify(githubSpy, times(1)).createUserClient("owner");
}

@Test
void asAppScopedClientReturnsEmptyIfNoInstallation() {
var appGithub = GitHubClient.create(client, URI.create("http://bogus"), new byte[] {}, 1);
var githubSpy = spy(appGithub);

var orgClientMock = mock(OrganisationClient.class);
when(githubSpy.createOrganisationClient("owner")).thenReturn(orgClientMock);

var appClientMock = mock(GithubAppClient.class);
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));

var userClientMock = mock(UserClient.class);
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);

var appClientMock2 = mock(GithubAppClient.class);
when(userClientMock.createGithubAppClient()).thenReturn(appClientMock2);

var installationMock = mock(Installation.class);
when(appClientMock2.getUserInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
when(installationMock.id()).thenReturn(1);

var maybeScopedClient = githubSpy.asAppScopedClient("owner").toCompletableFuture().join();
Assertions.assertTrue(maybeScopedClient.isEmpty());
}
}

0 comments on commit d62279a

Please sign in to comment.