diff --git a/README.markdown b/README.markdown index 10c2a9c..29c41b0 100644 --- a/README.markdown +++ b/README.markdown @@ -1,5 +1,4 @@ ## HipChat plugin for Jenkins Started with a fork of the Campfire plugin: - https://github.com/jgp/hudson_campfire_plugin diff --git a/pom.xml b/pom.xml index c4d4a59..8d65e9e 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,12 @@ 4.11 test + + org.mockito + mockito-all + 1.10.8 + test + log4j log4j @@ -77,6 +83,10 @@ junit junit + + org.mockito + mockito-all + diff --git a/src/main/java/jenkins/plugins/hipchat/ActiveNotifier.java b/src/main/java/jenkins/plugins/hipchat/ActiveNotifier.java index df84b28..8a97095 100644 --- a/src/main/java/jenkins/plugins/hipchat/ActiveNotifier.java +++ b/src/main/java/jenkins/plugins/hipchat/ActiveNotifier.java @@ -1,22 +1,17 @@ package jenkins.plugins.hipchat; -import hudson.Util; -import hudson.model.AbstractBuild; -import hudson.model.AbstractProject; -import hudson.model.CauseAction; -import hudson.model.Result; -import hudson.model.Run; +import hudson.model.*; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.AffectedFile; import hudson.scm.ChangeLogSet.Entry; +import hudson.tasks.Mailer; + import org.apache.commons.lang.StringUtils; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; + import jenkins.model.Jenkins; @SuppressWarnings("rawtypes") @@ -31,7 +26,7 @@ public ActiveNotifier(HipChatNotifier notifier) { this.notifier = notifier; } - private HipChatService getHipChat(AbstractBuild r) { + private HipChatService getHipChat() { return notifier.newHipChatService(); } @@ -49,12 +44,12 @@ public void started(AbstractBuild build) { message.append(cause.getShortDescription()); notifyStart(build, message.appendOpenLink().toString()); } else { - notifyStart(build, getBuildStatusMessage(build)); + notifyStart(build, getBuildStatusMessage(build, null)); } } private void notifyStart(AbstractBuild build, String message) { - getHipChat(build).publish(message, "green"); + getHipChat().publish(message, "green"); } public void finalized(AbstractBuild r) { @@ -71,7 +66,39 @@ public void completed(AbstractBuild r) { || (result == Result.SUCCESS && previousResult == Result.FAILURE && notifier.isNotifyBackToNormal()) || (result == Result.SUCCESS && notifier.isNotifySuccess()) || (result == Result.UNSTABLE && notifier.isNotifyUnstable())) { - getHipChat(r).publish(getBuildStatusMessage(r), getBuildColor(r)); + getHipChat().publish(getBuildStatusMessage(r, fetchCulpritsIfWanted(r)), getBuildColor(r)); + } + } + + private List fetchCulpritsIfWanted(AbstractBuild r) { + if (notifier.isIncludeCulprits()) { + return getCulpritsInHipchat(r); + } else { + return null; + } + } + + private List getCulpritsInHipchat(AbstractBuild r) { + List hipchatUsernames = new ArrayList(); + for(Object userObj : r.getCulprits()) { + User user = (User)userObj; + logger.log(Level.FINE, "Looking up mention name for user {0}", user); + Mailer.UserProperty mailProperty = (user).getProperty(Mailer.UserProperty.class); + if(mailProperty != null && !StringUtils.isEmpty(mailProperty.getAddress())) { + hipchatUsernames.add(buildMentionNameFromEmail(mailProperty)); + }else{ + hipchatUsernames.add(user.getFullName()); + } + } + return hipchatUsernames; + } + + private String buildMentionNameFromEmail(Mailer.UserProperty mailProperty) { + String mentionName = getHipChat().getMentionNameForEmail(mailProperty.getAddress()); + if (mentionName != null) { + return "@" + mentionName; + } else { + return mailProperty.getAddress(); } } @@ -122,10 +149,11 @@ static String getBuildColor(AbstractBuild r) { } } - String getBuildStatusMessage(AbstractBuild r) { + String getBuildStatusMessage(AbstractBuild r, List culpritsInHipchat) { MessageBuilder message = new MessageBuilder(r); message.appendStatusMessage(); message.appendDuration(); + message.appendCulprits(culpritsInHipchat); return message.appendOpenLink().toString(); } @@ -179,8 +207,11 @@ private MessageBuilder startMessage() { } public MessageBuilder appendOpenLink() { - String url = Jenkins.getInstance().getRootUrl() + build.getUrl(); - message.append(" (Open)"); + message.append(" (Open)"); return this; } @@ -190,6 +221,17 @@ public MessageBuilder appendDuration() { return this; } + public MessageBuilder appendCulprits(List culprits) { + if(culprits != null && culprits.size()>0) { + message.append(" Committers: "); + for (String hipchatUsername : culprits) { + message.append(hipchatUsername); + message.append(" "); + } + } + return this; + } + @Override public String toString() { return message.toString(); diff --git a/src/main/java/jenkins/plugins/hipchat/HipChatNotifier.java b/src/main/java/jenkins/plugins/hipchat/HipChatNotifier.java index 6cdbb68..8405935 100644 --- a/src/main/java/jenkins/plugins/hipchat/HipChatNotifier.java +++ b/src/main/java/jenkins/plugins/hipchat/HipChatNotifier.java @@ -37,10 +37,12 @@ public class HipChatNotifier extends Notifier { private boolean notifyUnstable; private boolean notifyFailure; private boolean notifyBackToNormal; + private boolean includeCulprits; @DataBoundConstructor public HipChatNotifier(String room, boolean startNotification, boolean notifySuccess, boolean notifyAborted, - boolean notifyNotBuilt, boolean notifyUnstable, boolean notifyFailure, boolean notifyBackToNormal) { + boolean notifyNotBuilt, boolean notifyUnstable, boolean notifyFailure, boolean notifyBackToNormal, + boolean includeCulprits) { this.room = room; this.startNotification = startNotification; this.notifySuccess = notifySuccess; @@ -49,6 +51,7 @@ public HipChatNotifier(String room, boolean startNotification, boolean notifySuc this.notifyUnstable = notifyUnstable; this.notifyFailure = notifyFailure; this.notifyBackToNormal = notifyBackToNormal; + this.includeCulprits = includeCulprits; } public boolean isStartNotification() { @@ -107,6 +110,14 @@ public void setNotifyBackToNormal(boolean notifyBackToNormal) { this.notifyBackToNormal = notifyBackToNormal; } + public boolean isIncludeCulprits() { + return includeCulprits; + } + + public void setIncludeCulprits(boolean includeCulprits) { + this.includeCulprits = includeCulprits; + } + public String getRoom() { return StringUtils.isBlank(room) ? getDescriptor().getRoom() : room; } @@ -151,7 +162,7 @@ public boolean prebuild(AbstractBuild build, BuildListener listener) { logger.info("Invoking Started..."); new ActiveNotifier(this).started(build); } - return super.prebuild(build, listener); + return true; } @Override @@ -159,7 +170,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen throws InterruptedException, IOException { logger.info("Invoking Completed..."); new ActiveNotifier(this).completed(build); - return super.perform(build, launcher, listener); + return true; } @Extension diff --git a/src/main/java/jenkins/plugins/hipchat/HipChatService.java b/src/main/java/jenkins/plugins/hipchat/HipChatService.java index 320d2b2..795ddc8 100644 --- a/src/main/java/jenkins/plugins/hipchat/HipChatService.java +++ b/src/main/java/jenkins/plugins/hipchat/HipChatService.java @@ -4,4 +4,6 @@ public interface HipChatService { void publish(String message); void publish(String message, String color); + + String getMentionNameForEmail(String email); } diff --git a/src/main/java/jenkins/plugins/hipchat/StandardHipChatService.java b/src/main/java/jenkins/plugins/hipchat/StandardHipChatService.java index cfb850f..ad1bc52 100644 --- a/src/main/java/jenkins/plugins/hipchat/StandardHipChatService.java +++ b/src/main/java/jenkins/plugins/hipchat/StandardHipChatService.java @@ -2,8 +2,12 @@ import hudson.ProxyConfiguration; import jenkins.model.Jenkins; +import net.sf.json.JSONObject; + import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import java.util.logging.Level; @@ -14,19 +18,25 @@ public class StandardHipChatService implements HipChatService { private static final Logger logger = Logger.getLogger(StandardHipChatService.class.getName()); private static final String[] DEFAULT_ROOMS = new String[0]; + private final HttpClient httpClient; private final String server; private final String token; private final String[] roomIds; private final String sendAs; - public StandardHipChatService(String server, String token, String roomIds, String sendAs) { + StandardHipChatService(HttpClient httpClient, String server, String token, String roomIds, String sendAs) { super(); + this.httpClient = httpClient; this.server = server; this.token = token; this.roomIds = roomIds == null ? DEFAULT_ROOMS : roomIds.split("\\s*,\\s*"); this.sendAs = sendAs; } + public StandardHipChatService(String server, String token, String roomIds, String sendAs) { + this(null, server, token, roomIds, sendAs); + } + public void publish(String message) { publish(message, "yellow"); } @@ -58,24 +68,57 @@ public void publish(String message, String color) { } } - private HttpClient getHttpClient() { - HttpClient client = new HttpClient(); - - if (Jenkins.getInstance() != null) { - ProxyConfiguration proxy = Jenkins.getInstance().proxy; - - if (proxy != null) { - client.getHostConfiguration().setProxy(proxy.name, proxy.port); + public String getMentionNameForEmail(String email) { + HttpClient client = getHttpClient(); + String url = "https://" + server + "/v1/users/show"; + GetMethod get = new GetMethod(url); + get.setQueryString(new NameValuePair[] { + new NameValuePair("user_id", email), + new NameValuePair("auth_token", token) + }); + + try { + int responseCode = client.executeMethod(get); + if (responseCode == HttpStatus.SC_OK) { + return parseMentionNameFromUserResponse(get.getResponseBodyAsString()); + } else { + logger.log(Level.WARNING, "HipChat user lookup has failed with error: {0}", get.getResponseBodyAsString()); + return null; } + } catch (Exception e) { + logger.log(Level.WARNING, "Error looking up user from HipChat", e); + return null; + } finally { + get.releaseConnection(); } + } - return client; + private HttpClient getHttpClient() { + if (httpClient != null) { + return httpClient; + } else { + HttpClient client = new HttpClient(); + + if (Jenkins.getInstance() != null) { + ProxyConfiguration proxy = Jenkins.getInstance().proxy; + + if (proxy != null) { + client.getHostConfiguration().setProxy(proxy.name, proxy.port); + } + } + + return client; + } } private String shouldNotify(String color) { return color.equalsIgnoreCase("green") ? "0" : "1"; } + private String parseMentionNameFromUserResponse(String response) { + return JSONObject.fromObject(response).getJSONObject("user").getString("mention_name"); + } + public String getServer() { return server; } diff --git a/src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/config.jelly b/src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/config.jelly index 35ba7d4..a656103 100644 --- a/src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/config.jelly +++ b/src/main/resources/jenkins/plugins/hipchat/HipChatNotifier/config.jelly @@ -32,5 +32,9 @@ + + + + diff --git a/src/test/java/jenkins/plugins/hipchat/ActiveNotifierTest.java b/src/test/java/jenkins/plugins/hipchat/ActiveNotifierTest.java new file mode 100644 index 0000000..c09f7a1 --- /dev/null +++ b/src/test/java/jenkins/plugins/hipchat/ActiveNotifierTest.java @@ -0,0 +1,89 @@ +package jenkins.plugins.hipchat; + +import java.util.HashSet; +import java.util.Set; + +import hudson.model.Result; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.User; +import hudson.tasks.Mailer; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; +import static org.hamcrest.CoreMatchers.*; + +public class ActiveNotifierTest { + private HipChatService hipChatService; + private HipChatNotifier hipChatNotifier; + private ActiveNotifier activeNotifier; + + @SuppressWarnings("rawtypes") + private AbstractProject project; + @SuppressWarnings("rawtypes") + private AbstractBuild lastBuild; + @SuppressWarnings("rawtypes") + private AbstractBuild build; + + @Before + public void setUp() { + hipChatNotifier = mock(HipChatNotifier.class); + activeNotifier = new ActiveNotifier(hipChatNotifier); + + hipChatService = mock(HipChatService.class); + when(hipChatNotifier.newHipChatService()).thenReturn(hipChatService); + + project = mock(AbstractProject.class); + lastBuild = mock(AbstractBuild.class); + build = mock(AbstractBuild.class); + + when(build.getProject()).thenReturn(project); + when(project.getLastBuild()).thenReturn(lastBuild); + } + + @Test + public void addCulpritsToPublishedMessageWhenEnabled() { + givenTheBuildHasJustFailedAndWereListeningForFailures(); + givenThereWereThreeCulpritsOneOfWhichIsKnownToHipChat(); + + when(hipChatNotifier.isIncludeCulprits()).thenReturn(true); + activeNotifier.completed(build); + + thenHipChatNotificationShouldContain("Henrys Cat"); + thenHipChatNotificationShouldContain("@KnobCat"); + thenHipChatNotificationShouldContain("grumpy@cat.com"); + } + + private void givenTheBuildHasJustFailedAndWereListeningForFailures() { + when(build.getResult()).thenReturn(Result.FAILURE); + when(hipChatNotifier.isNotifyFailure()).thenReturn(true); + when(build.getUrl()).thenReturn("http://www.cats.com"); + } + + private void givenThereWereThreeCulpritsOneOfWhichIsKnownToHipChat() { + Set culprits = new HashSet(); + culprits.add(makeUser("Henrys Cat", null)); + culprits.add(makeUser("#KnobCat", "knob@cat.com")); + culprits.add(makeUser("Grumpy Cat", "grumpy@cat.com")); + when(build.getCulprits()).thenReturn(culprits); + + when(hipChatService.getMentionNameForEmail("knob@cat.com")).thenReturn("KnobCat"); + } + + private void thenHipChatNotificationShouldContain(String contains) { + verify(hipChatService).publish(argThat(containsString(contains)), anyString()); + } + + private User makeUser(String fullName, String email) { + User user = mock(User.class); + when(user.getFullName()).thenReturn(fullName); + if (email != null) { + Mailer.UserProperty userProperty = mock(Mailer.UserProperty.class); + when(userProperty.getAddress()).thenReturn(email); + when(user.getProperty(Mailer.UserProperty.class)).thenReturn(userProperty); + } + return user; + } +} diff --git a/src/test/java/jenkins/plugins/hipchat/StandardHipChatServiceTest.java b/src/test/java/jenkins/plugins/hipchat/StandardHipChatServiceTest.java index 96cbb4d..38d3f07 100644 --- a/src/test/java/jenkins/plugins/hipchat/StandardHipChatServiceTest.java +++ b/src/test/java/jenkins/plugins/hipchat/StandardHipChatServiceTest.java @@ -1,11 +1,37 @@ package jenkins.plugins.hipchat; +import hudson.util.ReflectionUtils; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Method; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.GetMethod; +import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class StandardHipChatServiceTest { + private HttpClient httpClient; + private StandardHipChatService standardHipChatService; + + @Before + public void setUp() { + httpClient = mock(HttpClient.class); + standardHipChatService = new StandardHipChatService(httpClient, "api.hipchat.com", "token", "room", "from"); + } + @Test public void publishWithBadHostShouldNotRethrowExceptions() { StandardHipChatService service = new StandardHipChatService("badhost", "token", "room", "from"); @@ -41,4 +67,60 @@ public void shouldBeAbleToOverrideFrom() { StandardHipChatService service = new StandardHipChatService(null, "token", "room", "from"); assertEquals("from", service.getSendAs()); } + + @Test + public void shouldReturnMentionNameBasedOnEmail() { + givenTheRequestForBobGeldofsUserReturnsOk(); + String mentionName = standardHipChatService.getMentionNameForEmail("bob@geldof.com"); + assertEquals("BobbieG", mentionName); + } + + @Test + public void shouldReturnNullMentionNameForInvalidResponse() { + givenTheRequestForRodJaneAndFreddyUserReturnsInvalidJson(); + String mentionName = standardHipChatService.getMentionNameForEmail("rodjanefreddy@rainbow.com"); + assertNull(mentionName); + } + + @Test + public void shouldReturnNullMentionNameForErrorResponse() { + givenTheRequestForBonoReturnsAnErrorResponse(); + String mentionName = standardHipChatService.getMentionNameForEmail("bono@therealu2.com"); + assertNull(mentionName); + } + + private void givenTheRequestForBobGeldofsUserReturnsOk() { + mockUpHttpClient(HttpStatus.SC_OK, "user_id=bob%40geldof.com&auth_token=token", + "{\"user\":{\"mention_name\":\"BobbieG\"}}"); + } + + private void givenTheRequestForRodJaneAndFreddyUserReturnsInvalidJson() { + mockUpHttpClient(HttpStatus.SC_OK, "user_id=rodjanefreddy%40rainbow.com&auth_token=token", + "RAAAAAAAAAAA"); + } + + private void givenTheRequestForBonoReturnsAnErrorResponse() { + mockUpHttpClient(HttpStatus.SC_FORBIDDEN, "user_id=bono%40therealu2.com&auth_token=token", + "{\"error\":{\"code\":401}"); + } + + private void mockUpHttpClient(final int responseCode, final String expectedQueryString, final String response) { + try { + when(httpClient.executeMethod(any(GetMethod.class))).thenAnswer(new Answer() { + public Integer answer(InvocationOnMock invocation) throws Throwable { + GetMethod get = invocation.getArgumentAt(0, GetMethod.class); + assertEquals(expectedQueryString, get.getQueryString()); + + Method method = HttpMethodBase.class.getDeclaredMethod("setResponseStream", InputStream.class); + method.setAccessible(true); + InputStream in = new ByteArrayInputStream(response.getBytes()); + ReflectionUtils.invokeMethod(method, get, new Object[] {in}); + + return responseCode; + } + }); + } catch(Exception ex) { + throw new RuntimeException(ex); + } + } }