Skip to content

Commit 77f7f81

Browse files
authored
Merge pull request #1630 from solven-eu/EnableFetchPlanForCurrentAppAndAccount
Enable fetch plan for current app and account
2 parents 70872aa + e09bd39 commit 77f7f81

File tree

14 files changed

+459
-27
lines changed

14 files changed

+459
-27
lines changed

src/main/java/org/kohsuke/github/GHAppInstallation.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,25 @@ public GHAppCreateTokenBuilder createToken(Map<String, GHPermissionType> permiss
361361
public GHAppCreateTokenBuilder createToken() {
362362
return new GHAppCreateTokenBuilder(root(), String.format("/app/installations/%d/access_tokens", getId()));
363363
}
364+
365+
/**
366+
* Shows whether the user or organization account actively subscribes to a plan listed by the authenticated GitHub
367+
* App. When someone submits a plan change that won't be processed until the end of their billing cycle, you will
368+
* also see the upcoming pending change.
369+
*
370+
* <p>
371+
* GitHub Apps must use a JWT to access this endpoint.
372+
* <p>
373+
* OAuth Apps must use basic authentication with their client ID and client secret to access this endpoint.
374+
*
375+
* @return a GHMarketplaceAccountPlan instance
376+
* @throws IOException
377+
* it may throw an {@link IOException}
378+
* @see <a href=
379+
* "https://docs.github.com/en/rest/apps/marketplace?apiVersion=2022-11-28#get-a-subscription-plan-for-an-account">Get
380+
* a subscription plan for an account</a>
381+
*/
382+
public GHMarketplaceAccountPlan getMarketplaceAccount() throws IOException {
383+
return new GHMarketplacePlanForAccountBuilder(root(), account.getId()).createRequest();
384+
}
364385
}

src/main/java/org/kohsuke/github/GHMarketplaceAccount.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.kohsuke.github;
22

3+
import java.io.IOException;
34
import java.net.URL;
45

56
// TODO: Auto-generated Javadoc
@@ -72,4 +73,25 @@ public GHMarketplaceAccountType getType() {
7273
return type;
7374
}
7475

76+
/**
77+
* Shows whether the user or organization account actively subscribes to a plan listed by the authenticated GitHub
78+
* App. When someone submits a plan change that won't be processed until the end of their billing cycle, you will
79+
* also see the upcoming pending change.
80+
*
81+
* <p>
82+
* GitHub Apps must use a JWT to access this endpoint.
83+
* <p>
84+
* OAuth Apps must use basic authentication with their client ID and client secret to access this endpoint.
85+
*
86+
* @return a GHMarketplaceListAccountBuilder instance
87+
* @throws IOException
88+
* in case of {@link IOException}
89+
* @see <a href=
90+
* "https://docs.github.com/en/rest/apps/marketplace?apiVersion=2022-11-28#get-a-subscription-plan-for-an-account">Get
91+
* a subscription plan for an account</a>
92+
*/
93+
public GHMarketplaceAccountPlan getPlan() throws IOException {
94+
return new GHMarketplacePlanForAccountBuilder(root(), this.id).createRequest();
95+
}
96+
7597
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.kohsuke.github;
2+
3+
import java.io.IOException;
4+
5+
// TODO: Auto-generated Javadoc
6+
/**
7+
* Returns the plan associated with current account.
8+
*
9+
* @author Benoit Lacelle
10+
* @see GHMarketplacePlan#listAccounts()
11+
* @see GitHub#listMarketplacePlans()
12+
*/
13+
public class GHMarketplacePlanForAccountBuilder extends GitHubInteractiveObject {
14+
private final Requester builder;
15+
private final long accountId;
16+
17+
/**
18+
* Instantiates a new GH marketplace list account builder.
19+
*
20+
* @param root
21+
* the root
22+
* @param accountId
23+
* the account id
24+
*/
25+
GHMarketplacePlanForAccountBuilder(GitHub root, long accountId) {
26+
super(root);
27+
this.builder = root.createRequest();
28+
this.accountId = accountId;
29+
}
30+
31+
/**
32+
* Fetch the plan associated with the account specified on construction.
33+
* <p>
34+
* GitHub Apps must use a JWT to access this endpoint.
35+
*
36+
* @return a GHMarketplaceAccountPlan
37+
* @throws IOException
38+
* on error
39+
*/
40+
public GHMarketplaceAccountPlan createRequest() throws IOException {
41+
return builder.withUrlPath(String.format("/marketplace_listing/accounts/%d", this.accountId))
42+
.fetch(GHMarketplaceAccountPlan.class);
43+
}
44+
45+
}

src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.jsonwebtoken.JwtBuilder;
44
import io.jsonwebtoken.Jwts;
55
import io.jsonwebtoken.SignatureAlgorithm;
6+
import io.jsonwebtoken.jackson.io.JacksonSerializer;
67
import org.kohsuke.github.authorization.AuthorizationProvider;
78

89
import java.io.File;
@@ -181,7 +182,7 @@ private String refreshJWT() {
181182
validUntil = expiration.minus(Duration.ofMinutes(2));
182183

183184
// Builds the JWT and serializes it to a compact, URL-safe string
184-
return builder.compact();
185+
return builder.serializeToJsonWith(new JacksonSerializer<>()).compact();
185186
}
186187

187188
Instant getIssuedAt(Instant now) {

src/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.kohsuke.github;
22

3+
import com.google.common.collect.ImmutableSet;
34
import io.jsonwebtoken.Jwts;
45
import org.apache.commons.io.IOUtils;
56
import org.kohsuke.github.authorization.AuthorizationProvider;
@@ -9,6 +10,7 @@
910
import java.io.IOException;
1011
import java.nio.charset.StandardCharsets;
1112
import java.nio.file.Files;
13+
import java.nio.file.Paths;
1214
import java.security.GeneralSecurityException;
1315
import java.security.KeyFactory;
1416
import java.security.PrivateKey;
@@ -17,13 +19,19 @@
1719
import java.time.temporal.ChronoUnit;
1820
import java.util.Base64;
1921
import java.util.Date;
22+
import java.util.List;
2023

2124
// TODO: Auto-generated Javadoc
2225
/**
2326
* The Class AbstractGHAppInstallationTest.
2427
*/
2528
public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
2629

30+
private static String ENV_GITHUB_APP_ID = "GITHUB_APP_ID";
31+
private static String ENV_GITHUB_APP_JWK_PATH = "GITHUB_APP_JWK_PATH";
32+
private static String ENV_GITHUB_APP_ORG = "GITHUB_APP_ORG";
33+
private static String ENV_GITHUB_APP_REPO = "GITHUB_APP_REPO";
34+
2735
private static String TEST_APP_ID_1 = "82994";
2836
private static String TEST_APP_ID_2 = "83009";
2937
private static String TEST_APP_ID_3 = "89368";
@@ -44,16 +52,23 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
4452
* Instantiates a new abstract GH app installation test.
4553
*/
4654
protected AbstractGHAppInstallationTest() {
55+
String appId = System.getenv(ENV_GITHUB_APP_ID);
56+
String appJwkPath = System.getenv(ENV_GITHUB_APP_JWK_PATH);
4757
try {
48-
jwtProvider1 = new JWTTokenProvider(TEST_APP_ID_1,
49-
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_1).getFile()));
50-
jwtProvider2 = new JWTTokenProvider(TEST_APP_ID_2,
51-
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()).toPath());
52-
jwtProvider3 = new JWTTokenProvider(TEST_APP_ID_3,
53-
new String(
54-
Files.readAllBytes(
55-
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_3).getFile()).toPath()),
56-
StandardCharsets.UTF_8));
58+
if (appId != null && appJwkPath != null) {
59+
jwtProvider1 = new JWTTokenProvider(appId, Paths.get(appJwkPath));
60+
jwtProvider2 = jwtProvider1;
61+
jwtProvider3 = jwtProvider1;
62+
} else {
63+
jwtProvider1 = new JWTTokenProvider(TEST_APP_ID_1,
64+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_1).getFile()));
65+
jwtProvider2 = new JWTTokenProvider(TEST_APP_ID_2,
66+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()).toPath());
67+
jwtProvider3 = new JWTTokenProvider(TEST_APP_ID_3,
68+
new String(Files.readAllBytes(
69+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_3).getFile()).toPath()),
70+
StandardCharsets.UTF_8));
71+
}
5772
} catch (GeneralSecurityException | IOException e) {
5873
throw new RuntimeException("These should never fail", e);
5974
}
@@ -89,17 +104,28 @@ private String createJwtToken(String keyFileResouceName, String appId) {
89104
* Signals that an I/O exception has occurred.
90105
*/
91106
protected GHAppInstallation getAppInstallationWithToken(String jwtToken) throws IOException {
107+
if (jwtToken.startsWith("Bearer ")) {
108+
jwtToken = jwtToken.substring("Bearer ".length());
109+
}
110+
92111
GitHub gitHub = getGitHubBuilder().withJwtToken(jwtToken)
93112
.withEndpoint(mockGitHub.apiServer().baseUrl())
94113
.build();
95114

96-
GHAppInstallation appInstallation = gitHub.getApp()
97-
.listInstallations()
98-
.toList()
99-
.stream()
100-
.filter(it -> it.getAccount().login.equals("hub4j-test-org"))
101-
.findFirst()
102-
.get();
115+
GHApp app = gitHub.getApp();
116+
117+
GHAppInstallation appInstallation;
118+
if (ImmutableSet.of(TEST_APP_ID_1, TEST_APP_ID_2, TEST_APP_ID_3).contains(Long.toString(app.getId()))) {
119+
List<GHAppInstallation> installations = app.listInstallations().toList();
120+
appInstallation = installations.stream()
121+
.filter(it -> it.getAccount().login.equals("hub4j-test-org"))
122+
.findFirst()
123+
.get();
124+
} else {
125+
// We may be processing a custom JWK, for a custom GHApp: fetch a relevant repository dynamically
126+
appInstallation = app.getInstallationByRepository(System.getenv(ENV_GITHUB_APP_ORG),
127+
System.getenv(ENV_GITHUB_APP_REPO));
128+
}
103129

104130
// TODO: this is odd
105131
// appInstallation

src/test/java/org/kohsuke/github/GHAppInstallationTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,21 @@ public void testListRepositoriesNoPermissions() throws IOException {
4444
appInstallation.listRepositories().toList().isEmpty());
4545
}
4646

47+
/**
48+
* Test list repositories no permissions.
49+
*
50+
* @throws IOException
51+
* Signals that an I/O exception has occurred.
52+
*/
53+
@Test
54+
public void testGetMarketplaceAccount() throws IOException {
55+
GHAppInstallation appInstallation = getAppInstallationWithToken(jwtProvider3.getEncodedAuthorization());
56+
57+
GHMarketplaceAccountPlan marketplaceAccount = appInstallation.getMarketplaceAccount();
58+
GHMarketplacePlanTest.testMarketplaceAccount(marketplaceAccount);
59+
60+
GHMarketplaceAccountPlan plan = marketplaceAccount.getPlan();
61+
assertThat(plan.getType(), equalTo(GHMarketplaceAccountType.ORGANIZATION));
62+
}
63+
4764
}

src/test/java/org/kohsuke/github/GHMarketplacePlanTest.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.kohsuke.github;
22

3+
import org.hamcrest.Matchers;
34
import org.junit.Test;
45

56
import java.io.IOException;
7+
import java.util.Arrays;
68
import java.util.List;
79

810
import static org.hamcrest.Matchers.*;
@@ -40,7 +42,7 @@ protected GitHubBuilder getGitHubBuilder() {
4042
public void listMarketplacePlans() throws IOException {
4143
List<GHMarketplacePlan> plans = gitHub.listMarketplacePlans().toList();
4244
assertThat(plans.size(), equalTo(3));
43-
plans.forEach(this::testMarketplacePlan);
45+
plans.forEach(GHMarketplacePlanTest::testMarketplacePlan);
4446
}
4547

4648
/**
@@ -55,7 +57,7 @@ public void listAccounts() throws IOException {
5557
assertThat(plans.size(), equalTo(3));
5658
List<GHMarketplaceAccountPlan> marketplaceUsers = plans.get(0).listAccounts().createRequest().toList();
5759
assertThat(marketplaceUsers.size(), equalTo(2));
58-
marketplaceUsers.forEach(this::testMarketplaceAccount);
60+
marketplaceUsers.forEach(GHMarketplacePlanTest::testMarketplaceAccount);
5961
}
6062

6163
/**
@@ -75,7 +77,7 @@ public void listAccountsWithDirection() throws IOException {
7577
.createRequest()
7678
.toList();
7779
assertThat(marketplaceUsers.size(), equalTo(2));
78-
marketplaceUsers.forEach(this::testMarketplaceAccount);
80+
marketplaceUsers.forEach(GHMarketplacePlanTest::testMarketplaceAccount);
7981
}
8082

8183
}
@@ -98,12 +100,12 @@ public void listAccountsWithSortAndDirection() throws IOException {
98100
.createRequest()
99101
.toList();
100102
assertThat(marketplaceUsers.size(), equalTo(2));
101-
marketplaceUsers.forEach(this::testMarketplaceAccount);
103+
marketplaceUsers.forEach(GHMarketplacePlanTest::testMarketplaceAccount);
102104
}
103105

104106
}
105107

106-
private void testMarketplacePlan(GHMarketplacePlan plan) {
108+
static void testMarketplacePlan(GHMarketplacePlan plan) {
107109
// Non-nullable fields
108110
assertThat(plan.getUrl(), notNullValue());
109111
assertThat(plan.getAccountsUrl(), notNullValue());
@@ -118,10 +120,10 @@ private void testMarketplacePlan(GHMarketplacePlan plan) {
118120
assertThat(plan.getMonthlyPriceInCents(), greaterThanOrEqualTo(0L));
119121

120122
// list
121-
assertThat(plan.getBullets().size(), equalTo(2));
123+
assertThat(plan.getBullets().size(), Matchers.in(Arrays.asList(2, 3)));
122124
}
123125

124-
private void testMarketplaceAccount(GHMarketplaceAccountPlan account) {
126+
static void testMarketplaceAccount(GHMarketplaceAccountPlan account) {
125127
// Non-nullable fields
126128
assertThat(account.getLogin(), notNullValue());
127129
assertThat(account.getUrl(), notNullValue());
@@ -146,7 +148,7 @@ private void testMarketplaceAccount(GHMarketplaceAccountPlan account) {
146148
testMarketplacePendingChange(account.getMarketplacePendingChange());
147149
}
148150

149-
private void testMarketplacePurchase(GHMarketplacePurchase marketplacePurchase) {
151+
static void testMarketplacePurchase(GHMarketplacePurchase marketplacePurchase) {
150152
// Non-nullable fields
151153
assertThat(marketplacePurchase.getBillingCycle(), notNullValue());
152154
assertThat(marketplacePurchase.getNextBillingDate(), notNullValue());
@@ -165,11 +167,11 @@ private void testMarketplacePurchase(GHMarketplacePurchase marketplacePurchase)
165167
if (marketplacePurchase.getPlan().getPriceModel() == GHMarketplacePriceModel.PER_UNIT)
166168
assertThat(marketplacePurchase.getUnitCount(), notNullValue());
167169
else
168-
assertThat(marketplacePurchase.getUnitCount(), nullValue());
170+
assertThat(marketplacePurchase.getUnitCount(), Matchers.anyOf(nullValue(), is(1L)));
169171

170172
}
171173

172-
private void testMarketplacePendingChange(GHMarketplacePendingChange marketplacePendingChange) {
174+
static void testMarketplacePendingChange(GHMarketplacePendingChange marketplacePendingChange) {
173175
// Non-nullable fields
174176
assertThat(marketplacePendingChange.getEffectiveDate(), notNullValue());
175177
testMarketplacePlan(marketplacePendingChange.getPlan());
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"id": 83009,
3+
"slug": "cleanthat",
4+
"node_id": "MDM6QXBwNjU1NTA=",
5+
"owner": {
6+
"login": "solven-eu",
7+
"id": 34552197,
8+
"node_id": "MDEyOk9yZ2FuaXphdGlvbjM0NTUyMTk3",
9+
"avatar_url": "https://avatars.githubusercontent.com/u/34552197?v=4",
10+
"gravatar_id": "",
11+
"url": "https://api.github.com/users/solven-eu",
12+
"html_url": "https://github.com/solven-eu",
13+
"followers_url": "https://api.github.com/users/solven-eu/followers",
14+
"following_url": "https://api.github.com/users/solven-eu/following{/other_user}",
15+
"gists_url": "https://api.github.com/users/solven-eu/gists{/gist_id}",
16+
"starred_url": "https://api.github.com/users/solven-eu/starred{/owner}{/repo}",
17+
"subscriptions_url": "https://api.github.com/users/solven-eu/subscriptions",
18+
"organizations_url": "https://api.github.com/users/solven-eu/orgs",
19+
"repos_url": "https://api.github.com/users/solven-eu/repos",
20+
"events_url": "https://api.github.com/users/solven-eu/events{/privacy}",
21+
"received_events_url": "https://api.github.com/users/solven-eu/received_events",
22+
"type": "Organization",
23+
"site_admin": false
24+
},
25+
"name": "CleanThat",
26+
"description": "Cleanthat cleans branches automatically to fix/improve your code.\r\n\r\nFeatures :\r\n- Fix branches a pull_requests head\r\n- Open pull_request to fix protected branches\r\n- Format `.md`, `.java`, `.scala`, `.json`, `.yaml` with the help of [Spotless](https://github.com/diffplug/spotless)\r\n- Refactor `.java` files to improve code-style, security and stability",
27+
"external_url": "https://github.com/solven-eu/cleanthat",
28+
"html_url": "https://github.com/apps/cleanthat",
29+
"created_at": "2020-05-19T13:45:43Z",
30+
"updated_at": "2023-01-27T06:10:21Z",
31+
"permissions": {
32+
"checks": "write",
33+
"contents": "write",
34+
"metadata": "read",
35+
"pull_requests": "write"
36+
},
37+
"events": [
38+
"pull_request",
39+
"push"
40+
],
41+
"installations_count": 280
42+
}

0 commit comments

Comments
 (0)