Skip to content

Commit 6390b97

Browse files
authored
Fix the API build notification state when the sent payload contains a longer than allowed attribute value. (#928)
For bitbucket server and cloud: - name must be less than 255 characters; - URL must be less than 450 characters, in this case we can not trim otherwise the URL is invalid.
1 parent a71d0e0 commit 6390b97

File tree

12 files changed

+244
-37
lines changed

12 files changed

+244
-37
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ work
1717

1818
# VSCode
1919
.factorypath
20+
META-INF/

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@
127127
<version>3.26.3</version>
128128
<scope>test</scope>
129129
</dependency>
130+
<dependency>
131+
<groupId>net.javacrumbs.json-unit</groupId>
132+
<artifactId>json-unit-assertj</artifactId>
133+
<version>4.0.0</version>
134+
<scope>test</scope>
135+
</dependency>
130136
<dependency>
131137
<groupId>org.jenkins-ci.plugins</groupId>
132138
<artifactId>git</artifactId>

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotifications.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private static void createStatus(@NonNull Run<?, ?> build, @NonNull TaskListener
172172

173173
private static @CheckForNull BitbucketSCMSource findBitbucketSCMSource(Run<?, ?> build) {
174174
SCMSource s = SCMSource.SourceByItem.findSource(build.getParent());
175-
return s instanceof BitbucketSCMSource ? (BitbucketSCMSource) s : null;
175+
return s instanceof BitbucketSCMSource scm ? scm : null;
176176
}
177177

178178
private static void sendNotifications(BitbucketSCMSource source, Run<?, ?> build, TaskListener listener)
@@ -211,12 +211,11 @@ private static void sendNotifications(BitbucketSCMSource source, Run<?, ?> build
211211

212212
@CheckForNull
213213
private static String getHash(@CheckForNull SCMRevision revision) {
214-
if (revision instanceof PullRequestSCMRevision) {
215-
// unwrap
216-
revision = ((PullRequestSCMRevision) revision).getPull();
214+
if (revision instanceof PullRequestSCMRevision prRevision) {
215+
revision = prRevision.getPull();
217216
}
218-
if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) {
219-
return ((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash();
217+
if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl scmRevision) {
218+
return scmRevision.getHash();
220219
}
221220
return null;
222221
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.api;
2525

2626
import com.fasterxml.jackson.annotation.JsonIgnore;
27+
import edu.umd.cs.findbugs.annotations.NonNull;
2728
import org.apache.commons.codec.digest.DigestUtils;
2829
import org.kohsuke.accmod.Restricted;
2930
import org.kohsuke.accmod.restrictions.DoNotUse;
@@ -99,6 +100,20 @@ public BitbucketBuildStatus(String hash, String description, Status state, Strin
99100
this.name = name;
100101
}
101102

103+
/**
104+
* Copy constructor.
105+
*
106+
* @param other from copy to.
107+
*/
108+
public BitbucketBuildStatus(@NonNull BitbucketBuildStatus other) {
109+
this.hash = other.hash;
110+
this.description = other.description;
111+
this.state = other.state;
112+
this.url = other.url;
113+
this.key = other.key;
114+
this.name = other.name;
115+
}
116+
102117
public String getHash() {
103118
return hash;
104119
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,12 +657,15 @@ public List<BitbucketRepositoryHook> getWebHooks() throws IOException, Interrupt
657657
*/
658658
@Override
659659
public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException, InterruptedException {
660+
BitbucketBuildStatus newStatus = new BitbucketBuildStatus(status);
661+
newStatus.setName(truncateMiddle(newStatus.getName(), 255));
662+
660663
String url = UriTemplate.fromTemplate(REPO_URL_TEMPLATE + "/commit/{hash}/statuses/build")
661664
.set("owner", owner)
662665
.set("repo", repositoryName)
663-
.set("hash", status.getHash())
666+
.set("hash", newStatus.getHash())
664667
.expand();
665-
postRequest(url, JsonParser.toJson(status));
668+
postRequest(url, JsonParser.toJson(newStatus));
666669
}
667670

668671
/**

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/internal/api/AbstractBitbucketApi.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.internal.api;
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException;
27+
import edu.umd.cs.findbugs.annotations.CheckForNull;
2728
import hudson.ProxyConfiguration;
2829
import java.io.IOException;
2930
import java.io.InputStream;
@@ -50,12 +51,21 @@
5051
import org.kohsuke.accmod.restrictions.ProtectedExternally;
5152

5253
@Restricted(ProtectedExternally.class)
53-
public class AbstractBitbucketApi {
54+
public abstract class AbstractBitbucketApi {
5455
protected static final int API_RATE_LIMIT_STATUS_CODE = 429;
5556

5657
protected final Logger logger = Logger.getLogger(this.getClass().getName());
5758
protected HttpClientContext context;
5859

60+
protected String truncateMiddle(@CheckForNull String value, int maxLength) {
61+
int length = StringUtils.length(value);
62+
if (length > maxLength) {
63+
int halfLength = (maxLength - 3) / 2;
64+
return StringUtils.left(value, halfLength) + "..." + StringUtils.substring(value, -halfLength);
65+
} else {
66+
return value;
67+
}
68+
}
5969

6070
protected BitbucketRequestException buildResponseException(CloseableHttpResponse response, String errorMessage) {
6171
String headers = StringUtils.join(response.getAllHeaders(), "\n");

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -509,13 +509,13 @@ public void postCommitComment(@NonNull String hash, @NonNull String comment) thr
509509
*/
510510
@Override
511511
public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException, InterruptedException {
512-
postRequest(
513-
UriTemplate
514-
.fromTemplate(API_COMMIT_STATUS_PATH)
515-
.set("hash", status.getHash())
516-
.expand(),
517-
JsonParser.toJson(status)
518-
);
512+
BitbucketBuildStatus newStatus = new BitbucketBuildStatus(status);
513+
newStatus.setName(truncateMiddle(newStatus.getName(), 255));
514+
515+
String url = UriTemplate.fromTemplate(API_COMMIT_STATUS_PATH)
516+
.set("hash", newStatus.getHash())
517+
.expand();
518+
postRequest(url, JsonParser.toJson(newStatus));
519519
}
520520

521521
/**

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,87 @@
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.JsonParser;
2727
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
28+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
29+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status;
2830
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook;
2931
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.IRequestAudit;
3032
import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudRepository;
3133
import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint;
34+
import io.jenkins.cli.shaded.org.apache.commons.lang.RandomStringUtils;
3235
import java.io.IOException;
3336
import java.io.InputStream;
37+
import java.nio.charset.StandardCharsets;
3438
import java.util.Date;
3539
import java.util.Optional;
3640
import org.apache.commons.io.IOUtils;
37-
import org.hamcrest.CoreMatchers;
38-
import org.junit.Test;
41+
import org.apache.http.client.methods.HttpPost;
42+
import org.apache.http.client.methods.HttpPut;
43+
import org.apache.http.client.methods.HttpRequestBase;
44+
import org.junit.jupiter.api.Test;
45+
import org.mockito.ArgumentCaptor;
3946

40-
import static org.hamcrest.MatcherAssert.assertThat;
41-
import static org.junit.Assert.assertNotNull;
42-
import static org.junit.Assert.assertTrue;
47+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
48+
import static org.assertj.core.api.Assertions.assertThat;
4349
import static org.mockito.Mockito.reset;
4450
import static org.mockito.Mockito.verify;
4551

46-
public class BitbucketCloudApiClientTest {
52+
class BitbucketCloudApiClientTest {
4753

48-
public String loadPayload(String api) throws IOException {
54+
private String loadPayload(String api) throws IOException {
4955
try (InputStream is = getClass().getResourceAsStream(getClass().getSimpleName() + "/" + api + "Payload.json")) {
5056
return IOUtils.toString(is, "UTF-8");
5157
}
5258
}
5359

5460
@Test
55-
public void get_repository_parse_correctly_date_from_cloud() throws Exception {
61+
void verify_status_notitication_name_max_length() throws Exception {
62+
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient(BitbucketCloudEndpoint.SERVER_URL);
63+
BitbucketBuildStatus status = new BitbucketBuildStatus();
64+
status.setName(RandomStringUtils.randomAlphanumeric(300));
65+
status.setState(Status.INPROGRESS);
66+
status.setHash("046d9a3c1532acf4cf08fe93235c00e4d673c1d3");
67+
68+
client.postBuildStatus(status);
69+
70+
IRequestAudit clientAudit = ((IRequestAudit) client).getAudit();
71+
HttpRequestBase request = extractRequest(clientAudit);
72+
assertThat(request).isNotNull()
73+
.isInstanceOf(HttpPost.class);
74+
try (InputStream content = ((HttpPost) request).getEntity().getContent()) {
75+
String json = IOUtils.toString(content, StandardCharsets.UTF_8);
76+
assertThatJson(json).node("name").isString().hasSize(255);
77+
}
78+
}
79+
80+
private HttpRequestBase extractRequest(IRequestAudit clientAudit) {
81+
ArgumentCaptor<HttpRequestBase> captor = ArgumentCaptor.forClass(HttpRequestBase.class);
82+
verify(clientAudit).request(captor.capture());
83+
return captor.getValue();
84+
}
85+
86+
@Test
87+
void get_repository_parse_correctly_date_from_cloud() throws Exception {
5688
BitbucketCloudRepository repository = JsonParser.toJava(loadPayload("getRepository"), BitbucketCloudRepository.class);
57-
assertNotNull("update on date is null", repository.getUpdatedOn());
58-
Date date = DateUtils.getDate(2018, 4, 27, 15, 32, 8, 356);
59-
assertThat(repository.getUpdatedOn().getTime(), CoreMatchers.is(date.getTime()));
89+
assertThat(repository.getUpdatedOn()).describedAs("update on date is null").isNotNull();
90+
Date expectedDate = DateUtils.getDate(2018, 4, 27, 15, 32, 8, 356);
91+
assertThat(repository.getUpdatedOn()).isEqualTo(expectedDate);
6092
}
6193

6294
@Test
63-
public void verifyUpdateWebhookURL() throws Exception {
95+
void verifyUpdateWebhookURL() throws Exception {
6496
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient(BitbucketCloudEndpoint.SERVER_URL);
6597
IRequestAudit audit = ((IRequestAudit) client).getAudit();
6698
Optional<? extends BitbucketWebHook> webHook = client.getWebHooks().stream()
6799
.filter(h -> h.getDescription().contains("Jenkins"))
68100
.findFirst();
69-
assertTrue(webHook.isPresent());
101+
assertThat(webHook).isPresent();
70102

71103
reset(audit);
72104
client.updateCommitWebHook(webHook.get());
73-
verify(audit).request("https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/hooks/%7B202cf34e-7ccf-44b7-ba6b-8827a14d5324%7D");
105+
HttpRequestBase request = extractRequest(audit);
106+
assertThat(request).isNotNull()
107+
.isInstanceOfSatisfying(HttpPut.class, put ->
108+
assertThat(put.getURI()).hasToString("https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/hooks/%7B202cf34e-7ccf-44b7-ba6b-8827a14d5324%7D"));
74109
}
75110

76111
}

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
public class BitbucketIntegrationClientFactory {
4747

4848
public interface IRequestAudit {
49-
default void request(String url) {
49+
default void request(HttpRequestBase request) {
5050
// mockito audit
5151
}
5252

@@ -118,10 +118,12 @@ protected CloseableHttpResponse executeMethodNoRetry(CloseableHttpClient client,
118118
return createRateLimitResponse();
119119
}
120120
String path = httpMethod.getURI().toString();
121-
path = path.substring(path.indexOf("/rest/api/"));
122-
audit.request(path);
121+
audit.request(httpMethod);
123122

124-
String payloadPath = path.replace("/rest/api/", "").replace('/', '-').replaceAll("[=%&?]", "_");
123+
String payloadPath = path.substring(path.indexOf("/rest/"))
124+
.replace("/rest/api/", "")
125+
.replace("/rest/", "")
126+
.replace('/', '-').replaceAll("[=%&?]", "_");
125127
payloadPath = payloadRootPath + payloadPath + ".json";
126128

127129
return loadResponseFromResources(getClass(), path, payloadPath);
@@ -160,7 +162,7 @@ private BitbucketClouldIntegrationClient(String payloadRootPath, String owner, S
160162
@Override
161163
protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws InterruptedException, IOException {
162164
String path = httpMethod.getURI().toString();
163-
audit.request(path);
165+
audit.request(httpMethod);
164166

165167
String payloadPath = path.replace(API_ENDPOINT, "").replace('/', '-').replaceAll("[=%&?]", "_");
166168
payloadPath = payloadRootPath + payloadPath + ".json";

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,37 @@
11
package com.cloudbees.jenkins.plugins.bitbucket.server.client;
22

33
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
4+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus;
5+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status;
46
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository;
57
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory;
68
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.BitbucketServerIntegrationClient;
9+
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.IRequestAudit;
710
import com.damnhandy.uri.template.UriTemplate;
811
import com.damnhandy.uri.template.impl.Operator;
12+
import io.jenkins.cli.shaded.org.apache.commons.lang.RandomStringUtils;
13+
import java.io.InputStream;
14+
import java.nio.charset.StandardCharsets;
915
import java.util.List;
1016
import java.util.logging.Level;
17+
import org.apache.commons.io.IOUtils;
18+
import org.apache.http.client.methods.HttpPost;
19+
import org.apache.http.client.methods.HttpRequestBase;
1120
import org.apache.http.impl.client.CloseableHttpClient;
1221
import org.apache.http.impl.client.HttpClientBuilder;
1322
import org.junit.Assert;
23+
import org.junit.ClassRule;
1424
import org.junit.Rule;
1525
import org.junit.Test;
1626
import org.jvnet.hudson.test.JenkinsRule;
1727
import org.jvnet.hudson.test.LoggerRule;
1828
import org.jvnet.hudson.test.WithoutJenkins;
29+
import org.mockito.ArgumentCaptor;
1930
import org.mockito.MockedStatic;
2031

2132
import static com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient.API_BROWSE_PATH;
33+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
34+
import static org.assertj.core.api.Assertions.assertThat;
2235
import static org.hamcrest.MatcherAssert.assertThat;
2336
import static org.hamcrest.Matchers.containsString;
2437
import static org.hamcrest.Matchers.equalTo;
@@ -32,11 +45,38 @@
3245

3346
public class BitbucketServerAPIClientTest {
3447

35-
@Rule
36-
public JenkinsRule r = new JenkinsRule();
48+
@ClassRule
49+
public static JenkinsRule r = new JenkinsRule();
3750
@Rule
3851
public LoggerRule logger = new LoggerRule().record(BitbucketServerIntegrationClient.class, Level.FINE);
3952

53+
@Test
54+
@WithoutJenkins
55+
public void verify_status_notitication_name_max_length() throws Exception {
56+
BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.org");
57+
BitbucketBuildStatus status = new BitbucketBuildStatus();
58+
status.setName(RandomStringUtils.randomAlphanumeric(300));
59+
status.setState(Status.INPROGRESS);
60+
status.setHash("046d9a3c1532acf4cf08fe93235c00e4d673c1d3");
61+
62+
client.postBuildStatus(status);
63+
64+
IRequestAudit clientAudit = ((IRequestAudit) client).getAudit();
65+
HttpRequestBase request = extractRequest(clientAudit);
66+
assertThat(request).isNotNull()
67+
.isInstanceOf(HttpPost.class);
68+
try (InputStream content = ((HttpPost) request).getEntity().getContent()) {
69+
String json = IOUtils.toString(content, StandardCharsets.UTF_8);
70+
assertThatJson(json).node("name").isString().hasSize(255);
71+
}
72+
}
73+
74+
private HttpRequestBase extractRequest(IRequestAudit clientAudit) {
75+
ArgumentCaptor<HttpRequestBase> captor = ArgumentCaptor.forClass(HttpRequestBase.class);
76+
verify(clientAudit).request(captor.capture());
77+
return captor.getValue();
78+
}
79+
4080
@Test
4181
@WithoutJenkins
4282
public void repoBrowsePathFolder() {

0 commit comments

Comments
 (0)