Skip to content

Commit d02d076

Browse files
committed
Initial functional version, add unittests
1 parent 15b9aee commit d02d076

File tree

13 files changed

+429
-27
lines changed

13 files changed

+429
-27
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,12 @@
600600
<version>2.0.3</version>
601601
<scope>test</scope>
602602
</dependency>
603+
<dependency>
604+
<groupId>com.nimbusds</groupId>
605+
<artifactId>nimbus-jose-jwt</artifactId>
606+
<version>9.5</version>
607+
<scope>test</scope>
608+
</dependency>
603609
</dependencies>
604610
<repositories>
605611
<repository>

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,24 @@ 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+
* @see <a href=
378+
* "https://docs.github.com/en/rest/apps/marketplace?apiVersion=2022-11-28#get-a-subscription-plan-for-an-account">Get
379+
* a subscription plan for an account</a>
380+
*/
381+
public GHMarketplaceAccountPlan getMarketplaceAccount() throws IOException {
382+
return new GHMarketplacePlanForAccountBuilder(root(), account.getId()).createRequest();
383+
}
364384
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,27 @@ public GHMarketplaceAccountType getType() {
7272
return type;
7373
}
7474

75+
/**
76+
* Shows whether the user or organization account actively subscribes to a plan listed by the authenticated GitHub
77+
* App. When someone submits a plan change that won't be processed until the end of their billing cycle, you will
78+
* also see the upcoming pending change.
79+
*
80+
* <p>
81+
* You use the returned builder to set various properties, then call
82+
* {@link GHMarketplacePlanForAccountBuilder#createRequest()} to finally fetch the plan related this this account.
83+
*
84+
* <p>
85+
* GitHub Apps must use a JWT to access this endpoint.
86+
* <p>
87+
* OAuth Apps must use basic authentication with their client ID and client secret to access this endpoint.
88+
*
89+
* @return a GHMarketplaceListAccountBuilder instance
90+
* @see <a href=
91+
* "https://developer.github.com/v3/apps/marketplace/#list-all-github-accounts-user-or-organization-on-a-specific-plan">List
92+
* all GitHub accounts (user or organization) on a specific plan</a>
93+
*/
94+
public GHMarketplacePlanForAccountBuilder getPlan() {
95+
return new GHMarketplacePlanForAccountBuilder(root(), this.id);
96+
}
97+
7598
}
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/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java

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

3+
import com.nimbusds.jose.JOSEException;
4+
import com.nimbusds.jose.crypto.impl.RSAKeyUtils;
5+
import com.nimbusds.jose.jwk.RSAKey;
36
import io.jsonwebtoken.Jwts;
47
import org.apache.commons.io.IOUtils;
58
import org.kohsuke.github.authorization.AuthorizationProvider;
@@ -13,17 +16,25 @@
1316
import java.security.KeyFactory;
1417
import java.security.PrivateKey;
1518
import java.security.spec.PKCS8EncodedKeySpec;
19+
import java.text.ParseException;
1620
import java.time.Instant;
1721
import java.time.temporal.ChronoUnit;
1822
import java.util.Base64;
1923
import java.util.Date;
24+
import java.util.List;
25+
import java.util.Set;
2026

2127
// TODO: Auto-generated Javadoc
2228
/**
2329
* The Class AbstractGHAppInstallationTest.
2430
*/
2531
public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
2632

33+
private static String ENV_GITHUB_APP_ID = "GITHUB_APP_ID";
34+
private static String ENV_GITHUB_APP_TOKEN = "GITHUB_APP_TOKEN";
35+
private static String ENV_GITHUB_APP_ORG = "GITHUB_APP_ORG";
36+
private static String ENV_GITHUB_APP_REPO = "GITHUB_APP_REPO";
37+
2738
private static String TEST_APP_ID_1 = "82994";
2839
private static String TEST_APP_ID_2 = "83009";
2940
private static String TEST_APP_ID_3 = "89368";
@@ -44,17 +55,31 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
4455
* Instantiates a new abstract GH app installation test.
4556
*/
4657
protected AbstractGHAppInstallationTest() {
58+
String appId = System.getenv(ENV_GITHUB_APP_ID);
59+
String appToken = System.getenv(ENV_GITHUB_APP_TOKEN);
4760
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));
57-
} catch (GeneralSecurityException | IOException e) {
61+
if (appId != null && appToken != null) {
62+
RSAKey rsaJWK;
63+
try {
64+
rsaJWK = RSAKey.parse(appToken);
65+
} catch (IllegalStateException | ParseException e) {
66+
throw new IllegalStateException("Issue parsing privateKey", e);
67+
}
68+
69+
jwtProvider1 = new JWTTokenProvider(appId, RSAKeyUtils.toRSAPrivateKey(rsaJWK));
70+
jwtProvider2 = new JWTTokenProvider(appId, RSAKeyUtils.toRSAPrivateKey(rsaJWK));
71+
jwtProvider3 = new JWTTokenProvider(appId, RSAKeyUtils.toRSAPrivateKey(rsaJWK));
72+
} else {
73+
jwtProvider1 = new JWTTokenProvider(TEST_APP_ID_1,
74+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_1).getFile()));
75+
jwtProvider2 = new JWTTokenProvider(TEST_APP_ID_2,
76+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()).toPath());
77+
jwtProvider3 = new JWTTokenProvider(TEST_APP_ID_3,
78+
new String(Files.readAllBytes(
79+
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_3).getFile()).toPath()),
80+
StandardCharsets.UTF_8));
81+
}
82+
} catch (GeneralSecurityException | IOException | JOSEException e) {
5883
throw new RuntimeException("These should never fail", e);
5984
}
6085
}
@@ -89,17 +114,28 @@ private String createJwtToken(String keyFileResouceName, String appId) {
89114
* Signals that an I/O exception has occurred.
90115
*/
91116
protected GHAppInstallation getAppInstallationWithToken(String jwtToken) throws IOException {
117+
if (jwtToken.startsWith("Bearer ")) {
118+
jwtToken = jwtToken.substring("Bearer ".length());
119+
}
120+
92121
GitHub gitHub = getGitHubBuilder().withJwtToken(jwtToken)
93122
.withEndpoint(mockGitHub.apiServer().baseUrl())
94123
.build();
95124

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();
125+
GHApp app = gitHub.getApp();
126+
127+
GHAppInstallation appInstallation;
128+
if (Set.of(TEST_APP_ID_1, TEST_APP_ID_2, TEST_APP_ID_3).contains(Long.toString(app.getId()))) {
129+
List<GHAppInstallation> installations = app.listInstallations().toList();
130+
appInstallation = installations.stream()
131+
.filter(it -> it.getAccount().login.equals("hub4j-test-org"))
132+
.findFirst()
133+
.get();
134+
} else {
135+
// We may be processing a custom JWK, for a custom GHApp: fetch a relevant repository dynamically
136+
appInstallation = app.getInstallationByRepository(System.getenv(ENV_GITHUB_APP_ORG),
137+
System.getenv(ENV_GITHUB_APP_REPO));
138+
}
103139

104140
// TODO: this is odd
105141
// appInstallation

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,17 @@ 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+
GHMarketplacePlanTest.testMarketplaceAccount(appInstallation.getMarketplaceAccount());
58+
}
59+
4760
}

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.either(nullValue()).or(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": 65550,
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)