delete(final String path, final String data) {
- final Request request =
- requestBuilder(path)
- .method("DELETE", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
- .build();
- log.debug("Making DELETE request to {}", request.url().toString());
- return call(request);
- }
-
- /**
- * Create a URL for a given path to this Github server.
- *
- * @param path relative URI
- * @return URL to path on this server
- */
- String urlFor(final String path) {
- return baseUrl.toString().replaceAll("/+$", "") + "/" + path.replaceAll("^/+", "");
- }
-
- private Request.Builder requestBuilder(final String path) {
- final Request.Builder builder =
- new Request.Builder()
- .url(urlFor(path))
- .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
- .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
- builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(path));
-
- return builder;
- }
-
- private Request.Builder graphqlRequestBuilder() {
- URI url = graphqlUrl.orElseThrow(() -> new IllegalStateException("No graphql url set"));
- final Request.Builder builder =
- new Request.Builder()
- .url(url.toString())
- .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
- .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
- builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader("/graphql"));
- return builder;
- }
-
- public boolean isGraphqlEnabled() {
- return graphqlUrl.isPresent();
- }
-
-
- /*
- Generates the Authentication header, given the API endpoint and the credentials provided.
-
- Github Requests can be authenticated in 3 different ways.
- (1) Regular, static access token;
- (2) JWT Token, generated from a private key. Used in Github Apps;
- (3) Installation Token, generated from the JWT token. Also used in Github Apps.
- */
- private String getAuthorizationHeader(final String path) {
- if (isJwtRequest(path) && getPrivateKey().isEmpty()) {
- throw new IllegalStateException("This endpoint needs a client with a private key for an App");
- }
- if (getAccessToken().isPresent()) {
- return String.format("token %s", token);
- } else if (getPrivateKey().isPresent()) {
- final String jwtToken;
- try {
- jwtToken = JwtTokenIssuer.fromPrivateKey(privateKey).getToken(appId);
- } catch (Exception e) {
- throw new RuntimeException("There was an error generating JWT token", e);
- }
- if (isJwtRequest(path)) {
- return String.format("Bearer %s", jwtToken);
- }
- if (installationId == null) {
- throw new RuntimeException("This endpoint needs a client with an installation ID");
- }
- try {
- return String.format("token %s", getInstallationToken(jwtToken, installationId));
- } catch (Exception e) {
- throw new RuntimeException("Could not generate access token for github app", e);
- }
- }
- throw new RuntimeException("Not possible to authenticate. ");
- }
-
- private boolean isJwtRequest(final String path) {
- return path.startsWith("/app/installation") || path.endsWith("installation");
- }
-
- private String getInstallationToken(final String jwtToken, final int installationId)
- throws Exception {
-
- AccessToken installationToken = installationTokens.get(installationId);
-
- if (installationToken == null || isExpired(installationToken)) {
- log.info(
- "Github token for installation {} is either expired or null. Trying to get a new one.",
- installationId);
- installationToken = generateInstallationToken(jwtToken, installationId);
- installationTokens.put(installationId, installationToken);
- }
- return installationToken.token();
- }
-
- private boolean isExpired(final AccessToken token) {
- // Adds a few minutes to avoid making calls with an expired token due to clock differences
- return token.expiresAt().isBefore(ZonedDateTime.now().plusMinutes(EXPIRY_MARGIN_IN_MINUTES));
- }
-
- private AccessToken generateInstallationToken(final String jwtToken, final int installationId)
- throws Exception {
- log.info("Got JWT Token. Now getting Github access_token for installation {}", installationId);
- final String url = String.format(urlFor(GET_ACCESS_TOKEN_URL), installationId);
- final Request request =
- new Request.Builder()
- .addHeader("Accept", "application/vnd.github.machine-man-preview+json")
- .addHeader("Authorization", "Bearer " + jwtToken)
- .url(url)
- .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), ""))
- .build();
-
- final Response response = client.newCall(request).execute();
-
- if (!response.isSuccessful()) {
- throw new Exception(
- String.format(
- "Got non-2xx status %s when getting an access token from GitHub: %s",
- response.code(), response.message()));
- }
-
- if (response.body() == null) {
- throw new Exception(
- String.format(
- "Got empty response body when getting an access token from GitHub, HTTP status was: %s",
- response.message()));
- }
- final String text = response.body().string();
- response.body().close();
- return Json.create().fromJson(text, AccessToken.class);
- }
-
- private CompletableFuture call(final Request request) {
- final Call call = client.newCall(request);
-
- final CompletableFuture future = new CompletableFuture<>();
-
- // avoid multiple redirects
- final AtomicBoolean redirected = new AtomicBoolean(false);
-
- call.enqueue(
- new Callback() {
- @Override
- public void onFailure(final Call call, final IOException e) {
- future.completeExceptionally(e);
- }
-
- @Override
- public void onResponse(final Call call, final Response response) {
- processPossibleRedirects(response, redirected)
- .handle(
- (res, ex) -> {
- if (Objects.nonNull(ex)) {
- future.completeExceptionally(ex);
- } else if (!res.isSuccessful()) {
- try {
- future.completeExceptionally(mapException(res, request));
- } catch (final Throwable e) {
- future.completeExceptionally(e);
- } finally {
- if (res.body() != null) {
- res.body().close();
- }
+ public GitHubClient withTracer(final Tracer tracer) {
+ this.tracer = tracer;
+ return this;
+ }
+
+ public Optional getPrivateKey() {
+ return Optional.ofNullable(privateKey);
+ }
+
+ public Optional getAccessToken() {
+ return Optional.ofNullable(token);
+ }
+
+ /**
+ * Create a repository API client
+ *
+ * @param owner repository owner
+ * @param repo repository name
+ * @return repository API client
+ */
+ public RepositoryClient createRepositoryClient(final String owner, final String repo) {
+ return RepositoryClient.create(this, owner, repo);
+ }
+
+ /**
+ * Create a GitData API client
+ *
+ * @param owner repository owner
+ * @param repo repository name
+ * @return GitData API client
+ */
+ public GitDataClient createGitDataClient(final String owner, final String repo) {
+ return GitDataClient.create(this, owner, repo);
+ }
+
+ /**
+ * Create search API client
+ *
+ * @return search API client
+ */
+ public SearchClient createSearchClient() {
+ return SearchClient.create(this);
+ }
+
+ /**
+ * Create a checks API client
+ *
+ * @param owner repository owner
+ * @param repo repository name
+ * @return checks API client
+ */
+ public ChecksClient createChecksClient(final String owner, final String repo) {
+ return ChecksClient.create(this, owner, repo);
+ }
+
+ /**
+ * Create organisation API client
+ *
+ * @return organisation API client
+ */
+ public OrganisationClient createOrganisationClient(final String org) {
+ return OrganisationClient.create(this, org);
+ }
+
+ /**
+ * Create user API client
+ *
+ * @return user API client
+ */
+ public UserClient createUserClient(final String owner) {
+ return UserClient.create(this, owner);
+ }
+
+ Json json() {
+ return json;
+ }
+
+ /**
+ * Make an http GET request for the given path on the server
+ *
+ * @param path relative to the Github base url
+ * @return response body as a String
+ */
+ CompletableFuture request(final String path) {
+ final Request request = requestBuilder(path).build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http GET request for the given path on the server
+ *
+ * @param path relative to the Github base url
+ * @param extraHeaders extra github headers to be added to the call
+ * @return a reader of response body
+ */
+ CompletableFuture request(final String path, final Map extraHeaders) {
+ final Request.Builder builder = requestBuilder(path);
+ extraHeaders.forEach(builder::addHeader);
+ final Request request = builder.build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http GET request for the given path on the server
+ *
+ * @param path relative to the Github base url
+ * @return body deserialized as provided type
+ */
+ CompletableFuture request(final String path, final Class clazz) {
+ final Request request = requestBuilder(path).build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request)
+ .thenApply(body -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(body), clazz));
+ }
+
+ /**
+ * Make an http GET request for the given path on the server
+ *
+ * @param path relative to the Github base url
+ * @param extraHeaders extra github headers to be added to the call
+ * @return body deserialized as provided type
+ */
+ CompletableFuture request(
+ final String path, final Class clazz, final Map extraHeaders) {
+ final Request.Builder builder = requestBuilder(path);
+ extraHeaders.forEach(builder::addHeader);
+ final Request request = builder.build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request)
+ .thenApply(body -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(body), clazz));
+ }
+
+ /**
+ * Make an http request for the given path on the Github server.
+ *
+ * @param path relative to the Github base url
+ * @param extraHeaders extra github headers to be added to the call
+ * @return body deserialized as provided type
+ */
+ CompletableFuture request(
+ final String path,
+ final TypeReference typeReference,
+ final Map extraHeaders) {
+ final Request.Builder builder = requestBuilder(path);
+ extraHeaders.forEach(builder::addHeader);
+ final Request request = builder.build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request)
+ .thenApply(
+ response ->
+ json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), typeReference));
+ }
+
+ /**
+ * Make an http request for the given path on the Github server.
+ *
+ * @param path relative to the Github base url
+ * @return body deserialized as provided type
+ */
+ CompletableFuture request(final String path, final TypeReference typeReference) {
+ final Request request = requestBuilder(path).build();
+ log.debug("Making request to {}", request.url().toString());
+ return call(request)
+ .thenApply(
+ response ->
+ json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), typeReference));
+ }
+
+ /**
+ * Make an http POST request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @return response body as String
+ */
+ CompletableFuture post(final String path, final String data) {
+ final Request request =
+ requestBuilder(path)
+ .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
+ .build();
+ log.debug("Making POST request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http POST request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param extraHeaders
+ * @return response body as String
+ */
+ CompletableFuture post(
+ final String path, final String data, final Map extraHeaders) {
+ final Request.Builder builder =
+ requestBuilder(path)
+ .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data));
+ extraHeaders.forEach(builder::addHeader);
+ final Request request = builder.build();
+ log.debug("Making POST request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http POST request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param clazz class to cast response as
+ * @param extraHeaders
+ * @return response body deserialized as provided class
+ */
+ CompletableFuture post(
+ final String path,
+ final String data,
+ final Class clazz,
+ final Map extraHeaders) {
+ return post(path, data, extraHeaders)
+ .thenApply(
+ response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
+ }
+
+ /**
+ * Make an http POST request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param clazz class to cast response as
+ * @return response body deserialized as provided class
+ */
+ CompletableFuture post(final String path, final String data, final Class clazz) {
+ return post(path, data)
+ .thenApply(
+ response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
+ }
+
+ /**
+ * Make a POST request to the graphql endpoint of Github
+ *
+ * @param data request body as stringified JSON
+ * @return response
+ * @see "https://docs.github.com/en/enterprise-server@3.9/graphql/guides/forming-calls-with-graphql#communicating-with-graphql"
+ */
+ public CompletableFuture postGraphql(final String data) {
+ final Request request =
+ graphqlRequestBuilder()
+ .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
+ .build();
+ log.info("Making POST request to {}", request.url());
+ return call(request);
+ }
+
+ /**
+ * Make an http PUT request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @return response body as String
+ */
+ CompletableFuture put(final String path, final String data) {
+ final Request request =
+ requestBuilder(path)
+ .method("PUT", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
+ .build();
+ log.debug("Making POST request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make a HTTP PUT request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param clazz class to cast response as
+ * @return response body deserialized as provided class
+ */
+ CompletableFuture put(final String path, final String data, final Class clazz) {
+ return put(path, data)
+ .thenApply(
+ response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
+ }
+
+ /**
+ * Make an http PATCH request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @return response body as String
+ */
+ CompletableFuture patch(final String path, final String data) {
+ final Request request =
+ requestBuilder(path)
+ .method("PATCH", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
+ .build();
+ log.debug("Making PATCH request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http PATCH request for the given path with provided JSON body.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param clazz class to cast response as
+ * @return response body deserialized as provided class
+ */
+ CompletableFuture patch(final String path, final String data, final Class clazz) {
+ return patch(path, data)
+ .thenApply(
+ response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
+ }
+
+ /**
+ * Make an http PATCH request for the given path with provided JSON body
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @param clazz class to cast response as
+ * @return response body deserialized as provided class
+ */
+ CompletableFuture patch(
+ final String path,
+ final String data,
+ final Class clazz,
+ final Map extraHeaders) {
+ final Request.Builder builder =
+ requestBuilder(path)
+ .method("PATCH", RequestBody.create(parse(MediaType.APPLICATION_JSON), data));
+ extraHeaders.forEach(builder::addHeader);
+ final Request request = builder.build();
+ log.debug("Making PATCH request to {}", request.url().toString());
+ return call(request)
+ .thenApply(
+ response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
+ }
+
+ /**
+ * Make an http DELETE request for the given path.
+ *
+ * @param path relative to the Github base url
+ * @return response body as String
+ */
+ CompletableFuture delete(final String path) {
+ final Request request = requestBuilder(path).delete().build();
+ log.debug("Making DELETE request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Make an http DELETE request for the given path.
+ *
+ * @param path relative to the Github base url
+ * @param data request body as stringified JSON
+ * @return response body as String
+ */
+ CompletableFuture delete(final String path, final String data) {
+ final Request request =
+ requestBuilder(path)
+ .method("DELETE", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
+ .build();
+ log.debug("Making DELETE request to {}", request.url().toString());
+ return call(request);
+ }
+
+ /**
+ * Create a URL for a given path to this Github server.
+ *
+ * @param path relative URI
+ * @return URL to path on this server
+ */
+ String urlFor(final String path) {
+ return baseUrl.toString().replaceAll("/+$", "") + "/" + path.replaceAll("^/+", "");
+ }
+
+ private Request.Builder requestBuilder(final String path) {
+ final Request.Builder builder =
+ new Request.Builder()
+ .url(urlFor(path))
+ .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
+ .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
+ builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(path));
+
+ return builder;
+ }
+
+ private Request.Builder graphqlRequestBuilder() {
+ URI url = graphqlUrl.orElseThrow(() -> new IllegalStateException("No graphql url set"));
+ final Request.Builder builder =
+ new Request.Builder()
+ .url(url.toString())
+ .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
+ .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
+ builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader("/graphql"));
+ return builder;
+ }
+
+ public boolean isGraphqlEnabled() {
+ return graphqlUrl.isPresent();
+ }
+
+
+ /*
+ Generates the Authentication header, given the API endpoint and the credentials provided.
+
+ Github Requests can be authenticated in 3 different ways.
+ (1) Regular, static access token;
+ (2) JWT Token, generated from a private key. Used in Github Apps;
+ (3) Installation Token, generated from the JWT token. Also used in Github Apps.
+ */
+ private String getAuthorizationHeader(final String path) {
+ if (isJwtRequest(path) && getPrivateKey().isEmpty()) {
+ throw new IllegalStateException("This endpoint needs a client with a private key for an App");
+ }
+ if (getAccessToken().isPresent()) {
+ return String.format("token %s", token);
+ } else if (getPrivateKey().isPresent()) {
+ final String jwtToken;
+ try {
+ jwtToken = JwtTokenIssuer.fromPrivateKey(privateKey).getToken(appId);
+ } catch (Exception e) {
+ throw new RuntimeException("There was an error generating JWT token", e);
+ }
+ if (isJwtRequest(path)) {
+ return String.format("Bearer %s", jwtToken);
+ }
+ if (installationId == null) {
+ throw new RuntimeException("This endpoint needs a client with an installation ID");
+ }
+ try {
+ return String.format("token %s", getInstallationToken(jwtToken, installationId));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not generate access token for github app", e);
+ }
+ }
+ throw new RuntimeException("Not possible to authenticate. ");
+ }
+
+ private boolean isJwtRequest(final String path) {
+ return path.startsWith("/app/installation") || path.endsWith("installation");
+ }
+
+ private String getInstallationToken(final String jwtToken, final int installationId)
+ throws Exception {
+
+ AccessToken installationToken = installationTokens.get(installationId);
+
+ if (installationToken == null || isExpired(installationToken)) {
+ log.info(
+ "Github token for installation {} is either expired or null. Trying to get a new one.",
+ installationId);
+ installationToken = generateInstallationToken(jwtToken, installationId);
+ installationTokens.put(installationId, installationToken);
+ }
+ return installationToken.token();
+ }
+
+ private boolean isExpired(final AccessToken token) {
+ // Adds a few minutes to avoid making calls with an expired token due to clock differences
+ return token.expiresAt().isBefore(ZonedDateTime.now().plusMinutes(EXPIRY_MARGIN_IN_MINUTES));
+ }
+
+ private AccessToken generateInstallationToken(final String jwtToken, final int installationId)
+ throws Exception {
+ log.info("Got JWT Token. Now getting Github access_token for installation {}", installationId);
+ final String url = String.format(urlFor(GET_ACCESS_TOKEN_URL), installationId);
+ final Request request =
+ new Request.Builder()
+ .addHeader("Accept", "application/vnd.github.machine-man-preview+json")
+ .addHeader("Authorization", "Bearer " + jwtToken)
+ .url(url)
+ .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), ""))
+ .build();
+
+ final Response response = client.newCall(request).execute();
+
+ if (!response.isSuccessful()) {
+ throw new Exception(
+ String.format(
+ "Got non-2xx status %s when getting an access token from GitHub: %s",
+ response.code(), response.message()));
+ }
+
+ if (response.body() == null) {
+ throw new Exception(
+ String.format(
+ "Got empty response body when getting an access token from GitHub, HTTP status was: %s",
+ response.message()));
+ }
+ final String text = response.body().string();
+ response.body().close();
+ return Json.create().fromJson(text, AccessToken.class);
+ }
+
+ private CompletableFuture call(final Request request) {
+ try (Span span = tracer.span(request)) {
+ if (this.callFactory == null) {
+ this.callFactory = this.tracer.createTracedClient(this.client);
+ }
+ final Call call = this.callFactory.newCall(request);
+
+ final CompletableFuture future = new CompletableFuture<>();
+
+ // avoid multiple redirects
+ final AtomicBoolean redirected = new AtomicBoolean(false);
+
+ call.enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NotNull final Call call, final IOException e) {
+ future.completeExceptionally(e);
+ }
+
+ @Override
+ public void onResponse(@NotNull final Call call, final Response response) {
+ processPossibleRedirects(response, redirected)
+ .handle(
+ (res, ex) -> {
+ if (Objects.nonNull(ex)) {
+ future.completeExceptionally(ex);
+ } else if (!res.isSuccessful()) {
+ try {
+ future.completeExceptionally(mapException(res, request));
+ } catch (final Throwable e) {
+ future.completeExceptionally(e);
+ } finally {
+ if (res.body() != null) {
+ res.body().close();
+ }
+ }
+ } else {
+ future.complete(res);
+ }
+ return res;
+ });
}
- } else {
- future.complete(res);
- }
- return res;
});
- }
- });
- tracer.span(request.url().toString(), request.method(), future);
- return future;
- }
-
- private RequestNotOkException mapException(final Response res, final Request request)
- throws IOException {
- String bodyString = res.body() != null ? res.body().string() : "";
- Map> headersMap = res.headers().toMultimap();
-
- if (res.code() == FORBIDDEN) {
- if (bodyString.contains("Repository was archived so is read-only")) {
- return new ReadOnlyRepositoryException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
- }
- }
-
- return new RequestNotOkException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
- }
-
- CompletableFuture processPossibleRedirects(
- final Response response, final AtomicBoolean redirected) {
- if (response.code() >= PERMANENT_REDIRECT
- && response.code() <= TEMPORARY_REDIRECT
- && !redirected.get()) {
- redirected.set(true);
- // redo the same request with a new URL
- final String newLocation = response.header("Location");
- final Request request =
- requestBuilder(newLocation)
- .url(newLocation)
- .method(response.request().method(), response.request().body())
- .build();
- // Do the new call and complete the original future when the new call completes
- return call(request);
- }
-
- return completedFuture(response);
- }
-
- /**
- * Wrapper to Constructors that expose File object for the privateKey argument
- * */
- private static GitHubClient createOrThrow(final OkHttpClient httpClient, final URI baseUrl, final URI graphqlUrl, final File privateKey, final Integer appId, final Integer installationId) {
- try {
- return new GitHubClient(httpClient, baseUrl, graphqlUrl, null, FileUtils.readFileToByteArray(privateKey), appId, installationId);
- } catch (IOException e) {
- throw new RuntimeException("There was an error generating JWT token", e);
- }
- }
+ tracer.attachSpanToFuture(span, future);
+ return future;
+ }
+ }
+
+ private RequestNotOkException mapException(final Response res, final Request request)
+ throws IOException {
+ String bodyString = res.body() != null ? res.body().string() : "";
+ Map> headersMap = res.headers().toMultimap();
+
+ if (res.code() == FORBIDDEN) {
+ if (bodyString.contains("Repository was archived so is read-only")) {
+ return new ReadOnlyRepositoryException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
+ }
+ }
+
+ return new RequestNotOkException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
+ }
+
+ CompletableFuture processPossibleRedirects(
+ final Response response, final AtomicBoolean redirected) {
+ if (response.code() >= PERMANENT_REDIRECT
+ && response.code() <= TEMPORARY_REDIRECT
+ && !redirected.get()) {
+ redirected.set(true);
+ // redo the same request with a new URL
+ final String newLocation = response.header("Location");
+ final Request request =
+ requestBuilder(newLocation)
+ .url(newLocation)
+ .method(response.request().method(), response.request().body())
+ .build();
+ // Do the new call and complete the original future when the new call completes
+ return call(request);
+ }
+
+ return completedFuture(response);
+ }
+
+ /**
+ * Wrapper to Constructors that expose File object for the privateKey argument
+ */
+ private static GitHubClient createOrThrow(final OkHttpClient httpClient, final URI baseUrl, final URI graphqlUrl, final File privateKey, final Integer appId, final Integer installationId) {
+ try {
+ return new GitHubClient(httpClient, baseUrl, graphqlUrl, null, FileUtils.readFileToByteArray(privateKey), appId, installationId);
+ } catch (IOException e) {
+ throw new RuntimeException("There was an error generating JWT token", e);
+ }
+ }
}
diff --git a/src/main/java/com/spotify/github/v3/clients/NoopTracer.java b/src/main/java/com/spotify/github/v3/clients/NoopTracer.java
index 5b3c769e..60d64271 100644
--- a/src/main/java/com/spotify/github/v3/clients/NoopTracer.java
+++ b/src/main/java/com/spotify/github/v3/clients/NoopTracer.java
@@ -7,9 +7,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,11 +19,18 @@
*/
package com.spotify.github.v3.clients;
-import com.spotify.github.Span;
-import com.spotify.github.Tracer;
+
+import com.spotify.github.tracing.Span;
+import com.spotify.github.tracing.Tracer;
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletionStage;
+import static java.util.Objects.requireNonNull;
+
public class NoopTracer implements Tracer {
public static final NoopTracer INSTANCE = new NoopTracer();
@@ -40,10 +47,37 @@ public Span failure(final Throwable t) {
}
@Override
- public void close() {}
+ public void close() {
+ }
+
+ @Override
+ public Span addTag(final String key, final String value) {
+ return this;
+ }
+
+ @Override
+ public Span addTag(final String key, final boolean value) {
+ return this;
+ }
+
+ @Override
+ public Span addTag(final String key, final long value) {
+ return this;
+ }
+
+ @Override
+ public Span addTag(final String key, final double value) {
+ return this;
+ }
+
+ @Override
+ public Span addEvent(final String description) {
+ return this;
+ }
};
- private NoopTracer() {}
+ private NoopTracer() {
+ }
@Override
public Span span(
@@ -53,5 +87,46 @@ public Span span(
return SPAN;
}
+ @Override
+ public Span span(final String path, final String method) {
+ return SPAN;
+ }
+
+ @Override
+ public Span span(final Request request) {
+ return SPAN;
+ }
+
+ @Override
+ public Span span(final Request request, final CompletionStage> future) {
+ return SPAN;
+ }
+
+ @Override
+ public void attachSpanToFuture(final Span span, final CompletionStage> future) {
+ requireNonNull(span);
+ requireNonNull(future);
+ future.whenComplete(
+ (result, t) -> {
+ if (t == null) {
+ span.success();
+ } else {
+ span.failure(t);
+ }
+ span.close();
+ });
+ }
+
+ @Override
+ public Call.Factory createTracedClient(final OkHttpClient client) {
+ return new Call.Factory() {
+ @NotNull
+ @Override
+ public Call newCall(@NotNull final Request request) {
+ return client.newCall(request);
+ }
+ };
+ }
+
}
diff --git a/src/test/java/com/spotify/github/opencensus/OpenCensusSpanTest.java b/src/test/java/com/spotify/github/opencensus/OpenCensusSpanTest.java
deleted file mode 100644
index c6b5d94d..00000000
--- a/src/test/java/com/spotify/github/opencensus/OpenCensusSpanTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*-
- * -\-\-
- * github-api
- * --
- * Copyright (C) 2021 Spotify AB
- * --
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * -/-/-
- */
-
-package com.spotify.github.opencensus;
-
-import com.spotify.github.Span;
-import com.spotify.github.v3.exceptions.RequestNotOkException;
-import io.opencensus.trace.AttributeValue;
-import io.opencensus.trace.Status;
-import java.util.Collections;
-import org.junit.jupiter.api.Test;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-class OpenCensusSpanTest {
- private io.opencensus.trace.Span wrapped = mock(io.opencensus.trace.Span.class);
-
- @Test
- public void succeed() {
- final Span span = new OpenCensusSpan(wrapped);
- span.success();
- span.close();
-
- verify(wrapped).setStatus(Status.OK);
- verify(wrapped).end();
- }
-
- @Test
- public void fail() {
- final Span span = new OpenCensusSpan(wrapped);
- span.failure(new RequestNotOkException("method", "path", 404, "Not found", Collections.emptyMap()));
- span.close();
-
- verify(wrapped).setStatus(Status.UNKNOWN);
- verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(404));
- verify(wrapped).end();
- }
-
- @Test
- public void failOnServerError() {
- final Span span = new OpenCensusSpan(wrapped);
- span.failure(new RequestNotOkException("method", "path", 500, "Internal Server Error", Collections.emptyMap()));
- span.close();
-
- verify(wrapped).setStatus(Status.UNKNOWN);
- verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(500));
- verify(wrapped).putAttribute("error", AttributeValue.booleanAttributeValue(true));
- verify(wrapped).end();
- }
-
-}
diff --git a/src/test/java/com/spotify/github/opencensus/OpenCensusTracerTest.java b/src/test/java/com/spotify/github/opencensus/OpenCensusTracerTest.java
deleted file mode 100644
index 27b5c52b..00000000
--- a/src/test/java/com/spotify/github/opencensus/OpenCensusTracerTest.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*-
- * -\-\-
- * github-client
- * --
- * Copyright (C) 2016 - 2021 Spotify AB
- * --
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * -/-/-
- */
-
-package com.spotify.github.opencensus;
-
-
-import io.grpc.Context;
-import io.opencensus.trace.*;
-import io.opencensus.trace.config.TraceConfig;
-import io.opencensus.trace.config.TraceParams;
-import io.opencensus.trace.export.SpanData;
-import io.opencensus.trace.samplers.Samplers;
-import io.opencensus.trace.unsafe.ContextUtils;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-
-import static io.opencensus.trace.AttributeValue.stringAttributeValue;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class OpenCensusTracerTest {
-
-
- private final String rootSpanName = "root span";
- private TestExportHandler spanExporterHandler;
-
- /**
- * Test that trace() a) returns a future that completes when the input future completes and b)
- * sets up the Spans appropriately so that the Span for the operation is exported with the
- * rootSpan set as the parent.
- */
- @Test
- public void testTrace_CompletionStage_Simple() throws Exception {
- Span rootSpan = startRootSpan();
- final CompletableFuture future = new CompletableFuture<>();
- OpenCensusTracer tracer = new OpenCensusTracer();
-
- tracer.span("path", "GET", future);
- future.complete("all done");
- rootSpan.end();
-
- List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
- assertEquals(2, exportedSpans.size());
-
- SpanData root = findSpan(exportedSpans, rootSpanName);
- SpanData inner = findSpan(exportedSpans, "GitHub Request");
-
- assertEquals(root.getContext().getTraceId(), inner.getContext().getTraceId());
- assertEquals(root.getContext().getSpanId(), inner.getParentSpanId());
- final Map attributes = inner.getAttributes().getAttributeMap();
- assertEquals(stringAttributeValue("github-api-client"), attributes.get("component"));
- assertEquals(stringAttributeValue("github"), attributes.get("peer.service"));
- assertEquals(stringAttributeValue("path"), attributes.get("http.url"));
- assertEquals(stringAttributeValue("GET"), attributes.get("method"));
- assertEquals(Status.OK, inner.getStatus());
- }
-
- @Test
- public void testTrace_CompletionStage_Fails() throws Exception {
- Span rootSpan = startRootSpan();
- final CompletableFuture future = new CompletableFuture<>();
- OpenCensusTracer tracer = new OpenCensusTracer();
-
- tracer.span("path", "POST", future);
- future.completeExceptionally(new Exception("GitHub failed!"));
- rootSpan.end();
-
- List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
- assertEquals(2, exportedSpans.size());
-
- SpanData root = findSpan(exportedSpans, rootSpanName);
- SpanData inner = findSpan(exportedSpans, "GitHub Request");
-
- assertEquals(root.getContext().getTraceId(), inner.getContext().getTraceId());
- assertEquals(root.getContext().getSpanId(), inner.getParentSpanId());
- final Map attributes = inner.getAttributes().getAttributeMap();
- assertEquals(stringAttributeValue("github-api-client"), attributes.get("component"));
- assertEquals(stringAttributeValue("github"), attributes.get("peer.service"));
- assertEquals(stringAttributeValue("path"), attributes.get("http.url"));
- assertEquals(stringAttributeValue("POST"), attributes.get("method"));
- assertEquals(Status.UNKNOWN, inner.getStatus());
- }
-
- private Span startRootSpan() {
- Span rootSpan = Tracing.getTracer().spanBuilder(rootSpanName).startSpan();
- Context context = ContextUtils.withValue(Context.current(), rootSpan);
- context.attach();
- return rootSpan;
- }
-
- private SpanData findSpan(final List spans, final String name) {
- return spans.stream().filter(s -> s.getName().equals(name)).findFirst().get();
- }
-
- @BeforeEach
- public void setUpExporter() {
- spanExporterHandler = new TestExportHandler();
- Tracing.getExportComponent().getSpanExporter().registerHandler("test", spanExporterHandler);
- }
-
- @BeforeAll
- public static void setupTracing() {
- final TraceConfig traceConfig = Tracing.getTraceConfig();
- final Sampler sampler = Samplers.alwaysSample();
- final TraceParams newParams =
- traceConfig.getActiveTraceParams().toBuilder().setSampler(sampler).build();
- traceConfig.updateActiveTraceParams(newParams);
- }
-}
\ No newline at end of file
diff --git a/src/test/java/com/spotify/github/opencensus/TestExportHandler.java b/src/test/java/com/spotify/github/tracing/OcTestExportHandler.java
similarity index 93%
rename from src/test/java/com/spotify/github/opencensus/TestExportHandler.java
rename to src/test/java/com/spotify/github/tracing/OcTestExportHandler.java
index c1519572..b5919c99 100644
--- a/src/test/java/com/spotify/github/opencensus/TestExportHandler.java
+++ b/src/test/java/com/spotify/github/tracing/OcTestExportHandler.java
@@ -18,7 +18,7 @@
* -/-/-
*/
-package com.spotify.github.opencensus;
+package com.spotify.github.tracing;
import io.opencensus.trace.export.SpanData;
import io.opencensus.trace.export.SpanExporter;
@@ -39,8 +39,8 @@
* forever until the given number of spans is exported, which could be never. So instead we define
* our own very simple implementation.
*/
-class TestExportHandler extends SpanExporter.Handler {
- private static final Logger LOG = LoggerFactory.getLogger(TestExportHandler.class);
+class OcTestExportHandler extends SpanExporter.Handler {
+ private static final Logger LOG = LoggerFactory.getLogger(OcTestExportHandler.class);
private final List receivedSpans = new ArrayList<>();
private final Object lock = new Object();
diff --git a/src/test/java/com/spotify/github/tracing/OpenCensusSpanTest.java b/src/test/java/com/spotify/github/tracing/OpenCensusSpanTest.java
new file mode 100644
index 00000000..f53d4505
--- /dev/null
+++ b/src/test/java/com/spotify/github/tracing/OpenCensusSpanTest.java
@@ -0,0 +1,160 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2021 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.github.tracing;
+
+import com.spotify.github.tracing.opencensus.OpenCensusSpan;
+import com.spotify.github.v3.exceptions.RequestNotOkException;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Status;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+class OpenCensusSpanTest {
+ private final io.opencensus.trace.Span wrapped = mock(io.opencensus.trace.Span.class);
+
+ @Test
+ public void succeed() {
+ final Span span = new OpenCensusSpan(wrapped);
+ span.success();
+ span.close();
+
+ verify(wrapped).setStatus(Status.OK);
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void fail() {
+ final Span span = new OpenCensusSpan(wrapped);
+ span.failure(
+ new RequestNotOkException("method", "path", 404, "Not found", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(Status.UNKNOWN);
+ verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(404));
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void failOnServerError() {
+ final Span span = new OpenCensusSpan(wrapped);
+ span.failure(
+ new RequestNotOkException(
+ "method", "path", 500, "Internal Server Error", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(Status.UNKNOWN);
+ verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(500));
+ verify(wrapped).putAttribute("error", AttributeValue.booleanAttributeValue(true));
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void addTags() {
+ final Span span = new OpenCensusSpan(wrapped);
+ span.addTag("key", "value");
+ span.addTag("key", true);
+ span.addTag("key", 42L);
+ span.close();
+
+ verify(wrapped).putAttribute("key", AttributeValue.stringAttributeValue("value"));
+ verify(wrapped).putAttribute("key", AttributeValue.booleanAttributeValue(true));
+ verify(wrapped).putAttribute("key", AttributeValue.longAttributeValue(42L));
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void addEvent() {
+ final Span span = new OpenCensusSpan(wrapped);
+ span.addEvent("description");
+ span.close();
+
+ verify(wrapped).addAnnotation("description");
+ verify(wrapped).end();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void succeedDeprecated() {
+ final Span span = new com.spotify.github.opencensus.OpenCensusSpan(wrapped);
+ span.success();
+ span.close();
+
+ verify(wrapped).setStatus(Status.OK);
+ verify(wrapped).end();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void failDeprecated() {
+ final Span span = new com.spotify.github.opencensus.OpenCensusSpan(wrapped);
+ span.failure(
+ new RequestNotOkException("method", "path", 404, "Not found", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(Status.UNKNOWN);
+ verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(404));
+ verify(wrapped).end();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void failOnServerErrorDeprecated() {
+ final Span span = new com.spotify.github.opencensus.OpenCensusSpan(wrapped);
+ span.failure(
+ new RequestNotOkException(
+ "method", "path", 500, "Internal Server Error", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(Status.UNKNOWN);
+ verify(wrapped).putAttribute("http.status_code", AttributeValue.longAttributeValue(500));
+ verify(wrapped).putAttribute("error", AttributeValue.booleanAttributeValue(true));
+ verify(wrapped).end();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void addTagsDeprecated() {
+ final Span span = new com.spotify.github.opencensus.OpenCensusSpan(wrapped);
+ span.addTag("key", "value");
+ span.addTag("key", true);
+ span.addTag("key", 42L);
+ span.close();
+
+ verify(wrapped).putAttribute("key", AttributeValue.stringAttributeValue("value"));
+ verify(wrapped).putAttribute("key", AttributeValue.booleanAttributeValue(true));
+ verify(wrapped).putAttribute("key", AttributeValue.longAttributeValue(42L));
+ verify(wrapped).end();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void addEventDeprecated() {
+ final Span span = new com.spotify.github.opencensus.OpenCensusSpan(wrapped);
+ span.addEvent("description");
+ span.close();
+
+ verify(wrapped).addAnnotation("description");
+ verify(wrapped).end();
+ }
+}
diff --git a/src/test/java/com/spotify/github/tracing/OpenCensusTracerTest.java b/src/test/java/com/spotify/github/tracing/OpenCensusTracerTest.java
new file mode 100644
index 00000000..2d0b6fcf
--- /dev/null
+++ b/src/test/java/com/spotify/github/tracing/OpenCensusTracerTest.java
@@ -0,0 +1,185 @@
+/*-
+ * -\-\-
+ * github-client
+ * --
+ * Copyright (C) 2016 - 2021 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.github.tracing;
+
+import com.spotify.github.tracing.opencensus.OpenCensusTracer;
+import io.grpc.Context;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.*;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.samplers.Samplers;
+import io.opencensus.trace.unsafe.ContextUtils;
+import okhttp3.Call;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static io.opencensus.trace.AttributeValue.stringAttributeValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OpenCensusTracerTest {
+
+ private final String rootSpanName = "root span";
+ private OcTestExportHandler spanExporterHandler;
+
+ /**
+ * Test that trace() a) returns a future that completes when the input future completes and b)
+ * sets up the Spans appropriately so that the Span for the operation is exported with the
+ * rootSpan set as the parent.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageSimple(final String requestMethod) throws Exception {
+ io.opencensus.trace.Span rootSpan = startRootSpan();
+ final CompletableFuture future = new CompletableFuture<>();
+ OpenCensusTracer tracer = new OpenCensusTracer();
+
+ tracer.span("path", requestMethod, future);
+ future.complete("all done");
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getContext().getTraceId(), inner.getContext().getTraceId());
+ assertEquals(root.getContext().getSpanId(), inner.getParentSpanId());
+ final Map attributes = inner.getAttributes().getAttributeMap();
+ assertEquals(stringAttributeValue("github-api-client"), attributes.get("component"));
+ assertEquals(stringAttributeValue("github"), attributes.get("peer.service"));
+ assertEquals(stringAttributeValue("path"), attributes.get("http.url"));
+ assertEquals(stringAttributeValue(requestMethod), attributes.get("method"));
+ assertEquals(Status.OK, inner.getStatus());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageFails(final String requestMethod) throws Exception {
+ io.opencensus.trace.Span rootSpan = startRootSpan();
+ final CompletableFuture future = new CompletableFuture<>();
+ OpenCensusTracer tracer = new OpenCensusTracer();
+
+ tracer.span("path", requestMethod, future);
+ future.completeExceptionally(new Exception("GitHub failed!"));
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getContext().getTraceId(), inner.getContext().getTraceId());
+ assertEquals(root.getContext().getSpanId(), inner.getParentSpanId());
+ final Map attributes = inner.getAttributes().getAttributeMap();
+ assertEquals(stringAttributeValue("github-api-client"), attributes.get("component"));
+ assertEquals(stringAttributeValue("github"), attributes.get("peer.service"));
+ assertEquals(stringAttributeValue("path"), attributes.get("http.url"));
+ assertEquals(stringAttributeValue(requestMethod), attributes.get("method"));
+ assertEquals(Status.UNKNOWN, inner.getStatus());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageWithRequest(final String requestMethod) throws Exception {
+ io.opencensus.trace.Span rootSpan = startRootSpan();
+ OpenCensusTracer tracer = new OpenCensusTracer();
+ final CompletableFuture future = new CompletableFuture<>();
+ Request mockRequest = mock(Request.class);
+ when(mockRequest.url())
+ .thenReturn(HttpUrl.parse("https://api.github.com/repos/spotify/github-java-client"));
+ when(mockRequest.method()).thenReturn(requestMethod);
+
+ try (com.spotify.github.tracing.Span span = tracer.span(mockRequest)) {
+ tracer.attachSpanToFuture(span, future);
+ future.complete("all done");
+ }
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getContext().getTraceId(), inner.getContext().getTraceId());
+ assertEquals(root.getContext().getSpanId(), inner.getParentSpanId());
+ final Map attributes = inner.getAttributes().getAttributeMap();
+ assertEquals(stringAttributeValue("github-api-client"), attributes.get("component"));
+ assertEquals(stringAttributeValue("github"), attributes.get("peer.service"));
+ assertEquals(
+ stringAttributeValue("https://api.github.com/repos/spotify/github-java-client"),
+ attributes.get("http.url"));
+ assertEquals(stringAttributeValue(requestMethod), attributes.get("method"));
+ assertEquals(Status.OK, inner.getStatus());
+ }
+
+ @Test
+ public void createTracedClient() {
+ OpenCensusTracer tracer = new OpenCensusTracer();
+ OkHttpClient client = new OkHttpClient.Builder().build();
+ Call.Factory callFactory = tracer.createTracedClient(client);
+ assertNotNull(callFactory);
+ }
+
+ @SuppressWarnings("deprecation")
+ private io.opencensus.trace.Span startRootSpan() {
+ Span rootSpan = Tracing.getTracer().spanBuilder(rootSpanName).startSpan();
+ Context context = ContextUtils.withValue(Context.current(), rootSpan);
+ context.attach();
+ return rootSpan;
+ }
+
+ private SpanData findSpan(final List spans, final String name) {
+ return spans.stream().filter(s -> s.getName().equals(name)).findFirst().get();
+ }
+
+ @BeforeEach
+ public void setUpExporter() {
+ spanExporterHandler = new OcTestExportHandler();
+ Tracing.getExportComponent().getSpanExporter().registerHandler("test", spanExporterHandler);
+ }
+
+ @BeforeAll
+ public static void setupTracing() {
+ final TraceConfig traceConfig = Tracing.getTraceConfig();
+ final Sampler sampler = Samplers.alwaysSample();
+ final TraceParams newParams =
+ traceConfig.getActiveTraceParams().toBuilder().setSampler(sampler).build();
+ traceConfig.updateActiveTraceParams(newParams);
+ }
+}
diff --git a/src/test/java/com/spotify/github/tracing/OpenTelemetrySpanTest.java b/src/test/java/com/spotify/github/tracing/OpenTelemetrySpanTest.java
new file mode 100644
index 00000000..41223174
--- /dev/null
+++ b/src/test/java/com/spotify/github/tracing/OpenTelemetrySpanTest.java
@@ -0,0 +1,117 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2021 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.github.tracing;
+
+import com.spotify.github.tracing.opentelemetry.OpenTelemetrySpan;
+import com.spotify.github.v3.exceptions.RequestNotOkException;
+import io.opentelemetry.api.trace.StatusCode;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.mockito.Mockito.*;
+
+class OpenTelemetrySpanTest {
+ private final io.opentelemetry.api.trace.Span wrapped =
+ mock(io.opentelemetry.api.trace.Span.class);
+
+ @Test
+ public void succeed() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.success();
+ span.close();
+
+ verify(wrapped).setStatus(StatusCode.OK);
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void fail() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.failure(
+ new RequestNotOkException("method", "path", 404, "Not found", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(StatusCode.ERROR);
+ verify(wrapped).setAttribute("http.status_code", 404);
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void failOnServerError() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.failure(
+ new RequestNotOkException(
+ "method", "path", 500, "Internal Server Error", Collections.emptyMap()));
+ span.close();
+
+ verify(wrapped).setStatus(StatusCode.ERROR);
+ verify(wrapped).setAttribute("http.status_code", 500);
+ verify(wrapped).setAttribute("error", true);
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void failWithNullThrowable() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.failure(null);
+ span.close();
+
+ verify(wrapped).setStatus(StatusCode.ERROR);
+ verify(wrapped, never()).setAttribute(anyString(), any());
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void failWithNonRequestNotOkException() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.failure(new RuntimeException("Unexpected error"));
+ span.close();
+
+ verify(wrapped).setStatus(StatusCode.ERROR);
+ verify(wrapped, never()).setAttribute("http.status_code", 404);
+ verify(wrapped).setAttribute("error", true);
+ verify(wrapped).end();
+ }
+
+ @Test
+ public void addTags() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.addTag("key", "value");
+ span.addTag("key", true);
+ span.addTag("key", 42L);
+ span.close();
+
+ verify(wrapped).setAttribute("key", "value");
+ verify(wrapped).setAttribute("key", true);
+ verify(wrapped).setAttribute("key", 42L);
+ }
+
+ @Test
+ public void addEvent() {
+ final Span span = new OpenTelemetrySpan(wrapped);
+ span.addEvent("description");
+ span.close();
+
+ verify(wrapped).addEvent("description");
+ verify(wrapped).end();
+ }
+}
diff --git a/src/test/java/com/spotify/github/tracing/OpenTelemetryTracerTest.java b/src/test/java/com/spotify/github/tracing/OpenTelemetryTracerTest.java
new file mode 100644
index 00000000..3cf82360
--- /dev/null
+++ b/src/test/java/com/spotify/github/tracing/OpenTelemetryTracerTest.java
@@ -0,0 +1,199 @@
+/*-
+ * -\-\-
+ * github-client
+ * --
+ * Copyright (C) 2016 - 2021 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.github.tracing;
+
+import com.spotify.github.tracing.opentelemetry.OpenTelemetryTracer;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.sdk.trace.samplers.Sampler;
+import okhttp3.*;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OpenTelemetryTracerTest {
+
+ private final String rootSpanName = "root span";
+ private static OtTestExportHandler spanExporterHandler;
+ private final OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
+ private final Tracer tracer = openTelemetry.getTracer("github-java-client-test");
+
+ /**
+ * Test that trace() a) returns a future that completes when the input future completes and b)
+ * sets up the Spans appropriately so that the Span for the operation is exported with the
+ * rootSpan set as the parent.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageSimple(final String requestMethod) throws Exception {
+ Span rootSpan = startRootSpan();
+ final CompletableFuture future = new CompletableFuture<>();
+ OpenTelemetryTracer tracer = new OpenTelemetryTracer();
+
+ tracer.span("path", requestMethod, future);
+ future.complete("all done");
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getSpanContext().getTraceId(), inner.getSpanContext().getTraceId());
+ assertEquals(root.getSpanContext().getSpanId(), inner.getParentSpanId());
+ final Attributes attributes = inner.getAttributes();
+ assertEquals("github-api-client", attributes.get(AttributeKey.stringKey("component")));
+ assertEquals("github", attributes.get(AttributeKey.stringKey("peer.service")));
+ assertEquals("path", attributes.get(AttributeKey.stringKey("http.url")));
+ assertEquals(requestMethod, attributes.get(AttributeKey.stringKey("method")));
+ assertEquals(StatusCode.OK, inner.getStatus().getStatusCode());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageFails(final String requestMethod) throws Exception {
+ Span rootSpan = startRootSpan();
+ final CompletableFuture future = new CompletableFuture<>();
+ OpenTelemetryTracer tracer = new OpenTelemetryTracer();
+
+ tracer.span("path", requestMethod, future);
+ future.completeExceptionally(new Exception("GitHub failed!"));
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getSpanContext().getTraceId(), inner.getSpanContext().getTraceId());
+ assertEquals(root.getSpanContext().getSpanId(), inner.getParentSpanId());
+ final Attributes attributes = inner.getAttributes();
+ assertEquals("github-api-client", attributes.get(AttributeKey.stringKey("component")));
+ assertEquals("github", attributes.get(AttributeKey.stringKey("peer.service")));
+ assertEquals("path", attributes.get(AttributeKey.stringKey("http.url")));
+ assertEquals(requestMethod, attributes.get(AttributeKey.stringKey("method")));
+ assertEquals(StatusCode.ERROR, inner.getStatus().getStatusCode());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"GET", "POST", "PUT", "DELETE"})
+ public void traceCompletionStageWithRequest(final String requestMethod) throws Exception {
+ Span rootSpan = startRootSpan();
+ final CompletableFuture future = new CompletableFuture<>();
+ OpenTelemetryTracer tracer = new OpenTelemetryTracer();
+ Request mockRequest = mock(Request.class);
+ when(mockRequest.url())
+ .thenReturn(HttpUrl.parse("https://api.github.com/repos/spotify/github-java-client"));
+ when(mockRequest.method()).thenReturn(requestMethod);
+
+ try (com.spotify.github.tracing.Span span = tracer.span(mockRequest)) {
+ tracer.attachSpanToFuture(span, future);
+ future.complete("all done");
+ }
+ rootSpan.end();
+
+ List exportedSpans = spanExporterHandler.waitForSpansToBeExported(2);
+ assertEquals(2, exportedSpans.size());
+
+ SpanData root = findSpan(exportedSpans, rootSpanName);
+ SpanData inner = findSpan(exportedSpans, "GitHub Request");
+
+ assertEquals(root.getSpanContext().getTraceId(), inner.getSpanContext().getTraceId());
+ assertEquals(root.getSpanContext().getSpanId(), inner.getParentSpanId());
+ final Attributes attributes = inner.getAttributes();
+ assertEquals("github-api-client", attributes.get(AttributeKey.stringKey("component")));
+ assertEquals("github", attributes.get(AttributeKey.stringKey("peer.service")));
+ assertEquals(
+ "https://api.github.com/repos/spotify/github-java-client",
+ attributes.get(AttributeKey.stringKey("http.url")));
+ assertEquals(requestMethod, attributes.get(AttributeKey.stringKey("method")));
+ assertEquals(StatusCode.OK, inner.getStatus().getStatusCode());
+ }
+
+ @Test
+ public void createTracedClient() throws IOException {
+ OpenTelemetryTracer tracer = new OpenTelemetryTracer(openTelemetry);
+ OkHttpClient.Builder mockBuilder = mock(OkHttpClient.Builder.class);
+ OkHttpClient mockClient = mock(OkHttpClient.class);
+ LinkedList interceptors = new LinkedList<>();
+ when(mockClient.newBuilder()).thenReturn(mockBuilder);
+ when(mockBuilder.build()).thenReturn(mockClient);
+ when(mockBuilder.interceptors()).thenReturn(interceptors);
+ when(mockBuilder.networkInterceptors()).thenReturn(interceptors);
+ Call.Factory callFactory = tracer.createTracedClient(mockClient);
+ assertNotNull(callFactory);
+ assertEquals(
+ "class io.opentelemetry.instrumentation.okhttp.v3_0.TracingCallFactory",
+ callFactory.getClass().toString());
+ assertEquals(3, interceptors.size());
+ }
+
+ private Span startRootSpan() {
+ Span rootSpan = tracer.spanBuilder(rootSpanName).startSpan();
+ Context context = Context.current().with(rootSpan);
+ context.makeCurrent();
+ return rootSpan;
+ }
+
+ private SpanData findSpan(final List spans, final String name) {
+ return spans.stream().filter(s -> s.getName().equals(name)).findFirst().get();
+ }
+
+ @AfterEach
+ public void flushSpans() {
+ spanExporterHandler.flush();
+ }
+
+ @BeforeAll
+ public static void setupTracing() {
+ spanExporterHandler = new OtTestExportHandler();
+ SdkTracerProvider tracerProvider =
+ SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(spanExporterHandler))
+ .setSampler(Sampler.alwaysOn())
+ .build();
+ OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal();
+ }
+}
diff --git a/src/test/java/com/spotify/github/tracing/OtTestExportHandler.java b/src/test/java/com/spotify/github/tracing/OtTestExportHandler.java
new file mode 100644
index 00000000..5e68285f
--- /dev/null
+++ b/src/test/java/com/spotify/github/tracing/OtTestExportHandler.java
@@ -0,0 +1,93 @@
+/*-
+ * -\-\-
+ * github-client
+ * --
+ * Copyright (C) 2016 - 2021 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.github.tracing;
+
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A dummy SpanExporter.Handler which keeps any exported Spans in memory, so we can query against
+ * them in tests.
+ *
+ * The opencensus-testing library has a TestHandler that can be used in tests like this, but the
+ * only method it exposes to gain access to the received spans is waitForExport(int) which blocks
+ * forever until the given number of spans is exported, which could be never. So instead we define
+ * our own very simple implementation.
+ */
+class OtTestExportHandler implements SpanExporter {
+ private static final Logger LOG = LoggerFactory.getLogger(OtTestExportHandler.class);
+
+ private final List receivedSpans = new ArrayList<>();
+ private final Object lock = new Object();
+ @Override
+ public CompletableResultCode export(Collection spanDataList) {
+ synchronized (lock) {
+ receivedSpans.addAll(spanDataList);
+ LOG.info("received {} spans, {} total", spanDataList.size(), receivedSpans.size());
+ }
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode flush() {
+ this.receivedSpans.clear();
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode shutdown() {
+ return CompletableResultCode.ofSuccess();
+ }
+
+ List receivedSpans() {
+ synchronized (lock) {
+ return new ArrayList<>(receivedSpans);
+ }
+ }
+
+ /** Wait up to waitTime for at least `count` spans to be exported */
+ List waitForSpansToBeExported(final int count) throws InterruptedException {
+ Duration waitTime = Duration.ofSeconds(7);
+ Instant deadline = Instant.now().plus(waitTime);
+
+ List spanData = receivedSpans();
+ while (spanData.size() < count) {
+ //noinspection BusyWait
+ Thread.sleep(100);
+ spanData = receivedSpans();
+
+ if (!Instant.now().isBefore(deadline)) {
+ LOG.warn("ending busy wait for spans because deadline passed");
+ break;
+ }
+ }
+ return spanData;
+ }
+}
diff --git a/src/test/java/com/spotify/github/v3/clients/GitHubClientTest.java b/src/test/java/com/spotify/github/v3/clients/GitHubClientTest.java
index 5a710fdb..5a9edc45 100644
--- a/src/test/java/com/spotify/github/v3/clients/GitHubClientTest.java
+++ b/src/test/java/com/spotify/github/v3/clients/GitHubClientTest.java
@@ -7,9 +7,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -33,17 +33,15 @@
import static org.mockito.Mockito.*;
import com.google.common.io.Resources;
-import com.spotify.github.Tracer;
+import com.spotify.github.tracing.Span;
+import com.spotify.github.tracing.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;
import com.spotify.github.v3.repos.RepositoryInvitation;
-import com.spotify.github.v3.workflows.WorkflowsResponse;
-import com.spotify.github.v3.workflows.WorkflowsState;
-import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -52,6 +50,9 @@
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+
+import com.spotify.github.v3.workflows.WorkflowsResponse;
+import com.spotify.github.v3.workflows.WorkflowsState;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Headers;
@@ -61,6 +62,7 @@
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
+import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -70,7 +72,8 @@ public class GitHubClientTest {
private GitHubClient github;
private OkHttpClient client;
- private Tracer tracer = mock(Tracer.class);
+ private final Tracer tracer = mock(Tracer.class);
+ private final Span mockSpan = mock(Span.class);
private static String getFixture(String resource) throws IOException {
return Resources.toString(getResource(GitHubClientTest.class, resource), defaultCharset());
@@ -80,6 +83,7 @@ private static String getFixture(String resource) throws IOException {
public void setUp() {
client = mock(OkHttpClient.class);
github = GitHubClient.create(client, URI.create("http://bogus"), "token");
+ when(tracer.span(any())).thenReturn(mockSpan);
}
@Test
@@ -89,7 +93,9 @@ public void withScopedInstallationIdShouldFailWhenMissingPrivateKey() {
@Test
public void testWithScopedInstallationId() throws URISyntaxException {
- GitHubClient org = GitHubClient.create(new URI("http://apa.bepa.cepa"), "some_key_content".getBytes(), null, null);
+ GitHubClient org =
+ GitHubClient.create(
+ new URI("http://apa.bepa.cepa"), "some_key_content".getBytes(), null, null);
GitHubClient scoped = org.withScopeForInstallationId(1);
Assertions.assertTrue(scoped.getPrivateKey().isPresent());
Assertions.assertEquals(org.getPrivateKey().get(), scoped.getPrivateKey().get());
@@ -123,15 +129,23 @@ public void testSearchIssue() throws Throwable {
.build();
when(client.newCall(any())).thenReturn(call);
+ when(tracer.createTracedClient(client))
+ .thenReturn(
+ new Call.Factory() {
+ @NotNull
+ @Override
+ public Call newCall(@NotNull final Request request) {
+ return call;
+ }
+ });
IssueClient issueClient =
github.withTracer(tracer).createRepositoryClient("testorg", "testrepo").createIssueClient();
CompletableFuture maybeSucceeded = issueClient.editComment(1, "some comment");
capture.getValue().onResponse(call, response);
- verify(tracer,times(1)).span(anyString(), anyString(),any());
+ verify(tracer, times(1)).span(any(Request.class));
- Exception exception = assertThrows(ExecutionException.class,
- maybeSucceeded::get);
+ Exception exception = assertThrows(ExecutionException.class, maybeSucceeded::get);
Assertions.assertEquals(ReadOnlyRepositoryException.class, exception.getCause().getClass());
}
@@ -141,18 +155,17 @@ public void testRequestNotOkException() throws Throwable {
final ArgumentCaptor capture = ArgumentCaptor.forClass(Callback.class);
doNothing().when(call).enqueue(capture.capture());
- final Response response = new okhttp3.Response.Builder()
- .code(409) // Conflict
- .headers(Headers.of("x-ratelimit-remaining", "0"))
- .body(
- ResponseBody.create(
- MediaType.get("application/json"),
- "{\n \"message\": \"Merge Conflict\"\n}"
- ))
- .message("")
- .protocol(Protocol.HTTP_1_1)
- .request(new Request.Builder().url("http://localhost/").build())
- .build();
+ final Response response =
+ new okhttp3.Response.Builder()
+ .code(409) // Conflict
+ .headers(Headers.of("x-ratelimit-remaining", "0"))
+ .body(
+ ResponseBody.create(
+ MediaType.get("application/json"), "{\n \"message\": \"Merge Conflict\"\n}"))
+ .message("")
+ .protocol(Protocol.HTTP_1_1)
+ .request(new Request.Builder().url("http://localhost/").build())
+ .build();
when(client.newCall(any())).thenReturn(call);
RepositoryClient repoApi = github.createRepositoryClient("testorg", "testrepo");
@@ -196,8 +209,8 @@ public void testPutConvertsToClass() throws Throwable {
.request(new Request.Builder().url("http://localhost/").build())
.build();
- CompletableFuture future = github.put("collaborators/", "",
- RepositoryInvitation.class);
+ CompletableFuture future =
+ github.put("collaborators/", "", RepositoryInvitation.class);
callbackCapture.getValue().onResponse(call, response);
RepositoryInvitation invitation = future.get();
@@ -213,15 +226,17 @@ public void testGetCheckSuites() throws Throwable {
final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(Callback.class);
doNothing().when(call).enqueue(callbackCapture.capture());
- final Response response = new okhttp3.Response.Builder()
- .code(200)
- .body(
- ResponseBody.create(
- MediaType.get("application/json"), getFixture("../checks/check-suites-response.json")))
- .message("")
- .protocol(Protocol.HTTP_1_1)
- .request(new Request.Builder().url("http://localhost/").build())
- .build();
+ final Response response =
+ new okhttp3.Response.Builder()
+ .code(200)
+ .body(
+ ResponseBody.create(
+ MediaType.get("application/json"),
+ getFixture("../checks/check-suites-response.json")))
+ .message("")
+ .protocol(Protocol.HTTP_1_1)
+ .request(new Request.Builder().url("http://localhost/").build())
+ .build();
when(client.newCall(any())).thenReturn(call);
ChecksClient client = github.createChecksClient("testorg", "testrepo");
@@ -232,7 +247,6 @@ public void testGetCheckSuites() throws Throwable {
assertThat(result.totalCount(), is(1));
assertThat(result.checkSuites().get(0).app().get().slug().get(), is("octoapp"));
-
}
@Test
@@ -241,20 +255,35 @@ public void testGetWorkflow() throws Throwable {
final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(Callback.class);
doNothing().when(call).enqueue(callbackCapture.capture());
- final Response response = new okhttp3.Response.Builder()
- .code(200)
- .body(
- ResponseBody.create(
- MediaType.get("application/json"),
- getFixture("../workflows/workflows-get-workflow-response.json")))
- .message("")
- .protocol(Protocol.HTTP_1_1)
- .request(new Request.Builder().url("http://localhost/").build())
- .build();
+ final Response response =
+ new okhttp3.Response.Builder()
+ .code(200)
+ .body(
+ ResponseBody.create(
+ MediaType.get("application/json"),
+ getFixture("../workflows/workflows-get-workflow-response.json")))
+ .message("")
+ .protocol(Protocol.HTTP_1_1)
+ .request(new Request.Builder().url("http://localhost/").build())
+ .build();
+
+ when(tracer.createTracedClient(any(OkHttpClient.class)))
+ .thenReturn(
+ new Call.Factory() {
+ @NotNull
+ @Override
+ public Call newCall(@NotNull final Request request) {
+ return call;
+ }
+ });
when(client.newCall(any())).thenReturn(call);
- WorkflowsClient client = github.withTracer(tracer).createRepositoryClient("testorg", "testrepo")
- .createActionsClient().createWorkflowsClient();
+ WorkflowsClient client =
+ github
+ .withTracer(tracer)
+ .createRepositoryClient("testorg", "testrepo")
+ .createActionsClient()
+ .createWorkflowsClient();
CompletableFuture future = client.getWorkflow(161335);
callbackCapture.getValue().onResponse(call, response);
@@ -274,7 +303,8 @@ void asAppScopedClientGetsUserClientIfOrgClientNotFound() {
var appClientMock = mock(GithubAppClient.class);
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
- when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
+ when(appClientMock.getInstallation())
+ .thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
var userClientMock = mock(UserClient.class);
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);
@@ -303,7 +333,8 @@ void asAppScopedClientReturnsEmptyIfNoInstallation() {
var appClientMock = mock(GithubAppClient.class);
when(orgClientMock.createGithubAppClient()).thenReturn(appClientMock);
- when(appClientMock.getInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
+ when(appClientMock.getInstallation())
+ .thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
var userClientMock = mock(UserClient.class);
when(githubSpy.createUserClient("owner")).thenReturn(userClientMock);
@@ -312,7 +343,8 @@ void asAppScopedClientReturnsEmptyIfNoInstallation() {
when(userClientMock.createGithubAppClient()).thenReturn(appClientMock2);
var installationMock = mock(Installation.class);
- when(appClientMock2.getUserInstallation()).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
+ when(appClientMock2.getUserInstallation())
+ .thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>())));
when(installationMock.id()).thenReturn(1);
var maybeScopedClient = githubSpy.asAppScopedClient("owner").toCompletableFuture().join();