Index: test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java =================================================================== --- test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java (revision 3558) +++ test/net/sourceforge/cruisecontrol/publishers/JiraPublisherTest.java (working copy) @@ -41,155 +41,210 @@ import net.sourceforge.cruisecontrol.util.XMLLogHelper; import org.jdom.Element; +import javax.xml.rpc.ServiceException; +import java.rmi.RemoteException; + /** - * Unit test for the Jabber publisher which publishes - * a link to the build results via Jabber Instant Messaging framework. - * Currently, tests are limited to message generation and parameter - * validation. Testing of the XMPPConnection mechanism will require a + * Unit test for the Jira publisher which creates a new issue + * for newly failed build via SOAP to a Jira instance. + * + * Testing of the SOAP publishing mechanism requires a * publicly accessible Jabber server to order to login as the recipient. - * Only then can this test represent 100% code coverage. + * Only then can this test represent 100% code coverage. This functional test is disabled. * - * @see net.sourceforge.cruisecontrol.publishers.JabberPublisher - * @see net.sourceforge.cruisecontrol.publishers.LinkJabberPublisher + * @see JiraPublisher * - * @author Jonas Edgeworth - * @version 1.0 + * @author Jerome Lacoste */ -public class LinkJabberPublisherTest extends TestCase { +public class JiraPublisherTest extends TestCase { - private LinkJabberPublisher publisher; + private JiraPublisher publisher; + private Element successLog; private XMLLogHelper successLogHelper; + private Element newFailureLog; + private Element reFailedLog; + private Element buildFixedLog; private String baseURLString = "http://mybuildserver.com:8080/buildservlet/BuildServlet"; protected void setUp() throws Exception { - successLogHelper = createLogHelper(true, true); - publisher = new LinkJabberPublisher(); + successLog = createLog(true, true); + successLogHelper = new XMLLogHelper(successLog); + newFailureLog = createLog(false, true); + reFailedLog = createLog(false, false); + buildFixedLog = createLog(true, false); + publisher = new JiraPublisher(); } protected XMLLogHelper createLogHelper(boolean success, boolean lastBuildSuccess) { + Element cruisecontrolElement = createLog(success, lastBuildSuccess); + + return new XMLLogHelper(cruisecontrolElement); + } + + private Element createLog(boolean success, boolean lastBuildSuccess) { Element cruisecontrolElement = new Element("cruisecontrol"); Element infoElement = new Element("info"); Element logFileElement = new Element("property"); logFileElement.setAttribute("name", "logfile"); - logFileElement.setAttribute("value", "log20020206120000.xml"); + logFileElement.setAttribute("value", "log20070927232937.xml"); infoElement.addContent(logFileElement); + cruisecontrolElement.addContent(infoElement); + Element timestampElement = new Element("property"); + timestampElement.setAttribute("name", "cctimestamp"); + timestampElement.setAttribute("value", "20070927232937"); + infoElement.addContent(timestampElement); + Element projectNameElement = new Element("property"); projectNameElement.setAttribute("name", "projectname"); - projectNameElement.setAttribute("value", "Jabbertest"); + projectNameElement.setAttribute("value", "Jiratest"); infoElement.addContent(projectNameElement); + Element lastBuildElement = new Element("property"); + lastBuildElement.setAttribute("name", "lastbuildsuccessful"); + lastBuildElement.setAttribute("value", "" + lastBuildSuccess); + infoElement.addContent(lastBuildElement); + Element buildElement = new Element("build"); if (!success) { buildElement.setAttribute("error", "Something went wrong"); } cruisecontrolElement.addContent(buildElement); - - return new XMLLogHelper(cruisecontrolElement); + return cruisecontrolElement; } /** - * Test the message creation process assuring that the - * generated string matches the associated build URL - * @throws CruiseControlException + * This one should be disabled most of the time as it publishes against official Jira. + * Cf http://jira.atlassian.com/browse/TST-12633 for example + * @throws Exception */ - public void testCreateMessage() throws CruiseControlException { - publisher.setBuildResultsURL(baseURLString); - assertEquals( - "Build results for successful build of project Jabbertest: " + baseURLString + "?log=log20020206120000", - publisher.createMessage(successLogHelper)); + public void disabled_testIntegrationTestPublishToOfficialJira() throws Exception { + JiraPublisherTest test = new JiraPublisherTest(); + test.setUp(); + Element log = test.createLog(false, true); + + // cf com.atlassian.jira_soapclient.ClientConstants + test.publisher.setUsername("soaptester"); + test.publisher.setPassword("soaptester"); + test.publisher.setJiraProject("TST"); + + test.publisher.setJiraUrl("http://jira.atlassian.com"); + + test.publisher.setBuildResultsURL(baseURLString); + + test.publisher.validate(); + test.publisher.publish(log); } + static class MockJiraPublisher extends JiraPublisher { + private XMLLogHelper helper = null; + + protected void createJiraIssue(XMLLogHelper helper) throws ServiceException, RemoteException, + CruiseControlException { + this.helper = helper; + } + + public boolean hasSentIssue() { + return helper != null; + } + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation1() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon success", false, successLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation2() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("only send upon new failure", true, newFailureLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation3() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon existing failure", false, reFailedLog); + } + + public void testOnlyNewBrokenBuildsTriggerIssueCreation4() throws CruiseControlException { + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + + assertPublishEquals("don't send upon build fixed", false, buildFixedLog); + } + + private void assertPublishEquals(String message, boolean expectedPublished, Element log) + throws CruiseControlException { + MockJiraPublisher pub1 = new MockJiraPublisher(); + pub1.publish(log); + assertEquals(message, expectedPublished, pub1.hasSentIssue()); + } + + /** * Test the message creation process assuring that the * generated string matches the associated build URL - * with additional parameters passed included. - * @throws CruiseControlException + * @throws net.sourceforge.cruisecontrol.CruiseControlException */ - public void testQuestionMarkInBuildResultsURL() throws CruiseControlException { - publisher.setBuildResultsURL(baseURLString + "?key=value"); - + public void testCreateDescription() throws CruiseControlException { + publisher.setBuildResultsURL(baseURLString); assertEquals( - "Build results for successful build of project Jabbertest: " - + baseURLString - + "?key=value&log=log20020206120000", - publisher.createMessage(successLogHelper)); + "CruiseControl build failed for project Jiratest\n" + + "\n" + + "Check the build results at: " + baseURLString + "?log=log20070927232937", + publisher.createDescription(successLogHelper)); } /** * Test all validation scenarios related to the - * configuration parameters for the LinkJabberPublisher. + * configuration parameters for the JiraPublisher. * Currently, the validation checks the following:

- * If host is null
- * If username is null
- * If password is null
- * If recipient is null
- * If buildResultsURL is null
- * If username is not of the form username@myserver.com
- * If recipient is of the form recipient@myserver.com

- * Validation has not been tested against groupchat configurations. + * validation fails if one required attribute is missing
+ * validation doesn't fail if no required attribute is missing
*/ public void testValidate() { - // Test if host is null - publisher.setPassword("asdfdsaf"); - publisher.setRecipient("recipient"); - publisher.setBuildResultsURL("http://foo.com"); + ensureValidateFailsOnRequiredMissingAttribute(null, "project", "username", "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", null, "username", "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", null, "password", baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", "username", null, baseURLString); + ensureValidateFailsOnRequiredMissingAttribute("url", "project", "username", "password", null); + + ensureValidateDoesntFailWhenNoMissingRequiredAttribute("url", "project", "username", "password", baseURLString); + } + + private void ensureValidateFailsOnRequiredMissingAttribute(String url, String jiraProject, String password, + String username, String buildResultsURL) { + publisher.setJiraUrl(url); + publisher.setJiraProject(jiraProject); + publisher.setUsername(username); + publisher.setPassword(password); + publisher.setBuildResultsURL(buildResultsURL); + try { publisher.validate(); - fail("should throw exception if host not set"); + fail("should throw exception if one required attribute is not set"); } catch (CruiseControlException e) { } - // Test if username is null - publisher.setHost("host"); - publisher.setUsername(null); + } + + private void ensureValidateDoesntFailWhenNoMissingRequiredAttribute(String url, String jiraProject, String password, + String username, String buildResultsURL) { + publisher.setJiraUrl(url); + publisher.setJiraProject(jiraProject); + publisher.setUsername(username); + publisher.setPassword(password); + publisher.setBuildResultsURL(buildResultsURL); + try { publisher.validate(); - fail("should throw exception if username not set"); } catch (CruiseControlException e) { + fail("should not throw exception if all required attribute are set"); } - // Test is username is of the incorrect form - publisher.setUsername("username@adsfdsaf.com"); - try { - publisher.validate(); - fail("should throw exception if username is of the wrong form"); - } catch (CruiseControlException e) { - } - // Test if password is null - publisher.setUsername("username"); - publisher.setPassword(null); - try { - publisher.validate(); - fail("should throw exception if password not set"); - } catch (CruiseControlException e) { - } - // Test if recipient is null - publisher.setPassword("adsfadsf"); - publisher.setRecipient(null); - try { - publisher.validate(); - fail("should throw exception if recipient not set"); - } catch (CruiseControlException e) { - } - // Test if recipient is of the incorrect form - publisher.setRecipient("recipient"); - try { - publisher.validate(); - fail("should throw exception if recipient is of the wrong form"); - } catch (CruiseControlException e) { - } - // Test if build results url is null - publisher.setRecipient("recipient@foo.com"); - publisher.setBuildResultsURL(null); - try { - publisher.validate(); - fail("should throw exception if BuildResultURL not set"); - } catch (CruiseControlException e) { - } } } \ No newline at end of file Index: src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java =================================================================== --- src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java (revision 0) +++ src/net/sourceforge/cruisecontrol/publishers/JiraPublisher.java (revision 0) @@ -0,0 +1,188 @@ +/******************************************************************************** + * CruiseControl, a Continuous Integration Toolkit + * Copyright (c) 2001-2003, ThoughtWorks, Inc. + * 200 E. Randolph, 25th Floor + * Chicago, IL 60601 USA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * + Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * + Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ********************************************************************************/ +package net.sourceforge.cruisecontrol.publishers; + +import _soapclient.JiraSoapService; +import _soapclient.JiraSoapServiceServiceLocator; +import com.atlassian.jira.rpc.soap.beans.RemoteIssue; +import net.sourceforge.cruisecontrol.CruiseControlException; +import net.sourceforge.cruisecontrol.Publisher; +import net.sourceforge.cruisecontrol.util.ValidationHelper; +import net.sourceforge.cruisecontrol.util.XMLLogHelper; +import org.apache.log4j.Logger; +import org.jdom.Element; + +import java.util.Calendar; + +/** + * Publish issues to Jira via SOAP upon newly failed builds. + * + * @author Jerome Lacoste + * @version 1.0 + */ + +public class JiraPublisher implements Publisher { + + private static final Logger LOG = Logger.getLogger(JiraPublisher.class); + + private String jiraUrl; + private String username; + private String password; + private String jiraProject; + private String issueType = "1"; + private String issuePriority = "1"; + + private String buildResultsURL; + + public void setBuildResultsURL(String buildResultsURL) { + this.buildResultsURL = buildResultsURL; + } + + public void setJiraUrl(String jiraUrl) { + this.jiraUrl = jiraUrl; + } + + public void setJiraProject(String jiraProject) { + this.jiraProject = jiraProject; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setIssueType(String issueType) { + this.issueType = issueType; + } + + public void setIssuePriority(String issuePriority) { + this.issuePriority = issuePriority; + } + + /** + * Validate that all the mandatory parameters were specified in order + * to properly initial the Jabber client service. Note that this is called + * after the configuration file is read. + * + * @throws net.sourceforge.cruisecontrol.CruiseControlException if there was a configuration error. + */ + public void validate() throws CruiseControlException { + + ValidationHelper.assertIsSet(jiraProject, "jiraProject", this.getClass()); + ValidationHelper.assertIsSet(username, "username", this.getClass()); + + ValidationHelper.assertIsSet(password, "password", this.getClass()); + ValidationHelper.assertIsSet(jiraUrl, "jiraUrl", this.getClass()); + + ValidationHelper.assertIsSet(buildResultsURL, "buildresulturl", this.getClass()); + } + + /** + * Publish the results to the Jabber transport via an instant message. + * + * @param cruisecontrolLog + * @throws net.sourceforge.cruisecontrol.CruiseControlException + */ + public void publish(Element cruisecontrolLog) throws CruiseControlException { + XMLLogHelper helper = new XMLLogHelper(cruisecontrolLog); + if (!helper.isBuildNewlyBroken()) { + LOG.debug("No new issue to report. Skipping."); + return; + } + + try { + createJiraIssue(helper); + } catch (Throwable e) { + LOG.error("Unable to send message to Jira", e); + } + } + + protected void createJiraIssue(XMLLogHelper helper) throws javax.xml.rpc.ServiceException, java.rmi.RemoteException, + CruiseControlException { + LOG.info("Creating a test issue on " + jiraUrl + " ..."); + JiraSoapServiceServiceLocator jiraSoapServiceGetter = new JiraSoapServiceServiceLocator(); + + jiraSoapServiceGetter.setJirasoapserviceV2EndpointAddress(jiraUrl + "/rpc/soap/jirasoapservice-v2"); + JiraSoapService jiraSoapService = jiraSoapServiceGetter.getJirasoapserviceV2(); + + String token = jiraSoapService.login(username, password); + + // Create the issue + RemoteIssue issue = new RemoteIssue(); + issue.setProject(jiraProject); + issue.setType(issueType); + issue.setPriority(issuePriority); + issue.setDuedate(Calendar.getInstance()); + + // RemoteComponent component = new RemoteComponent(); + // component.setId("10242"); + // issue.setComponents(new RemoteComponent[]{component}); + + issue.setSummary("CI build failed: " + helper.getBuildTimestamp()); + + // optional + // RemoteVersion version = new RemoteVersion(); + // version.setId("10330"); + // RemoteVersion[] remoteVersions = new RemoteVersion[]{version}; + // issue.setFixVersions(remoteVersions); + + String description = createDescription(helper); + + issue.setDescription(description); + + RemoteIssue returnedIssue = jiraSoapService.createIssue(token, issue); + LOG.info("Successfully created issue " + jiraUrl + "/browse/" + returnedIssue.getKey()); + } + + /** + * Creates the Issue description to be sent to Jira. + * + * @return String that makes up the body of the Issue description + * @throws net.sourceforge.cruisecontrol.CruiseControlException + */ + protected String createDescription(XMLLogHelper logHelper) throws CruiseControlException { + StringBuffer message = new StringBuffer(); + message.append("CruiseControl build failed for project ").append(logHelper.getProjectName()); + message.append("\n").append("\n"); + message.append("Check the build results at: "); + message.append(logHelper.createBuildResultsURL(buildResultsURL)); + return message.toString(); + } +} \ No newline at end of file