diff --git a/src/main/java/jenkins/plugins/slack/CredentialsObtainer.java b/src/main/java/jenkins/plugins/slack/CredentialsObtainer.java new file mode 100644 index 00000000..2358f88d --- /dev/null +++ b/src/main/java/jenkins/plugins/slack/CredentialsObtainer.java @@ -0,0 +1,57 @@ +package jenkins.plugins.slack; + +import com.cloudbees.plugins.credentials.CredentialsMatcher; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import hudson.model.Item; +import hudson.security.ACL; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; + +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +public class CredentialsObtainer { + + public static StringCredentials lookupCredentials(String credentialId) { + List credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()); + return getCredentialWithId(credentialId, credentials); + } + + public static StringCredentials lookupCredentials(String credentialId, Item item) { + List credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, item, ACL.SYSTEM, Collections.emptyList()); + return getCredentialWithId(credentialId, credentials); + } + + /** + * Attempts to obtain the credential with the providedId from the item's credential context, otherwise returns token + * @param credentialId the id from the credential to be used + * @param item the item with the context to obtain the credential from. + * @param token the fallback token + * @return the obtained token + */ + public static String getTokenToUse(String credentialId, Item item, String token) { + String response; + if (StringUtils.isEmpty(credentialId)) { + response = token; + } else { + StringCredentials credentials = lookupCredentials(StringUtils.trim(credentialId), item); + if (credentials != null) { + response = credentials.getSecret().getPlainText(); + } else { + response = token; + } + } + if (StringUtils.isEmpty(response)) { + throw new IllegalArgumentException("the token with the provided ID could not be found and no token was specified"); + } + return response; + } + + private static StringCredentials getCredentialWithId(String credentialId, List credentials) { + CredentialsMatcher matcher = CredentialsMatchers.withId(credentialId); + return CredentialsMatchers.firstOrNull(credentials, matcher); + } +} diff --git a/src/main/java/jenkins/plugins/slack/SlackNotifier.java b/src/main/java/jenkins/plugins/slack/SlackNotifier.java index f39c7345..28417068 100755 --- a/src/main/java/jenkins/plugins/slack/SlackNotifier.java +++ b/src/main/java/jenkins/plugins/slack/SlackNotifier.java @@ -1,5 +1,6 @@ package jenkins.plugins.slack; +import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.HostnameRequirement; import hudson.EnvVars; @@ -10,6 +11,7 @@ import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Item; +import hudson.model.Project; import hudson.security.ACL; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; @@ -33,6 +35,7 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import java.util.NoSuchElementException; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -405,7 +408,7 @@ public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } - public SlackService newSlackService(AbstractBuild r, BuildListener listener) { + public SlackService newSlackService(AbstractBuild abstractBuild, BuildListener listener) { DescriptorImpl descriptor = getDescriptor(); String teamDomain = Util.fixEmpty(this.teamDomain) != null ? this.teamDomain : descriptor.getTeamDomain(); String baseUrl = Util.fixEmpty(this.baseUrl) != null ? this.baseUrl : descriptor.getBaseUrl(); @@ -417,7 +420,7 @@ public SlackService newSlackService(AbstractBuild r, BuildListener listener) { EnvVars env; try { - env = r.getEnvironment(listener); + env = abstractBuild.getEnvironment(listener); } catch (Exception e) { listener.getLogger().println("Error retrieving environment vars: " + e.getMessage()); env = new EnvVars(); @@ -427,8 +430,8 @@ public SlackService newSlackService(AbstractBuild r, BuildListener listener) { authToken = env.expand(authToken); authTokenCredentialId = env.expand(authTokenCredentialId); room = env.expand(room); - - return new StandardSlackService(baseUrl, teamDomain, authToken, authTokenCredentialId, botUser, room); + final String populatedToken = CredentialsObtainer.getTokenToUse(authTokenCredentialId, abstractBuild.getParent(), authToken); + return new StandardSlackService(baseUrl, teamDomain, botUser, room, false, populatedToken); } @Override @@ -442,20 +445,29 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen BuildAwareLogger log = createLogger(listener); log.debug(buildKey, "Performing complete notifications"); JenkinsTokenExpander tokenExpander = new JenkinsTokenExpander(listener); - new ActiveNotifier(this, slackFactory(listener), log, tokenExpander).completed(build); - if (notifyRegression) { - log.debug(buildKey, "Performing finalize notifications"); - new ActiveNotifier(this, slackFactory(listener), log, tokenExpander).finalized(build); + try { + new ActiveNotifier(this, slackFactory(listener), log, tokenExpander).completed(build); + if (notifyRegression) { + log.debug(buildKey, "Performing finalize notifications"); + new ActiveNotifier(this, slackFactory(listener), log, tokenExpander).finalized(build); + } + } catch (Exception e) { + log.info(buildKey,"Exception attempting Slack notification: " + e.getMessage()); } return true; } @Override public boolean prebuild(AbstractBuild build, BuildListener listener) { - if (startNotification) { - BuildAwareLogger log = createLogger(listener); - log.debug(BuildKey.format(build), "Performing start notifications"); - new ActiveNotifier(this, slackFactory(listener), log, new JenkinsTokenExpander(listener)).started(build); + String buildKey = BuildKey.format(build); + BuildAwareLogger log = createLogger(listener); + try { + if (startNotification) { + log.debug(buildKey, "Performing start notifications"); + new ActiveNotifier(this, slackFactory(listener), log, new JenkinsTokenExpander(listener)).started(build); + } + } catch (Exception e) { + log.info(buildKey,"Exception attempting Slack notification: " + e.getMessage()); } return super.prebuild(build, listener); } @@ -604,8 +616,21 @@ public boolean configure(StaplerRequest req, JSONObject formData) { return true; } - SlackService getSlackService(final String baseUrl, final String teamDomain, final String authTokenCredentialId, final boolean botUser, final String room) { - return new StandardSlackService(baseUrl, teamDomain, authTokenCredentialId, botUser, room); + /** + * @deprecated use {@link #getSlackService(String, String, String, boolean, String, Item)} instead} + */ + @Deprecated + SlackService getSlackService(final String baseUrl, final String teamDomain, final String authTokenCredentialId, final boolean botUser, final String roomId) { + return getSlackService(baseUrl, teamDomain, authTokenCredentialId, botUser, roomId, null); + } + + SlackService getSlackService(final String baseUrl, final String teamDomain, final String authTokenCredentialId, final boolean botUser, final String roomId, final Item item) { + final String populatedToken = CredentialsObtainer.getTokenToUse(authTokenCredentialId, item,null ); + if (populatedToken != null) { + return new StandardSlackService(baseUrl, teamDomain, botUser, roomId, false, populatedToken); + } else { + throw new NoSuchElementException("Could not obtain credentials with credential id: " + authTokenCredentialId); + } } @Nonnull @@ -618,7 +643,8 @@ public FormValidation doTestConnection(@QueryParameter("baseUrl") final String b @QueryParameter("teamDomain") final String teamDomain, @QueryParameter("tokenCredentialId") final String tokenCredentialId, @QueryParameter("botUser") final boolean botUser, - @QueryParameter("room") final String room) { + @QueryParameter("room") final String room, + @AncestorInPath Project project) { try { String targetUrl = baseUrl; @@ -636,7 +662,7 @@ public FormValidation doTestConnection(@QueryParameter("baseUrl") final String b this.tokenCredentialId; String targetRoom = Util.fixEmpty(room) != null ? room : this.room; - SlackService testSlackService = getSlackService(targetUrl, targetDomain, targetTokenCredentialId, targetBotUser, targetRoom); + SlackService testSlackService = getSlackService(targetUrl, targetDomain, targetTokenCredentialId, targetBotUser, targetRoom, project); String message = "Slack/Jenkins plugin: you're all set on " + DisplayURLProvider.get().getRoot(); boolean success = testSlackService.publish(message, "good"); return success ? FormValidation.ok("Success") : FormValidation.error("Failure"); diff --git a/src/main/java/jenkins/plugins/slack/StandardSlackService.java b/src/main/java/jenkins/plugins/slack/StandardSlackService.java index 0edd2eb1..160fe1e3 100755 --- a/src/main/java/jenkins/plugins/slack/StandardSlackService.java +++ b/src/main/java/jenkins/plugins/slack/StandardSlackService.java @@ -3,6 +3,7 @@ import com.cloudbees.plugins.credentials.CredentialsMatcher; import com.cloudbees.plugins.credentials.CredentialsMatchers; import hudson.ProxyConfiguration; +import hudson.model.Item; import hudson.security.ACL; import jenkins.model.Jenkins; import jenkins.plugins.slack.logging.BuildKey; @@ -44,31 +45,67 @@ public class StandardSlackService implements SlackService { private String host = "slack.com"; private String baseUrl; private String teamDomain; - private String token; - private String authTokenCredentialId; private boolean botUser; private String[] roomIds; private boolean replyBroadcast; - private String responseString = null; + private String responseString; + private String populatedToken; + /** + * @deprecated use {@link #StandardSlackService(String, String, boolean, String, boolean, String)} instead} + */ + @Deprecated public StandardSlackService(String baseUrl, String teamDomain, String authTokenCredentialId, boolean botUser, String roomId) { this(baseUrl, teamDomain, null, authTokenCredentialId, botUser, roomId, false); } + /** + * @deprecated use {@link #StandardSlackService(String, String, boolean, String, boolean, String)} instead} + */ + @Deprecated public StandardSlackService(String baseUrl, String teamDomain, String token, String authTokenCredentialId, boolean botUser, String roomId) { this(baseUrl, teamDomain, token, authTokenCredentialId, botUser, roomId, false); } + /** + * @deprecated use {@link #StandardSlackService(String, String, boolean, String, boolean, String)} instead} + */ + @Deprecated public StandardSlackService(String baseUrl, String teamDomain, String token, String authTokenCredentialId, boolean botUser, String roomId, boolean replyBroadcast) { + this(baseUrl, teamDomain, botUser, roomId, replyBroadcast); + this.populatedToken = getTokenToUse(authTokenCredentialId, token); + if (this.populatedToken == null) { + throw new IllegalArgumentException("No slack token found, setup a secret text credential and configure it to be used"); + } + } + + /** + * @param baseUrl the full url to use, this is an alternative to specifying teamDomain + * @param teamDomain the teamDomain inside slack.com to use + * @param botUser + * @param roomId a semicolon separated list of rooms to notify + * @param replyBroadcast + * @param populatedToken a non-null token to use for authentication + */ + public StandardSlackService(String baseUrl, String teamDomain, boolean botUser, String roomId, boolean replyBroadcast, String populatedToken) { + this(baseUrl, teamDomain, botUser, roomId, replyBroadcast); + if (populatedToken == null) { + throw new IllegalArgumentException("No slack token found, setup a secret text credential and configure it to be used"); + } + this.populatedToken = populatedToken; + } + + private StandardSlackService(String baseUrl, String teamDomain, boolean botUser, String roomId, boolean replyBroadcast) { super(); this.baseUrl = baseUrl; if(this.baseUrl != null && !this.baseUrl.isEmpty() && !this.baseUrl.endsWith("/")) { this.baseUrl += "/"; } this.teamDomain = teamDomain; - this.token = token; - this.authTokenCredentialId = StringUtils.trim(authTokenCredentialId); this.botUser = botUser; + if (roomId == null) { + throw new IllegalArgumentException("Project Channel or Slack User ID must be specified."); + } this.roomIds = roomId.split("[,; ]+"); this.replyBroadcast = replyBroadcast; } @@ -120,12 +157,11 @@ public boolean publish(String message, JSONArray attachments, String color) { roomId = splitThread[0]; threadTs = splitThread[1]; } - //prepare post methods for both requests types if (!botUser || !StringUtils.isEmpty(baseUrl)) { - url = "https://" + teamDomain + "." + host + "/services/hooks/jenkins-ci?token=" + getTokenToUse(); + url = "https://" + teamDomain + "." + host + "/services/hooks/jenkins-ci?token=" + populatedToken; if (!StringUtils.isEmpty(baseUrl)) { - url = baseUrl + getTokenToUse(); + url = baseUrl + populatedToken; } post = new HttpPost(url); JSONObject json = new JSONObject(); @@ -139,7 +175,7 @@ public boolean publish(String message, JSONArray attachments, String color) { nvps.add(new BasicNameValuePair("payload", json.toString())); } else { - url = "https://slack.com/api/chat.postMessage?token=" + getTokenToUse() + + url = "https://slack.com/api/chat.postMessage?token=" + populatedToken + "&channel=" + roomId.replace("#", "") + "&link_names=1" + "&as_user=true"; @@ -185,9 +221,9 @@ public boolean publish(String message, JSONArray attachments, String color) { return result; } - private String getTokenToUse() { - if (authTokenCredentialId != null && !authTokenCredentialId.isEmpty()) { - StringCredentials credentials = lookupCredentials(authTokenCredentialId); + private String getTokenToUse(String authTokenCredentialId, String token) { + if (!StringUtils.isEmpty(authTokenCredentialId)) { + StringCredentials credentials = CredentialsObtainer.lookupCredentials(authTokenCredentialId); if (credentials != null) { logger.fine("Using Integration Token Credential ID."); return credentials.getSecret().getPlainText(); @@ -195,16 +231,9 @@ private String getTokenToUse() { } logger.fine("Using Integration Token."); - return token; } - private StringCredentials lookupCredentials(String credentialId) { - List credentials = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()); - CredentialsMatcher matcher = CredentialsMatchers.withId(credentialId); - return CredentialsMatchers.firstOrNull(credentials, matcher); - } - protected CloseableHttpClient getHttpClient() { final HttpClientBuilder clientBuilder = HttpClients.custom(); final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); diff --git a/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java b/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java index ae92bf5d..0d024dc6 100644 --- a/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java +++ b/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java @@ -15,6 +15,7 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; +import jenkins.plugins.slack.CredentialsObtainer; import jenkins.plugins.slack.Messages; import jenkins.plugins.slack.SlackNotifier; import jenkins.plugins.slack.SlackService; @@ -34,9 +35,11 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import java.util.logging.Level; import java.util.Objects; import java.util.Set; +import java.util.logging.Logger; import javax.annotation.Nonnull; import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials; @@ -46,6 +49,8 @@ */ public class SlackSendStep extends Step { + private static final Logger logger = Logger.getLogger(SlackSendStep.class.getName()); + private String message; private String color; private String token; @@ -58,7 +63,6 @@ public class SlackSendStep extends Step { private Object attachments; private boolean replyBroadcast; - @Nonnull public String getMessage() { return message; @@ -190,12 +194,12 @@ public String getDisplayName() { return Messages.SlackSendStepDisplayName(); } - public ListBoxModel doFillTokenCredentialIdItems(@AncestorInPath Project project) { + public ListBoxModel doFillTokenCredentialIdItems(@AncestorInPath Item item) { Jenkins jenkins = Jenkins.get(); - if (project == null && !jenkins.hasPermission(Jenkins.ADMINISTER) || - project != null && !project.hasPermission(Item.EXTENDED_READ)) { + if (item == null && !jenkins.hasPermission(Jenkins.ADMINISTER) || + item != null && !item.hasPermission(Item.EXTENDED_READ)) { return new StandardListBoxModel(); } @@ -203,7 +207,7 @@ public ListBoxModel doFillTokenCredentialIdItems(@AncestorInPath Project project .withEmptySelection() .withAll(lookupCredentials( StringCredentials.class, - project, + item, ACL.SYSTEM, new HostnameRequirement("*.slack.com")) ); @@ -232,7 +236,7 @@ public static class SlackSendStepExecution extends SynchronousNonBlockingStepExe protected SlackResponse run() throws Exception { Jenkins jenkins = Jenkins.get(); - + Item item = getItemForCredentials(); SlackNotifier.DescriptorImpl slackDesc = jenkins.getDescriptorByType(SlackNotifier.DescriptorImpl.class); String baseUrl = step.baseUrl != null ? step.baseUrl : slackDesc.getBaseUrl(); @@ -251,10 +255,17 @@ protected SlackResponse run() throws Exception { defaultIfEmpty(baseUrl), defaultIfEmpty(teamDomain), channel, defaultIfEmpty(color), botUser, defaultIfEmpty(tokenCredentialId)) ); + final String populatedToken; + try { + populatedToken = CredentialsObtainer.getTokenToUse(tokenCredentialId, item, token); + } catch (IllegalArgumentException e) { + listener.error(Messages + .NotificationFailedWithException(e)); + return null; + } SlackService slackService = getSlackService( - baseUrl, teamDomain, token, tokenCredentialId, botUser, channel, step.replyBroadcast - ); + baseUrl, teamDomain, botUser, channel, step.replyBroadcast, populatedToken); final boolean publishSuccess; if (step.attachments != null) { JSONArray jsonArray = getAttachmentsAsJSONArray(); @@ -300,13 +311,12 @@ protected SlackResponse run() throws Exception { JSONArray getAttachmentsAsJSONArray() throws Exception { final TaskListener listener = getContext().get(TaskListener.class); - final String jsonString; - if (step.attachments instanceof String) { - jsonString = (String) step.attachments; - } - else { - jsonString = JsonOutput.toJson(step.attachments); - } + final String jsonString; + if (step.attachments instanceof String) { + jsonString = (String) step.attachments; + } else { + jsonString = JsonOutput.toJson(step.attachments); + } JsonSlurper jsonSlurper = new JsonSlurper(); JSON json = null; @@ -316,20 +326,46 @@ JSONArray getAttachmentsAsJSONArray() throws Exception { listener.error(Messages.NotificationFailedWithException(e)); return null; } - if(!(json instanceof JSONArray)){ + if (!(json instanceof JSONArray)) { listener.error(Messages.NotificationFailedWithException(new IllegalArgumentException("Attachments must be JSONArray"))); return null; } return (JSONArray) json; } + /** + * Tries to obtain the proper Item object to provide to CredentialsProvider. + * Project works for freestyle jobs, the parent of the Run works for pipelines. + * In case the proper item cannot be found, null is returned, since when null is provided to CredentialsProvider, + * it will internally use Jenkins.getInstance() which effectively only allows global credentials. + * + * @return the item to use for CredentialsProvider credential lookup + */ + private Item getItemForCredentials() { + Item item = null; + try { + item = getContext().get(Project.class); + if (item == null) { + Run run = getContext().get(Run.class); + if (run != null) { + item = run.getParent(); + } else { + item = null; + } + } + } catch (Exception e) { + logger.log(Level.INFO, "Exception obtaining item for credentials lookup. Only global credentials will be available", e); + } + return item; + } + private String defaultIfEmpty(String value) { return Util.fixEmpty(value) != null ? value : Messages.SlackSendStepValuesEmptyMessage(); } //streamline unit testing - SlackService getSlackService(String baseUrl, String team, String token, String tokenCredentialId, boolean botUser, String channel, boolean replyBroadcast) { - return new StandardSlackService(baseUrl, team, token, tokenCredentialId, botUser, channel, replyBroadcast); + SlackService getSlackService(String baseUrl, String team, boolean botUser, String channel, boolean replyBroadcast, String populatedToken) { + return new StandardSlackService(baseUrl, team, botUser, channel, replyBroadcast, populatedToken); } } } diff --git a/src/test/java/jenkins/plugins/slack/CloseableHttpClientStub.java b/src/test/java/jenkins/plugins/slack/CloseableHttpClientStub.java index 4ea06df6..298fde83 100644 --- a/src/test/java/jenkins/plugins/slack/CloseableHttpClientStub.java +++ b/src/test/java/jenkins/plugins/slack/CloseableHttpClientStub.java @@ -16,8 +16,10 @@ public class CloseableHttpClientStub extends CloseableHttpClient { private int numberOfCallsToExecuteMethod; private int httpStatus; private boolean failAlternateResponses = false; + private HttpUriRequest lastRequest = null; public CloseableHttpResponse execute(HttpUriRequest post) { + lastRequest = post; numberOfCallsToExecuteMethod++; if (failAlternateResponses && (numberOfCallsToExecuteMethod % 2 == 0)) { return new CloseableHttpResponseStub(HttpStatus.SC_NOT_FOUND); @@ -57,4 +59,8 @@ public void setHttpStatus(int httpStatus) { public void setFailAlternateResponses(boolean failAlternateResponses) { this.failAlternateResponses = failAlternateResponses; } + + public HttpUriRequest getLastRequest() { + return lastRequest; + } } diff --git a/src/test/java/jenkins/plugins/slack/SlackNotifierStub.java b/src/test/java/jenkins/plugins/slack/SlackNotifierStub.java index 440625d0..aafb57cf 100644 --- a/src/test/java/jenkins/plugins/slack/SlackNotifierStub.java +++ b/src/test/java/jenkins/plugins/slack/SlackNotifierStub.java @@ -1,5 +1,7 @@ package jenkins.plugins.slack; +import hudson.model.Item; + public class SlackNotifierStub extends SlackNotifier { public SlackNotifierStub(String baseUrl, String teamDomain, String authToken, boolean botUser, String room, String authTokenCredentialId, @@ -23,7 +25,7 @@ public synchronized void load() { } @Override - SlackService getSlackService(final String baseUrl, final String teamDomain, final String authTokenCredentialId, final boolean botUser, final String room) { + SlackService getSlackService(final String baseUrl, final String teamDomain, final String authTokenCredentialId, final boolean botUser, final String room, final Item item) { return slackService; } diff --git a/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java b/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java index 2a9070bd..8ec62b46 100644 --- a/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java +++ b/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java @@ -52,7 +52,7 @@ public void testDoTestConnection() { } descriptor.setSlackService(slackServiceStub); FormValidation result = descriptor - .doTestConnection("baseUrl", "teamDomain", "authTokenCredentialId", false, "room"); + .doTestConnection("baseUrl", "teamDomain", "authTokenCredentialId", false, "room", null); assertEquals(result.kind, expectedResult); } diff --git a/src/test/java/jenkins/plugins/slack/StandardSlackServiceStub.java b/src/test/java/jenkins/plugins/slack/StandardSlackServiceStub.java index bf2763f5..62eba477 100644 --- a/src/test/java/jenkins/plugins/slack/StandardSlackServiceStub.java +++ b/src/test/java/jenkins/plugins/slack/StandardSlackServiceStub.java @@ -4,8 +4,8 @@ public class StandardSlackServiceStub extends StandardSlackService { private CloseableHttpClientStub httpClientStub; - public StandardSlackServiceStub(String baseUrl, String teamDomain, String token, String tokenCredentialId, boolean botUser, String roomId) { - super(baseUrl, teamDomain, token, tokenCredentialId, botUser, roomId); + public StandardSlackServiceStub(String baseUrl, String teamDomain, boolean botUser, String roomId, String populatedToken) { + super(baseUrl, teamDomain, botUser, roomId, false, populatedToken); } @Override diff --git a/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java b/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java index 402cc6ea..44cb1e58 100755 --- a/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java +++ b/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java @@ -13,7 +13,7 @@ public class StandardSlackServiceTest { */ @Test public void publishWithBadHostShouldNotRethrowExceptions() { - StandardSlackService service = new StandardSlackService("", "foo", "token", null, false, "#general"); + StandardSlackService service = new StandardSlackService("", "foo", false, "#general", false, "token"); service.setHost("hostvaluethatwillcausepublishtofail"); service.publish("message"); } @@ -23,7 +23,7 @@ public void publishWithBadHostShouldNotRethrowExceptions() { */ @Test public void invalidTeamDomainShouldFail() { - StandardSlackService service = new StandardSlackService("", "my", "token", null, false, "#general"); + StandardSlackService service = new StandardSlackService("", "my", false, "#general", false, "token"); service.publish("message"); } @@ -32,13 +32,13 @@ public void invalidTeamDomainShouldFail() { */ @Test public void invalidTokenShouldFail() { - StandardSlackService service = new StandardSlackService("", "tinyspeck", "token", null, false, "#general"); + StandardSlackService service = new StandardSlackService("", "tinyspeck", false, "#general", false, "token"); service.publish("message"); } @Test public void publishToASingleRoomSendsASingleMessage() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); service.setHttpClient(httpClientStub); service.publish("message"); @@ -47,7 +47,7 @@ public void publishToASingleRoomSendsASingleMessage() { @Test public void publishToMultipleRoomsSendsAMessageToEveryRoom() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1,#room2,#room3"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1,#room2,#room3", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); service.setHttpClient(httpClientStub); service.publish("message"); @@ -56,7 +56,17 @@ public void publishToMultipleRoomsSendsAMessageToEveryRoom() { @Test public void successfulPublishToASingleRoomReturnsTrue() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1", "token"); + CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); + httpClientStub.setHttpStatus(HttpStatus.SC_OK); + service.setHttpClient(httpClientStub); + assertTrue(service.publish("message")); + } + + + @Test + public void successfulPublishToSingleRoomWithProvidedTokenReturnsTrue() { + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1", "providedtoken"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_OK); service.setHttpClient(httpClientStub); @@ -65,7 +75,7 @@ public void successfulPublishToASingleRoomReturnsTrue() { @Test public void successfulPublishToMultipleRoomsReturnsTrue() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1,#room2,#room3"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1,#room2,#room3", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_OK); service.setHttpClient(httpClientStub); @@ -74,7 +84,7 @@ public void successfulPublishToMultipleRoomsReturnsTrue() { @Test public void failedPublishToASingleRoomReturnsFalse() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "#room1", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_NOT_FOUND); service.setHttpClient(httpClientStub); @@ -83,7 +93,7 @@ public void failedPublishToASingleRoomReturnsFalse() { @Test public void singleFailedPublishToMultipleRoomsReturnsFalse() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, "#room1,#room2,#room3"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain",false, "#room1,#room2,#room3", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setFailAlternateResponses(true); httpClientStub.setHttpStatus(HttpStatus.SC_OK); @@ -93,7 +103,7 @@ public void singleFailedPublishToMultipleRoomsReturnsFalse() { @Test public void publishToEmptyRoomReturnsTrue() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, false, ""); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", false, "", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_OK); service.setHttpClient(httpClientStub); @@ -103,7 +113,7 @@ public void publishToEmptyRoomReturnsTrue() { @Test public void sendAsBotUserReturnsTrue() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, true, "#room1"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", true, "#room1", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_OK); service.setHttpClient(httpClientStub); @@ -112,10 +122,21 @@ public void sendAsBotUserReturnsTrue() { @Test public void sendAsBotUserInThreadReturnsTrue() { - StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", "token", null, true, "#room1:1528317530"); + StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", true, "#room1:1528317530", "token"); CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); httpClientStub.setHttpStatus(HttpStatus.SC_OK); service.setHttpClient(httpClientStub); assertTrue(service.publish("message")); } + + @Test + public void populatedTokenIsUsed() { + final String populatedToken = "secret-text"; + final StandardSlackServiceStub service = new StandardSlackServiceStub("", "domain", true, "#room1:1528317530", populatedToken); + final CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); + httpClientStub.setHttpStatus(HttpStatus.SC_OK); + service.setHttpClient(httpClientStub); + service.publish("message"); + assertTrue(httpClientStub.getLastRequest().getURI().toString().contains(populatedToken)); + } } diff --git a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java index f8b7e7e1..08f91cb7 100644 --- a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java +++ b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java @@ -1,7 +1,11 @@ package jenkins.plugins.slack.workflow; +import hudson.model.Item; +import hudson.model.Project; +import hudson.model.Run; import hudson.model.TaskListener; import jenkins.model.Jenkins; +import jenkins.plugins.slack.CredentialsObtainer; import jenkins.plugins.slack.SlackNotifier; import jenkins.plugins.slack.SlackService; import net.sf.json.JSONArray; @@ -25,6 +29,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doNothing; @@ -37,7 +44,7 @@ * Traditional Unit tests, allows testing null Jenkins.get() */ @RunWith(PowerMockRunner.class) -@PrepareForTest({Jenkins.class,SlackSendStep.class}) +@PrepareForTest({Jenkins.class,SlackSendStep.class, CredentialsObtainer.class}) public class SlackSendStepTest { @Mock @@ -47,6 +54,10 @@ public class SlackSendStepTest { @Mock StepContext stepContextMock; @Mock + Project project; + @Mock + Run run; + @Mock SlackService slackServiceMock; @Mock Jenkins jenkins; @@ -56,37 +67,43 @@ public class SlackSendStepTest { @Before public void setUp() throws IOException, InterruptedException { PowerMockito.mockStatic(Jenkins.class); + PowerMockito.mockStatic(CredentialsObtainer.class); when(jenkins.getDescriptorByType(SlackNotifier.DescriptorImpl.class)).thenReturn(slackDescMock); + PowerMockito.when(Jenkins.getInstance()).thenReturn(jenkins); when(taskListenerMock.getLogger()).thenReturn(printStreamMock); when(stepContextMock.get(TaskListener.class)).thenReturn(taskListenerMock); } @Test public void testStepOverrides() throws Exception { + final String token = "mytoken"; SlackSendStep slackSendStep = new SlackSendStep(); slackSendStep.setMessage("message"); - slackSendStep.setToken("token"); + slackSendStep.setToken(token); slackSendStep.setTokenCredentialId("tokenCredentialId"); slackSendStep.setBotUser(true); slackSendStep.setBaseUrl("baseUrl/"); slackSendStep.setTeamDomain("teamDomain"); slackSendStep.setChannel("channel"); slackSendStep.setColor("good"); - SlackSendStep.SlackSendStepExecution stepExecution = spy(new SlackSendStep.SlackSendStepExecution(slackSendStep, stepContextMock)); when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), any(Item.class), anyString())).thenReturn(token); + + when(stepContextMock.get(Project.class)).thenReturn(project); + when(slackDescMock.isBotUser()).thenReturn(false); when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); when(slackServiceMock.publish(anyString(), anyString())).thenReturn(true); stepExecution.run(); - verify(stepExecution, times(1)).getSlackService("baseUrl/", "teamDomain", "token", "tokenCredentialId", true, "channel", false); + verify(stepExecution, times(1)).getSlackService("baseUrl/", "teamDomain", true, "channel", false, token); verify(slackServiceMock, times(1)).publish("message", "good"); } @@ -106,10 +123,12 @@ public void testStepWithAttachments() throws Exception { when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), any(Item.class), anyString())).thenReturn("token"); + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString() )).thenReturn(slackServiceMock); stepExecution.run(); verify(slackServiceMock, times(0)).publish("message", ""); @@ -132,10 +151,12 @@ public void testStepWithAttachmentsAsListOfMap() throws Exception { when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), any(Item.class), anyString())).thenReturn("token"); + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); stepExecution.run(); verify(slackServiceMock, times(0)).publish("message", ""); @@ -155,8 +176,41 @@ public void testValuesForGlobalConfig() throws Exception { SlackSendStep step = new SlackSendStep(); step.setMessage("message"); + SlackSendStep.SlackSendStepExecution stepExecution = spy(new SlackSendStep.SlackSendStepExecution(step, stepContextMock)); + + when(Jenkins.get()).thenReturn(jenkins); + + PowerMockito.when(CredentialsObtainer.getTokenToUse(eq("globalTokenCredentialId"), any(Item.class), anyString())).thenReturn("token2"); + + when(stepContextMock.get(Project.class)).thenReturn(project); + + when(slackDescMock.getBaseUrl()).thenReturn("globalBaseUrl"); + when(slackDescMock.getTeamDomain()).thenReturn("globalTeamDomain"); + when(slackDescMock.getTokenCredentialId()).thenReturn("globalTokenCredentialId"); + when(slackDescMock.isBotUser()).thenReturn(false); + when(slackDescMock.getRoom()).thenReturn("globalChannel"); + + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); + doNothing().when(printStreamMock).println(); + + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); + + stepExecution.run(); + verify(stepExecution, times(1)).getSlackService("globalBaseUrl", "globalTeamDomain",false, "globalChannel", false, "token2"); + verify(slackServiceMock, times(1)).publish("message", ""); + } + + + @Test + public void testCanGetItemFromRun() throws Exception { + SlackSendStep step = new SlackSendStep(); + step.setMessage("message"); + SlackSendStep.SlackSendStepExecution stepExecution = spy(new SlackSendStep.SlackSendStepExecution(step, stepContextMock)); when(Jenkins.get()).thenReturn(jenkins); + when(stepContextMock.get(Run.class)).thenReturn(run); + when(run.getParent()).thenReturn(project); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), eq(project), anyString())).thenReturn("runcredentials"); when(slackDescMock.getBaseUrl()).thenReturn("globalBaseUrl"); when(slackDescMock.getTeamDomain()).thenReturn("globalTeamDomain"); @@ -167,10 +221,12 @@ public void testValuesForGlobalConfig() throws Exception { when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); stepExecution.run(); - verify(stepExecution, times(1)).getSlackService("globalBaseUrl", "globalTeamDomain", null, "globalTokenCredentialId", false, "globalChannel", false); + + verify(stepExecution, times(1)).getSlackService("globalBaseUrl", "globalTeamDomain", + false, "globalChannel", false, "runcredentials"); verify(slackServiceMock, times(1)).publish("message", ""); } @@ -184,6 +240,10 @@ public void testReplyBroadcast() throws Exception { when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(eq("globalTokenCredentialId"), any(Item.class), anyString())).thenReturn("token"); + + when(stepContextMock.get(Project.class)).thenReturn(project); + when(slackDescMock.getBaseUrl()).thenReturn("globalBaseUrl"); when(slackDescMock.getTeamDomain()).thenReturn("globalTeamDomain"); when(slackDescMock.getTokenCredentialId()).thenReturn("globalTokenCredentialId"); @@ -193,10 +253,10 @@ public void testReplyBroadcast() throws Exception { when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); stepExecution.run(); - verify(stepExecution, times(1)).getSlackService("globalBaseUrl", "globalTeamDomain", null, "globalTokenCredentialId", false, "globalChannel", true); + verify(stepExecution, times(1)).getSlackService("globalBaseUrl", "globalTeamDomain", false, "globalChannel", true, "token"); verify(slackServiceMock, times(1)).publish("message", ""); } @@ -207,12 +267,15 @@ public void testNonNullEmptyColor() throws Exception { step.setColor(""); SlackSendStep.SlackSendStepExecution stepExecution = spy(new SlackSendStep.SlackSendStepExecution(step, stepContextMock)); + when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), any(Item.class), anyString())).thenReturn("token"); + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); stepExecution.run(); verify(slackServiceMock, times(1)).publish("message", ""); @@ -234,10 +297,12 @@ public void testSlackResponseObject() throws Exception { when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(anyString(), any(Item.class), anyString())).thenReturn("token"); + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); String savedResponse = IOUtils.toString( this.getClass().getResourceAsStream("response.json") @@ -272,10 +337,12 @@ public void testSlackResponseObjectNullNonBotUser() throws Exception { when(Jenkins.get()).thenReturn(jenkins); + PowerMockito.when(CredentialsObtainer.getTokenToUse(eq("tokenCredentialId"), any(Item.class), anyString())).thenReturn("token"); + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); doNothing().when(printStreamMock).println(); - when(stepExecution.getSlackService(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean())).thenReturn(slackServiceMock); + when(stepExecution.getSlackService(anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyString())).thenReturn(slackServiceMock); when(slackServiceMock.getResponseString()).thenReturn(null); when(slackServiceMock.publish(anyString(), anyString())).thenReturn(true);