Skip to content

Commit a1eae86

Browse files
authored
Feat/add authenticated app endpoint (#251)
* feat: add GET /app endpoint to retrieve authenticated app details * refactor: add requireOwner helper for better error messages Replaces Optional.get() calls with requireOwner() helper that provides clear error messages when owner-dependent methods are called on app-level clients without owner context. * format * try update maven enforcer
1 parent ef607b0 commit a1eae86

File tree

7 files changed

+187
-18
lines changed

7 files changed

+187
-18
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ target
1111
.idea
1212
*.iml
1313

14+
## VS Code
15+
.vscode
16+
1417
## Logs
1518
*.log
1619

@@ -29,4 +32,4 @@ pom.xml.releaseBackup
2932
release.properties
3033

3134
# macOS
32-
.DS_Store
35+
.DS_Store

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@
457457
</plugin>
458458
<plugin>
459459
<artifactId>maven-enforcer-plugin</artifactId>
460-
<version>1.4.1</version>
460+
<version>3.6.2</version>
461461
<executions>
462462
<execution>
463463
<id>enforce</id>

src/main/java/com/spotify/github/v3/checks/App.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,6 +22,7 @@
2222

2323
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2424
import com.spotify.github.GithubStyle;
25+
import com.spotify.github.v3.User;
2526
import java.time.ZonedDateTime;
2627
import java.util.List;
2728
import java.util.Map;
@@ -112,4 +113,53 @@ public interface App {
112113
* @return the optional count
113114
*/
114115
Optional<Integer> installationsCount();
116+
117+
/**
118+
* The client ID of the GitHub App.
119+
*
120+
* @return the optional client ID
121+
*/
122+
Optional<String> clientId();
123+
124+
/**
125+
* The name of the single file the GitHub App can access (if applicable).
126+
*
127+
* @return the optional single file name
128+
*/
129+
Optional<String> singleFileName();
130+
131+
/**
132+
* Whether the GitHub App has access to multiple single files.
133+
*
134+
* @return the optional boolean
135+
*/
136+
Optional<Boolean> hasMultipleSingleFiles();
137+
138+
/**
139+
* The list of single file paths the GitHub App can access.
140+
*
141+
* @return the optional list of file paths
142+
*/
143+
Optional<List<String>> singleFilePaths();
144+
145+
/**
146+
* The slug name of the GitHub App.
147+
*
148+
* @return the optional app slug
149+
*/
150+
Optional<String> appSlug();
151+
152+
/**
153+
* The date the App was suspended.
154+
*
155+
* @return the optional suspended date
156+
*/
157+
Optional<ZonedDateTime> suspendedAt();
158+
159+
/**
160+
* The user who suspended the App.
161+
*
162+
* @return the optional user
163+
*/
164+
Optional<User> suspendedBy();
115165
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,15 @@ public UserClient createUserClient(final String owner) {
588588
return UserClient.create(this, owner);
589589
}
590590

591+
/**
592+
* Create GitHub App API client
593+
*
594+
* @return GitHub App API client
595+
*/
596+
public GithubAppClient createGithubAppClient() {
597+
return new GithubAppClient(this);
598+
}
599+
591600
Json json() {
592601
return json;
593602
}
@@ -1017,7 +1026,9 @@ private CompletableFuture<String> getAuthorizationHeader(final String path) {
10171026
}
10181027

10191028
private boolean isJwtRequest(final String path) {
1020-
return path.startsWith("/app/installation") || path.endsWith("installation");
1029+
return path.equals("/app")
1030+
|| path.startsWith("/app/installation")
1031+
|| path.endsWith("installation");
10211032
}
10221033

10231034
/**

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,6 +24,7 @@
2424
import com.google.common.collect.ImmutableMap;
2525
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
2626
import com.spotify.github.v3.checks.AccessToken;
27+
import com.spotify.github.v3.checks.App;
2728
import com.spotify.github.v3.checks.Installation;
2829
import java.util.List;
2930
import java.util.Map;
@@ -34,6 +35,7 @@
3435
/** Apps API client */
3536
public class GithubAppClient {
3637

38+
private static final String GET_AUTHENTICATED_APP_URL = "/app";
3739
private static final String GET_INSTALLATION_BY_ID_URL = "/app/installations/%s";
3840
private static final String GET_ACCESS_TOKEN_URL = "/app/installations/%s/access_tokens";
3941
private static final String GET_INSTALLATIONS_URL = "/app/installations?per_page=100";
@@ -48,7 +50,7 @@ public class GithubAppClient {
4850
private static final String GET_INSTALLATION_USER_URL = "/users/%s/installation";
4951

5052
private final GitHubClient github;
51-
private final String owner;
53+
private final Optional<String> maybeOwner;
5254
private final Optional<String> maybeRepo;
5355

5456
private final Map<String, String> extraHeaders =
@@ -59,16 +61,38 @@ public class GithubAppClient {
5961

6062
GithubAppClient(final GitHubClient github, final String owner, final String repo) {
6163
this.github = github;
62-
this.owner = owner;
64+
this.maybeOwner = Optional.of(owner);
6365
this.maybeRepo = Optional.of(repo);
6466
}
6567

6668
GithubAppClient(final GitHubClient github, final String owner) {
6769
this.github = github;
68-
this.owner = owner;
70+
this.maybeOwner = Optional.of(owner);
6971
this.maybeRepo = Optional.empty();
7072
}
7173

74+
GithubAppClient(final GitHubClient github) {
75+
this.github = github;
76+
this.maybeOwner = Optional.empty();
77+
this.maybeRepo = Optional.empty();
78+
}
79+
80+
/**
81+
* Gets the owner, throwing a descriptive exception if not present.
82+
*
83+
* @return the owner string
84+
* @throws IllegalStateException if owner is not present
85+
*/
86+
private String requireOwner() {
87+
return maybeOwner.orElseThrow(
88+
() ->
89+
new IllegalStateException(
90+
"This operation requires an owner context. "
91+
+ "Use GitHubClient.createOrganisationClient(owner).createGithubAppClient() "
92+
+ "or GitHubClient.createRepositoryClient(owner, repo).createGithubAppClient() "
93+
+ "instead of GitHubClient.createGithubAppClient()"));
94+
}
95+
7296
/**
7397
* List Installations of an app.
7498
*
@@ -99,29 +123,32 @@ public CompletableFuture<Installation> getInstallation(final Integer installatio
99123

100124
/**
101125
* Get an installation of a repo
126+
*
102127
* @return an Installation
103128
*/
104129
private CompletableFuture<Installation> getRepoInstallation(final String repo) {
105130
return github.request(
106-
String.format(GET_INSTALLATION_REPO_URL, owner, repo), Installation.class);
131+
String.format(GET_INSTALLATION_REPO_URL, requireOwner(), repo), Installation.class);
107132
}
108133

109134
/**
110135
* Get an installation of an org
136+
*
111137
* @return an Installation
112138
*/
113139
private CompletableFuture<Installation> getOrgInstallation() {
114140
return github.request(
115-
String.format(GET_INSTALLATION_ORG_URL, owner), Installation.class);
141+
String.format(GET_INSTALLATION_ORG_URL, requireOwner()), Installation.class);
116142
}
117143

118-
/**
144+
/**
119145
* Get an installation of a user
146+
*
120147
* @return an Installation
121148
*/
122149
public CompletableFuture<Installation> getUserInstallation() {
123150
return github.request(
124-
String.format(GET_INSTALLATION_USER_URL, owner), Installation.class);
151+
String.format(GET_INSTALLATION_USER_URL, requireOwner()), Installation.class);
125152
}
126153

127154
/**
@@ -146,4 +173,17 @@ public CompletableFuture<InstallationRepositoriesResponse> listAccessibleReposit
146173
return GitHubClient.scopeForInstallationId(github, installationId)
147174
.request(LIST_ACCESSIBLE_REPOS_URL, InstallationRepositoriesResponse.class, extraHeaders);
148175
}
176+
177+
/**
178+
* Get the authenticated GitHub App.
179+
*
180+
* <p>Returns the authenticated app. You must use a JWT to access this endpoint.
181+
*
182+
* <p>see https://docs.github.com/en/rest/apps/apps#get-the-authenticated-app
183+
*
184+
* @return the authenticated App
185+
*/
186+
public CompletableFuture<App> getAuthenticatedApp() {
187+
return github.request(GET_AUTHENTICATED_APP_URL, App.class);
188+
}
149189
}

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,6 +30,7 @@
3030
import com.google.common.io.Resources;
3131
import com.spotify.github.FixtureHelper;
3232
import com.spotify.github.v3.apps.InstallationRepositoriesResponse;
33+
import com.spotify.github.v3.checks.App;
3334
import com.spotify.github.v3.checks.Installation;
3435
import java.io.File;
3536
import java.io.IOException;
@@ -110,8 +111,7 @@ public void listAccessibleRepositories() throws Exception {
110111
.setResponseCode(200)
111112
.setBody(FixtureHelper.loadFixture("githubapp/accessible-repositories.json")));
112113

113-
InstallationRepositoriesResponse response =
114-
client.listAccessibleRepositories(1234).join();
114+
InstallationRepositoriesResponse response = client.listAccessibleRepositories(1234).join();
115115

116116
assertThat(response.totalCount(), is(2));
117117
assertThat(response.repositories().size(), is(2));
@@ -160,4 +160,26 @@ public void getInstallationByInstallationId() throws Exception {
160160
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
161161
assertThat(recordedRequest.getRequestUrl().encodedPath(), is("/app/installations/1234"));
162162
}
163+
164+
@Test
165+
public void getAuthenticatedApp() throws Exception {
166+
mockServer.enqueue(
167+
new MockResponse()
168+
.setResponseCode(200)
169+
.setBody(FixtureHelper.loadFixture("githubapp/authenticated-app.json")));
170+
171+
App app = client.getAuthenticatedApp().join();
172+
173+
assertThat(app.id(), is(1));
174+
assertThat(app.slug().get(), is("octoapp"));
175+
assertThat(app.name(), is("Octocat App"));
176+
assertThat(app.clientId().get(), is("Iv1.8a61f9b3a7aba766"));
177+
assertThat(app.installationsCount().get(), is(5));
178+
179+
RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS);
180+
assertThat(recordedRequest.getRequestUrl().encodedPath(), is("/app"));
181+
assertThat(
182+
recordedRequest.getHeaders().values("Authorization").get(0).startsWith("Bearer "),
183+
is(true));
184+
}
163185
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"id": 1,
3+
"slug": "octoapp",
4+
"node_id": "MDExOkludGVncmF0aW9uMQ==",
5+
"owner": {
6+
"login": "github",
7+
"id": 1,
8+
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE=",
9+
"url": "https://api.github.com/orgs/github",
10+
"repos_url": "https://api.github.com/orgs/github/repos",
11+
"events_url": "https://api.github.com/orgs/github/events",
12+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
13+
"gravatar_id": "",
14+
"html_url": "https://github.com/github",
15+
"followers_url": "https://api.github.com/users/github/followers",
16+
"following_url": "https://api.github.com/users/github/following{/other_user}",
17+
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
18+
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
19+
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
20+
"organizations_url": "https://api.github.com/users/github/orgs",
21+
"received_events_url": "https://api.github.com/users/github/received_events",
22+
"type": "User",
23+
"site_admin": true
24+
},
25+
"name": "Octocat App",
26+
"description": "Test GitHub App",
27+
"external_url": "https://example.com",
28+
"html_url": "https://github.com/apps/octoapp",
29+
"created_at": "2017-07-08T16:18:44-04:00",
30+
"updated_at": "2017-07-08T16:18:44-04:00",
31+
"permissions": {
32+
"metadata": "read",
33+
"contents": "read",
34+
"issues": "write",
35+
"single_file": "write"
36+
},
37+
"events": ["push", "pull_request"],
38+
"installations_count": 5,
39+
"client_id": "Iv1.8a61f9b3a7aba766",
40+
"client_secret": "1726be1638095a19edd134c77bde3aa2ece1e5d8",
41+
"webhook_secret": "e340154128314309424b7c8e90325147d99fdafa",
42+
"pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuEPzOUE+kiEH1WLiMeBytTEF856j0hgg...\n-----END RSA PRIVATE KEY-----"
43+
}

0 commit comments

Comments
 (0)