diff options
7 files changed, 275 insertions, 59 deletions
@@ -8,7 +8,7 @@ <artifactId>bitbucket-pullrequest-builder</artifactId> <name>Bitbucket Pullrequest Builder Plugin</name> - <version>1.4.14-SNAPSHOT</version> + <version>1.4.15</version> <description>This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results.</description> <packaging>hpi</packaging> <url>https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin</url> diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java index cd9345c..2375ec7 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java @@ -1,12 +1,17 @@ package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; import antlr.ANTLRException; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import hudson.Extension; import hudson.model.*; import hudson.model.queue.QueueTaskFuture; import hudson.plugins.git.RevisionParameterAction; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; +import hudson.util.ListBoxModel; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; @@ -17,6 +22,8 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; + /** * Created by nishio */ @@ -24,6 +31,7 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); private final String projectPath; private final String cron; + private final String credentialsId; private final String username; private final String password; private final String repositoryOwner; @@ -45,6 +53,7 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { public BitbucketBuildTrigger( String projectPath, String cron, + String credentialsId, String username, String password, String repositoryOwner, @@ -60,6 +69,7 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { super(cron); this.projectPath = projectPath; this.cron = cron; + this.credentialsId = credentialsId; this.username = username; this.password = password; this.repositoryOwner = repositoryOwner; @@ -81,6 +91,10 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { return this.cron; } + public String getCredentialsId() { + return credentialsId; + } + public String getUsername() { return username; } @@ -208,5 +222,12 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { save(); return super.configure(req, json); } + + public ListBoxModel doFillCredentialsIdItems() { + return new StandardListBoxModel() + .withEmptySelection() + .withMatching(instanceOf(UsernamePasswordCredentials.class), + CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class)); + } } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java index 6736ee9..23b0607 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java @@ -13,12 +13,18 @@ import java.util.LinkedList; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import jenkins.model.Jenkins; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; import org.apache.commons.lang.StringUtils; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; + /** * Created by nishio */ @@ -34,26 +40,46 @@ public class BitbucketRepository { private BitbucketPullRequestsBuilder builder; private BitbucketBuildTrigger trigger; private ApiClient client; - + public BitbucketRepository(String projectPath, BitbucketPullRequestsBuilder builder) { this.projectPath = projectPath; this.builder = builder; } public void init() { - this.init(null); + this.init(null, null); + } + + public <T extends ApiClient.HttpClientFactory> void init(Class<T> httpFactory) { + this.init(null, httpFactory); } public void init(ApiClient client) { + this.init(client, null); + } + + public <T extends ApiClient.HttpClientFactory> void init(ApiClient client, Class<T> httpFactory) { this.trigger = this.builder.getTrigger(); - this.client = (client == null) ? new ApiClient( - trigger.getUsername(), - trigger.getPassword(), + + if (client == null) { + String username = trigger.getUsername(); + String password = trigger.getPassword(); + StandardUsernamePasswordCredentials credentials = getCredentials(trigger.getCredentialsId()); + if (credentials != null) { + username = credentials.getUsername(); + password = credentials.getPassword().getPlainText(); + } + this.client = new ApiClient( + username, + password, trigger.getRepositoryOwner(), trigger.getRepositoryName(), trigger.getCiKey(), - trigger.getCiName() - ) : client; + trigger.getCiName(), + httpFactory + ); + + } else this.client = client; } public Collection<Pullrequest> getTargetPullRequests() { @@ -67,6 +93,10 @@ public class BitbucketRepository { } return targetPullRequests; } + + public ApiClient getClient() { + return this.client; + } public void addFutureBuildTasks(Collection<Pullrequest> pullRequests) { for(Pullrequest pullRequest : pullRequests) { @@ -267,4 +297,12 @@ public class BitbucketRepository { return filter.approved(cause); } + + private StandardUsernamePasswordCredentials getCredentials(String credentialsId) { + return CredentialsMatchers + .firstOrNull( + CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class), + CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId), + instanceOf(UsernamePasswordCredentials.class))); + } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java index e635f65..509dfaf 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java @@ -17,7 +17,6 @@ import java.util.logging.Logger; import jenkins.model.Jenkins; import hudson.ProxyConfiguration; import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.httpclient.util.EncodingUtil; /** @@ -33,13 +32,55 @@ public class ApiClient { private Credentials credentials; private String key; private String name; - - public ApiClient(String username, String password, String owner, String repositoryName, String key, String name) { + private HttpClientFactory factory; + + public static class HttpClientFactory { + public static final HttpClientFactory INSTANCE = new HttpClientFactory(); + + public HttpClient getInstanceHttpClient() { + HttpClient client = new HttpClient(); + if (Jenkins.getInstance() == null) return client; + + ProxyConfiguration proxy = Jenkins.getInstance().proxy; + if (proxy == null) return client; + + logger.log(Level.INFO, "Jenkins proxy: {0}:{1}", new Object[]{ proxy.name, proxy.port }); + client.getHostConfiguration().setProxy(proxy.name, proxy.port); + String username = proxy.getUserName(); + String password = proxy.getPassword(); + + // Consider it to be passed if username specified. Sufficient? + if (username != null && !"".equals(username.trim())) { + logger.log(Level.INFO, "Using proxy authentication (user={0})", username); + client.getState().setProxyCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(username, password)); + } + + return client; + } + } + + public <T extends HttpClientFactory> ApiClient( + String username, String password, + String owner, String repositoryName, + String key, String name, + Class<T> httpFactory + ) { this.credentials = new UsernamePasswordCredentials(username, password); this.owner = owner; this.repositoryName = repositoryName; this.key = key; this.name = name; + + try { + this.factory = (httpFactory != null) ? httpFactory.newInstance() : HttpClientFactory.INSTANCE; + } catch(InstantiationException e) { + logger.log(Level.WARNING, "failed new instance of {0}: {1} ", new Object[] { httpFactory.getName(), e }); + e.printStackTrace(); + } catch (IllegalAccessException e) { + logger.log(Level.WARNING, "failed new instance of {0}: {1} ", new Object[] { httpFactory.getName(), e }); + e.printStackTrace(); + } } public List<Pullrequest> getPullRequests() { @@ -47,6 +88,7 @@ public class ApiClient { return parse(get(v2("/pullrequests/")), Pullrequest.Response.class).getPullrequests(); } catch(Exception e) { logger.log(Level.WARNING, "invalid pull request response.", e); + e.printStackTrace(); } return Collections.EMPTY_LIST; } @@ -56,6 +98,7 @@ public class ApiClient { return parse(get(v1("/pullrequests/" + pullRequestId + "/comments")), new TypeReference<List<Pullrequest.Comment>>() {}); } catch(Exception e) { logger.log(Level.WARNING, "invalid pull request response.", e); + e.printStackTrace(); } return Collections.EMPTY_LIST; } @@ -112,6 +155,7 @@ public class ApiClient { return parse(post(v2("/pullrequests/" + pullRequestId + "/approve"), new NameValuePair[]{}), Pullrequest.Participant.class); } catch (IOException e) { + logger.log(Level.WARNING, "Invalid pull request approval response.", e); e.printStackTrace(); } return null; @@ -131,24 +175,7 @@ public class ApiClient { } private HttpClient getHttpClient() { - HttpClient client = new HttpClient(); - if (Jenkins.getInstance() == null) return client; - - ProxyConfiguration proxy = Jenkins.getInstance().proxy; - if (proxy == null) return client; - - logger.info("Jenkins proxy: " + proxy.name + ":" + proxy.port); - client.getHostConfiguration().setProxy(proxy.name, proxy.port); - String username = proxy.getUserName(); - String password = proxy.getPassword(); - - // Consider it to be passed if username specified. Sufficient? - if (username != null && !"".equals(username.trim())) { - logger.info("Using proxy authentication (user=" + username + ")"); - client.getState().setProxyCredentials(AuthScope.ANY, - new UsernamePasswordCredentials(username, password)); - } - return client; + return this.factory.getInstanceHttpClient(); } private String v1(String url) { @@ -193,8 +220,10 @@ public class ApiClient { client.executeMethod(req); return req.getResponseBodyAsString(); } catch (HttpException e) { + logger.log(Level.WARNING, "Failed to send request.", e); e.printStackTrace(); } catch (IOException e) { + logger.log(Level.WARNING, "Failed to send request.", e); e.printStackTrace(); } return null; diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly index 25acac0..d4b23da 100644 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly +++ b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly @@ -1,7 +1,10 @@ -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:c="/lib/credentials"> <f:entry title="Cron" field="cron"> <f:textbox /> </f:entry> + <f:entry title="${%Credentials}" field="credentialsId"> + <c:select/> + </f:entry> <f:entry title="Bitbucket BasicAuth Username" field="username"> <f:textbox /> </f:entry> diff --git a/src/test/java/BitbucketBuildFilterTest.java b/src/test/java/BitbucketBuildFilterTest.java index 0fabfe4..9b84e73 100644 --- a/src/test/java/BitbucketBuildFilterTest.java +++ b/src/test/java/BitbucketBuildFilterTest.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildFilter; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketCause; @@ -10,9 +5,7 @@ import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequ import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; -import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; @@ -25,8 +18,7 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; /** - * - * @author maxvodo + * Tests */ public class BitbucketBuildFilterTest { @@ -288,24 +280,4 @@ public class BitbucketBuildFilterTest { for(Pullrequest.Comment comment : comments) assertFalse(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); } - - //@Test - @WithoutJenkins - public void ttpCommentTest() { - ApiClient client = EasyMock.createNiceMock(ApiClient.class); - Collection<List<Pullrequest>> prs = new LinkedList<List<Pullrequest>>(); - - prs.add(Arrays.asList(new Pullrequest[] { - new Pullrequest() - })); - - for(List<Pullrequest> pr : prs) EasyMock.expect(client.getPullRequests()).andReturn(pr).times(1); - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.replay(client, builder); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(client); - - Collection<Pullrequest> targetPRs = repo.getTargetPullRequests(); - } } diff --git a/src/test/java/BitbucketBuildRepositoryTest.java b/src/test/java/BitbucketBuildRepositoryTest.java new file mode 100644 index 0000000..bb6de8f --- /dev/null +++ b/src/test/java/BitbucketBuildRepositoryTest.java @@ -0,0 +1,153 @@ + +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildTrigger; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import org.easymock.*; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; +import jenkins.model.Jenkins; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpState; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.junit.Assert; + + +interface ICredentialsInterceptor { + void assertCredentials(Credentials actual); +} + +/** + * Utility class for interceptor functionality + * @param <T> + */ +class HttpClientInterceptor<T extends ICredentialsInterceptor> extends HttpClient { + + class CredentialsInterceptor<T extends ICredentialsInterceptor> extends HttpState { + private final T interceptor; + public CredentialsInterceptor(T interceptor) { this.interceptor = interceptor; } + + @Override + public synchronized void setCredentials(AuthScope authscope, Credentials credentials) { + super.setCredentials(authscope, credentials); + this.interceptor.assertCredentials(credentials); + throw new AssertionError(); + } + } + + private final T interceptor; + public HttpClientInterceptor(T interceptor) { this.interceptor = interceptor; } + + @Override + public synchronized HttpState getState() { return new CredentialsInterceptor(this.interceptor); } +} + +/** + * Utility class for credentials assertion + * Used with + * @author maxvodo + */ +class AssertCredentials implements ICredentialsInterceptor { + private final Credentials expected; + public AssertCredentials(Credentials expected) { this.expected = expected; } + + public void assertCredentials(Credentials actual) { + if (actual == null) assertTrue(this.expected == null); + else assertTrue(this.expected != null); + + if (actual instanceof UsernamePasswordCredentials) { + UsernamePasswordCredentials actual_ = (UsernamePasswordCredentials)actual, + expected_ = (UsernamePasswordCredentials)this.expected; + assertNotNull(expected_); + Assert.assertArrayEquals(new Object[] { + actual_.getUserName(), actual_.getPassword() + }, new Object[] { + expected_.getUserName(), expected_.getPassword() + }); + } + } +} + +/** + * Tests + */ +public class BitbucketBuildRepositoryTest { + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Test + public void repositorySimpleUserPasswordTest() throws Exception { + BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( + "", "@hourly", + "JenkinsCID", + "foo", + "bar", + "", "", + "", true, + "", "", "", + true, + true + ); + + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); + EasyMock.replay(builder); + + ApiClient.HttpClientFactory httpFactory = EasyMock.createMock(ApiClient.HttpClientFactory.class); + EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( + new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("foo", "bar"))) + ).anyTimes(); + EasyMock.replay(httpFactory); + + BitbucketRepository repo = new BitbucketRepository("", builder); + repo.init(httpFactory.getClass()); + + try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } + } + + @Test + public void repositoryCtorWithTriggerTest() throws Exception { + BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( + "", "@hourly", + "JenkinsCID", + "foo", + "bar", + "", "", + "", true, + "", "", "", + true, + true + ); + + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); + EasyMock.replay(builder); + + CredentialsStore store = CredentialsProvider.lookupStores(Jenkins.getInstance()).iterator().next(); + assertNotNull(store); + store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, "JenkinsCID", "description", "username", "password" + )); + + ApiClient.HttpClientFactory httpFactory = EasyMock.createMock(ApiClient.HttpClientFactory.class); + EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( + new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("username", "password"))) + ).anyTimes(); + EasyMock.replay(httpFactory); + + BitbucketRepository repo = new BitbucketRepository("", builder); + repo.init(httpFactory.getClass()); + + try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } + } +} |