diff --git a/.gitignore b/.gitignore index adfb8243..9459736a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ target/ *.iws *.ipr .idea -out \ No newline at end of file +out +.DS_Store \ No newline at end of file diff --git a/README.adoc b/README.adoc index 7faa3dc2..9a7a38a1 100644 --- a/README.adoc +++ b/README.adoc @@ -23,8 +23,7 @@ For example, if this field is set to `@acme.org`, then user foo will by default There are some advanced options as well: -* **Use SMTP Authentication**: check this option to use SMTP authentication when sending out e-mails. -If your environment requires the use of SMTP authentication, specify the user name and the password in the fields shown when this option is checked. +* **Use SMTP Authentication**: this lets you specify a Jenkins Username/Password https://www.jenkins.io/doc/book/using/using-credentials/[credential] to use for SMTP authentication when sending e-mails. * **Use SSL**: Whether or not to use SSL for connecting to the SMTP server. Defaults to port `465`. Other advanced configurations can be done by setting system properties. See this document for possible values and effects. diff --git a/pom.xml b/pom.xml index 966855a4..b3f79b0d 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,10 @@ org.jenkins-ci.plugins display-url-api + + org.jenkins-ci.plugins + credentials + org.jenkins-ci.plugins junit diff --git a/src/main/java/hudson/tasks/Mailer.java b/src/main/java/hudson/tasks/Mailer.java index 0644dabf..896c5669 100644 --- a/src/main/java/hudson/tasks/Mailer.java +++ b/src/main/java/hudson/tasks/Mailer.java @@ -24,6 +24,9 @@ */ package hudson.tasks; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -38,6 +41,7 @@ import hudson.RestrictedSince; import hudson.Util; import hudson.model.*; +import hudson.security.ACL; import jenkins.plugins.mailer.tasks.i18n.Messages; import hudson.security.Permission; import hudson.util.FormValidation; @@ -56,7 +60,9 @@ import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Collections; import java.util.Date; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -214,7 +220,7 @@ public static InternetAddress StringToAddress(String strAddress, String charset) * @throws UnsupportedEncodingException Unsupported encoding * @since TODO */ - public static @NonNull InternetAddress stringToAddress(@NonNull String strAddress, + public static @NonNull InternetAddress stringToAddress(@NonNull String strAddress, @NonNull String charset) throws AddressException, UnsupportedEncodingException { Matcher m = ADDRESS_PATTERN.matcher(strAddress); if(!m.matches()) { @@ -262,7 +268,6 @@ public static final class DescriptorImpl extends BuildStepDescriptor @Deprecated private transient String smtpAuthUsername; - /** @deprecated as of 1.23, use {@link #authentication} */ @Deprecated private transient Secret smtpAuthPassword; @@ -364,16 +369,17 @@ public void setReplyToAddress(String address) { * @return mail session based on the underlying session parameters. */ public Session createSession() { - return createSession(smtpHost,smtpPort,useSsl,useTls,getSmtpAuthUserName(),getSmtpAuthPasswordSecret()); + String credentialsId = Optional.ofNullable(authentication).map(auth -> auth.getCredentialsId()).orElse(null); + return createSession(smtpHost,smtpPort,useSsl,useTls,credentialsId); } - private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, boolean useTls, String smtpAuthUserName, Secret smtpAuthPassword) { + + private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, boolean useTls, String credentialsId) { final String SMTP_PORT_PROPERTY = "mail.smtp.port"; final String SMTP_SOCKETFACTORY_PORT_PROPERTY = "mail.smtp.socketFactory.port"; final String SMTP_SSL_ENABLE_PROPERTY = "mail.smtp.ssl.enable"; smtpHost = Util.fixEmptyAndTrim(smtpHost); smtpPort = Util.fixEmptyAndTrim(smtpPort); - smtpAuthUserName = Util.fixEmptyAndTrim(smtpAuthUserName); Properties props = new Properties(System.getProperties()); if(smtpHost!=null) { @@ -419,26 +425,22 @@ private static Session createSession(String smtpHost, String smtpPort, boolean u props.put("mail.smtp.starttls.enable", "true"); props.put("mail.smtp.starttls.required", "true"); } - if(smtpAuthUserName!=null) - props.put("mail.smtp.auth","true"); + + Optional authenticator = Optional.ofNullable(credentialsId) + .map(id -> CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, (Item) null, ACL.SYSTEM, Collections.emptyList()), + CredentialsMatchers.withId(id))) + .map(creds -> new StandardUsernamePasswordCredentialsAuthenticator(creds)); + if (authenticator.isPresent()) { + LOGGER.fine(String.format("Sending mail with SMTP authentication (credential ID: %s)", credentialsId)); + props.put("mail.smtp.auth", "true"); + } // avoid hang by setting some timeout. props.put("mail.smtp.timeout","60000"); props.put("mail.smtp.connectiontimeout","60000"); - return Session.getInstance(props,getAuthenticator(smtpAuthUserName,Secret.toString(smtpAuthPassword))); - } - - private static Authenticator getAuthenticator(final String smtpAuthUserName, final String smtpAuthPassword) { - if(smtpAuthUserName == null) { - return null; - } - return new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(smtpAuthUserName,smtpAuthPassword); - } - }; + return Session.getInstance(props, authenticator.orElse(null)); } @Override @@ -449,7 +451,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti // case of failure to databind, it gets reverted to previous value. // Would not be necessary by https://github.com/jenkinsci/jenkins/pull/3669 SMTPAuthentication current = this.authentication; - + try (BulkChange b = new BulkChange(this)) { this.authentication = null; req.bindJSON(this, json); @@ -471,7 +473,7 @@ public String getSmtpHost() { return smtpHost; } - + /** @deprecated as of 1.23, use {@link #getSmtpHost()} */ @Deprecated public String getSmtpServer() { @@ -513,31 +515,6 @@ public String getUrl() { return getJenkinsLocationConfiguration().getUrl(); } - /** - * @deprecated as of 1.21 - * Use {@link #authentication} - */ - @Deprecated - public String getSmtpAuthUserName() { - if (authentication == null) return null; - return authentication.getUsername(); - } - - /** - * @deprecated as of 1.21 - * Use {@link #authentication} - */ - @Deprecated - public String getSmtpAuthPassword() { - if (authentication == null) return null; - return Secret.toString(authentication.getPassword()); - } - - public Secret getSmtpAuthPasswordSecret() { - if (authentication == null) return null; - return authentication.getPassword(); - } - public boolean getUseSsl() { return useSsl; } @@ -626,19 +603,6 @@ public SMTPAuthentication getAuthentication() { return authentication; } - /** - * @deprecated as of 1.21 - * Use {@link #authentication} - */ - @Deprecated - public void setSmtpAuth(String userName, String password) { - if (userName == null && password == null) { - this.authentication = null; - } else { - this.authentication = new SMTPAuthentication(userName, Secret.fromString(password)); - } - } - @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { Mailer m = (Mailer)super.newInstance(req, formData); @@ -693,8 +657,7 @@ public FormValidation doCheckDefaultSuffix(@QueryParameter String value) { * @param smtpHost name of the SMTP server to use for mail sending * @param adminAddress Jenkins administrator mail address * @param authentication if set to {@code true} SMTP is used without authentication (username and password) - * @param username plaintext username for SMTP authentication - * @param password secret password for SMTP authentication + * @param credentialsId Jenkins Username/Password credential for SMTP authentication * @param useSsl if set to {@code true} SSL is used * @param useTls if set to {@code true} TLS is used * @param smtpPort port to use for SMTP transfer @@ -705,17 +668,16 @@ public FormValidation doCheckDefaultSuffix(@QueryParameter String value) { @RequirePOST public FormValidation doSendTestMail( @QueryParameter String smtpHost, @QueryParameter String adminAddress, @QueryParameter boolean authentication, - @QueryParameter String username, @QueryParameter Secret password, + @QueryParameter String credentialsId, @QueryParameter boolean useSsl, @QueryParameter boolean useTls, @QueryParameter String smtpPort, @QueryParameter String charset, @QueryParameter String sendTestMailTo) throws IOException { try { Jenkins.get().checkPermission(DescriptorImpl.getJenkinsManageOrAdmin()); if (!authentication) { - username = null; - password = null; + credentialsId = null; } - MimeMessage msg = new MimeMessage(createSession(smtpHost, smtpPort, useSsl, useTls, username, password)); + MimeMessage msg = new MimeMessage(createSession(smtpHost, smtpPort, useSsl, useTls, credentialsId)); msg.setSubject(Messages.Mailer_TestMail_Subject(testEmailCount.incrementAndGet()), charset); msg.setText(Messages.Mailer_TestMail_Content(testEmailCount.get(), Jenkins.get().getDisplayName()), charset); msg.setFrom(stringToAddress(adminAddress, charset)); diff --git a/src/main/java/hudson/tasks/Retryable.java b/src/main/java/hudson/tasks/Retryable.java new file mode 100644 index 00000000..2d92bfae --- /dev/null +++ b/src/main/java/hudson/tasks/Retryable.java @@ -0,0 +1,39 @@ +package hudson.tasks; + +class Retryable { + + private Retryable() { + + } + + interface Fn { + void run(int attempt) throws Exception; + } + + /** + * Run the provided function once, and if it fails, retry up to n times. + * + * This does not support asynchronous functions. + * + * @param times the number of times to retry after a failure. When times=0, the function runs once, with no retries if it fails. When times=2, the function runs once, with up to 2 retries if it fails. + * @param fn the function to run and retry + * @return whether any of the function invocations succeeded + */ + static boolean retry(int times, Fn fn) { + int attempt = 0; + + do { + try { + fn.run(attempt + 1); + + return true; + } catch (Exception e) { + // try again + } + + ++attempt; + } while (attempt <= times); + + return false; + } +} diff --git a/src/main/java/hudson/tasks/SMTPAuthentication.java b/src/main/java/hudson/tasks/SMTPAuthentication.java index da921c42..d22a16a6 100644 --- a/src/main/java/hudson/tasks/SMTPAuthentication.java +++ b/src/main/java/hudson/tasks/SMTPAuthentication.java @@ -1,10 +1,31 @@ package hudson.tasks; +import com.cloudbees.plugins.credentials.*; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.security.ACL; +import hudson.util.ListBoxModel; import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.StreamSupport; import hudson.Util; /** @@ -12,24 +33,70 @@ */ public class SMTPAuthentication extends AbstractDescribableImpl { - private String username; + private static final Logger LOGGER = Logger.getLogger(SMTPAuthentication.class.getName()); + + /** use StandardUsernamePasswordCredentials instead */ + @Deprecated + private transient String username; + + /** use StandardUsernamePasswordCredentials instead */ + @Deprecated + private transient Secret password; - private Secret password; + /** The ID of the Jenkins Username/Password credential to use. */ + private String credentialsId; @DataBoundConstructor - public SMTPAuthentication(String username, Secret password) { + public SMTPAuthentication(String credentialsId) { + this.credentialsId = credentialsId; + } + + @Deprecated + @Restricted(NoExternalUse.class) + SMTPAuthentication(String username, Secret password) { this.username = Util.fixEmptyAndTrim(username); this.password = password; + readResolve(); } + @Deprecated public String getUsername() { return username; } + @Deprecated public Secret getPassword() { return password; } + public String getCredentialsId() { + return credentialsId; + } + + private Object readResolve() { + if (StringUtils.isBlank(credentialsId) && (username != null || password != null)) { + LOGGER.log(Level.CONFIG, "Migrating the Mailer SMTP authentication details to credential..."); + + final StandardUsernamePasswordCredentials migratedCredential = new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, + null, + "Mailer SMTP authentication credentials (migrated)", + username, + password.getPlainText()); + + SystemCredentialsProvider.getInstance().getCredentials().add(migratedCredential); + + credentialsId = migratedCredential.getId(); + } + + username = null; + password = null; + + return this; + } + + + @Extension public static class DescriptorImpl extends Descriptor { @@ -37,5 +104,24 @@ public static class DescriptorImpl extends Descriptor { public String getDisplayName() { return "Use SMTP Authentication"; } + + public ListBoxModel doFillCredentialsIdItems( + @AncestorInPath Item item, + @QueryParameter String credentialsId) { + StandardListBoxModel result = new StandardListBoxModel(); + if (item == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return result.includeCurrentValue(credentialsId); + } + } else { + if (!item.hasPermission(Item.EXTENDED_READ) + && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return result.includeCurrentValue(credentialsId); + } + } + return result + .includeMatchingAs(ACL.SYSTEM, (Item) null, StandardUsernamePasswordCredentials.class, Collections.emptyList(), CredentialsMatchers.always()) + .includeCurrentValue(credentialsId); + } } } diff --git a/src/main/java/hudson/tasks/StandardUsernamePasswordCredentialsAuthenticator.java b/src/main/java/hudson/tasks/StandardUsernamePasswordCredentialsAuthenticator.java new file mode 100644 index 00000000..fca79acd --- /dev/null +++ b/src/main/java/hudson/tasks/StandardUsernamePasswordCredentialsAuthenticator.java @@ -0,0 +1,24 @@ +package hudson.tasks; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.util.Secret; + +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; + +/** + * Allow JavaMail to authenticate using a Jenkins StandardUsernamePasswordCredentials. + */ +class StandardUsernamePasswordCredentialsAuthenticator extends Authenticator { + + private final StandardUsernamePasswordCredentials credentials; + + StandardUsernamePasswordCredentialsAuthenticator(StandardUsernamePasswordCredentials credentials) { + this.credentials = credentials; + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(credentials.getUsername(), Secret.toString(credentials.getPassword())); + } +} diff --git a/src/main/resources/hudson/tasks/Mailer/global.jelly b/src/main/resources/hudson/tasks/Mailer/global.jelly index e7b919ce..4b9b0df1 100644 --- a/src/main/resources/hudson/tasks/Mailer/global.jelly +++ b/src/main/resources/hudson/tasks/Mailer/global.jelly @@ -55,7 +55,7 @@ THE SOFTWARE. - + diff --git a/src/main/resources/hudson/tasks/SMTPAuthentication/config.jelly b/src/main/resources/hudson/tasks/SMTPAuthentication/config.jelly index d74fee96..81c3621d 100644 --- a/src/main/resources/hudson/tasks/SMTPAuthentication/config.jelly +++ b/src/main/resources/hudson/tasks/SMTPAuthentication/config.jelly @@ -23,11 +23,8 @@ THE SOFTWARE. --> - - - - - - + + + diff --git a/src/test/java/hudson/tasks/MailerTest.java b/src/test/java/hudson/tasks/MailerTest.java index 0f7f739c..be8a9362 100644 --- a/src/test/java/hudson/tasks/MailerTest.java +++ b/src/test/java/hudson/tasks/MailerTest.java @@ -23,6 +23,12 @@ */ package hudson.tasks; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.Functions; @@ -40,6 +46,7 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.Assume; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Bug; @@ -95,10 +102,19 @@ public class MailerTest { private static final String AUTHOR2 = "author2@example.com"; /** Change counter. */ private static final AtomicLong COUNTER = new AtomicLong(0L); + public static final String CREDENTIALS_ID = "foo"; @Rule public JenkinsRule rule = new JenkinsRule(); + @Before + public void setupSmtpAuthenticationCredential() throws IOException { + CredentialsStore store = CredentialsProvider.lookupStores(rule).iterator().next(); + StandardUsernamePasswordCredentials credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, CREDENTIALS_ID, "Description", "user", "pass"); + store.addCredentials(Domain.global(), credentials); + store.save(); + } + private static Mailbox getMailbox(String recipient) throws Exception { return Mailbox.get(new InternetAddress(recipient)); } @@ -237,7 +253,7 @@ public void testGlobalConfigRoundtrip() throws Exception { d.setSmtpHost("smtp.host"); d.setSmtpPort("1025"); d.setUseSsl(true); - d.setAuthentication(new SMTPAuthentication("user", Secret.fromString("pass"))); + d.setAuthentication(new SMTPAuthentication(CREDENTIALS_ID)); rule.submit(rule.createWebClient().goTo("configure").getFormByName("config")); @@ -247,8 +263,7 @@ public void testGlobalConfigRoundtrip() throws Exception { assertEquals("1025",d.getSmtpPort()); assertEquals(true,d.getUseSsl()); SMTPAuthentication authentication = d.getAuthentication(); - assertEquals("user",authentication.getUsername()); - assertEquals("pass",authentication.getPassword().getPlainText()); + assertEquals(CREDENTIALS_ID,authentication.getCredentialsId()); d.setUseSsl(false); d.setAuthentication(null); @@ -266,8 +281,7 @@ public void globalConfig() throws Exception { form.getInputByName("_.smtpHost").setValueAttribute("acme.com"); form.getInputByName("_.defaultSuffix").setValueAttribute("@acme.com"); form.getInputByName("_.authentication").setChecked(true); - form.getInputByName("_.username").setValueAttribute("user"); - form.getInputByName("_.password").setValueAttribute("pass"); + form.getSelectByName("_.credentialsId").setSelectedAttribute(CREDENTIALS_ID, true); rule.submit(form); @@ -276,8 +290,7 @@ public void globalConfig() throws Exception { assertEquals("@acme.com", d.getDefaultSuffix()); SMTPAuthentication auth = d.getAuthentication(); assertNotNull(auth); - assertEquals("user", auth.getUsername()); - assertEquals("pass", auth.getPassword().getPlainText()); + assertEquals(CREDENTIALS_ID, auth.getCredentialsId()); cp = webClient.goTo("configure"); form = cp.getFormByName("config"); @@ -429,7 +442,7 @@ public void testPipelineCompatibility() throws Exception { public void testMigrateOldData() { Mailer.DescriptorImpl descriptor = Mailer.descriptor(); assertNotNull("Mailer can not be found", descriptor); - assertEquals(String.format("Authentication did not migrate properly. Username expected %s but received %s", "olduser", descriptor.getAuthentication().getUsername()), "olduser", descriptor.getAuthentication().getUsername()); + assertEquals(String.format("Authentication did not migrate properly. Credentials ID expected %s but received %s", CREDENTIALS_ID, descriptor.getAuthentication().getCredentialsId()), CREDENTIALS_ID, descriptor.getAuthentication().getCredentialsId()); assertEquals(String.format("Charset did not migrate properly. Expected %s but received %s", "UTF-8", descriptor.getCharset()), "UTF-8", descriptor.getCharset()); assertEquals(String.format("Default suffix did not migrate properly. Expected %s but received %s", "@mydomain.com", descriptor.getDefaultSuffix()), "@mydomain.com", descriptor.getDefaultSuffix()); assertEquals(String.format("ReplayTo address did not migrate properly. Expected %s but received %s", "noreplay@mydomain.com", descriptor.getReplyToAddress()), "noreplay@mydomain.com", descriptor.getReplyToAddress()); diff --git a/src/test/java/hudson/tasks/RetryableTest.java b/src/test/java/hudson/tasks/RetryableTest.java new file mode 100644 index 00000000..7f4421aa --- /dev/null +++ b/src/test/java/hudson/tasks/RetryableTest.java @@ -0,0 +1,55 @@ +package hudson.tasks; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; + +public class RetryableTest { + + /** + * Retry 0x = do once + */ + @Test + public void testRetrySuccess() { + final List attempts = new ArrayList<>(); + final boolean isSuccess = Retryable.retry(0, (attempt) -> { + attempts.add(attempt); + // no-op + }); + + assertTrue(isSuccess); + assertThat(attempts, contains(1)); + } + + @Test + public void testRetryFailure() { + final List attempts = new ArrayList<>(); + final boolean isSuccess = Retryable.retry(0, (attempt) -> { + attempts.add(attempt); + throw new Exception("Fail"); + }); + + assertFalse(isSuccess); + assertThat(attempts, contains(1)); + } + + /** + * Retry 2x = do three times + */ + @Test + public void testMultipleRetries() { + final List attempts = new ArrayList<>(); + + final boolean isSuccess = Retryable.retry(2, (attempt) -> { + attempts.add(attempt); + throw new Exception("Fail"); + }); + + assertFalse(isSuccess); + assertThat(attempts, contains(1, 2, 3)); + } +} diff --git a/src/test/java/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest.java b/src/test/java/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest.java new file mode 100644 index 00000000..0af4138b --- /dev/null +++ b/src/test/java/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest.java @@ -0,0 +1,27 @@ +package hudson.tasks.migrations; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.tasks.Mailer; +import hudson.tasks.SMTPAuthentication; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AddCredentialsToSmtpAuthenticationTest extends MigrationTest { + @Override + protected void change(Mailer.DescriptorImpl descriptor) { + final SMTPAuthentication authentication = descriptor.getAuthentication(); + + assertNotNull(authentication); + + final String credentialsId = authentication.getCredentialsId(); + + assertNotNull(credentialsId); + + final StandardUsernamePasswordCredentials migratedCredential = Util.lookupCredential(credentialsId) + .orElseThrow(() -> new RuntimeException("Can't find the migrated test credential")); + + assertEquals("olduser", migratedCredential.getUsername()); + assertEquals("{AQAAABAAAAAQ1UuHpGkqtUa56seSp+wJjfuiggZPi/D+t38985a5tXU=}", migratedCredential.getPassword().getPlainText()); + } +} diff --git a/src/test/java/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest.java b/src/test/java/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest.java new file mode 100644 index 00000000..fd04fb4f --- /dev/null +++ b/src/test/java/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest.java @@ -0,0 +1,28 @@ +package hudson.tasks.migrations; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.tasks.Mailer; +import hudson.tasks.SMTPAuthentication; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AddSmtpAuthenticationToMailerTest extends MigrationTest { + + @Override + public void change(Mailer.DescriptorImpl descriptor) { + final SMTPAuthentication authentication = descriptor.getAuthentication(); + + assertNotNull(authentication); + + final String credentialsId = authentication.getCredentialsId(); + + assertNotNull(credentialsId); + + final StandardUsernamePasswordCredentials migratedCredential = Util.lookupCredential(credentialsId) + .orElseThrow(() -> new RuntimeException("Can't find the migrated test credential")); + + assertEquals("olduser", migratedCredential.getUsername()); + assertEquals("{AQAAABAAAAAQ1UuHpGkqtUa56seSp+wJjfuiggZPi/D+t38985a5tXU=}", migratedCredential.getPassword().getPlainText()); + } +} diff --git a/src/test/java/hudson/tasks/migrations/MigrationTest.java b/src/test/java/hudson/tasks/migrations/MigrationTest.java new file mode 100644 index 00000000..88e2f802 --- /dev/null +++ b/src/test/java/hudson/tasks/migrations/MigrationTest.java @@ -0,0 +1,72 @@ +package hudson.tasks.migrations; + +import hudson.tasks.Mailer; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.HudsonHomeLoader; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.net.URL; + +/** + * Defines migration tests for the Jenkins plugin configuration data model. + *

The format is loosely based on Ruby On Rails' ActiveRecord Migrations. Like ActiveRecord Migrations, it uses naming conventions, and a single change() method per test case. Unlike ActiveRecord Migrations, there is no concept of migrating backwards as we do not support plugin downgrades, so we only need to test the forward path.

+ */ +public abstract class MigrationTest { + + @Rule + public final JenkinsRule jenkins = new JenkinsRule() + .with(new LocalClass(this.getClass())); + + /** + * Implement this to assert that the property in question has been migrated correctly. + * + * @param descriptor The descriptor in which the property will be found, or not found if it was removed. + */ + protected abstract void change(Mailer.DescriptorImpl descriptor); + + @Test + public void shouldMigrate() { + final Mailer.DescriptorImpl config = getPluginConfiguration(); + change(config); + } + + private Mailer.DescriptorImpl getPluginConfiguration() { + return (Mailer.DescriptorImpl) jenkins.getInstance().getDescriptor(Mailer.class); + } + + /** + * Adaptation of Local for when we just want to load one Hudson home per class, rather than one per test. + */ + private static class LocalClass implements HudsonHomeLoader { + + private final Class testClass; + + public LocalClass(Class testClass) { + this.testClass = testClass; + } + + @Override + public File allocate() throws Exception { + URL res = findDataResource(); + if(!res.getProtocol().equals("file")) + throw new AssertionError("Test data is not available in the file system: "+res); + File home = new File(res.toURI()); + System.err.println("Loading $JENKINS_HOME from " + home); + + return new CopyExisting(home).allocate(); + } + + private URL findDataResource() { + for( String suffix : SUFFIXES ) { + URL res = testClass.getResource(testClass.getSimpleName() + suffix); + if(res!=null) return res; + } + + throw new AssertionError("No test resource was found for "+testClass); + } + + private static final String[] SUFFIXES = {"/", ".zip"}; + } +} diff --git a/src/test/java/hudson/tasks/migrations/Util.java b/src/test/java/hudson/tasks/migrations/Util.java new file mode 100644 index 00000000..95ba5a7c --- /dev/null +++ b/src/test/java/hudson/tasks/migrations/Util.java @@ -0,0 +1,19 @@ +package hudson.tasks.migrations; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import hudson.model.Item; + +import java.util.List; +import java.util.Optional; + +public class Util { + static Optional lookupCredential(String id) { + final List credentials = CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, (Item) null, null, (List) null); + + return credentials.stream() + .filter(cred -> cred.getId().equals(id)) + .findFirst(); + } +} diff --git a/src/test/java/jenkins/plugins/mailer/MailerJCasCCompatibilityTest.java b/src/test/java/jenkins/plugins/mailer/MailerJCasCCompatibilityTest.java index 68ff7e54..43d16299 100644 --- a/src/test/java/jenkins/plugins/mailer/MailerJCasCCompatibilityTest.java +++ b/src/test/java/jenkins/plugins/mailer/MailerJCasCCompatibilityTest.java @@ -1,8 +1,11 @@ package jenkins.plugins.mailer; +import org.jvnet.hudson.test.RestartableJenkinsRule; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; import org.jvnet.hudson.test.RestartableJenkinsRule; @@ -15,7 +18,7 @@ public class MailerJCasCCompatibilityTest extends RoundTripAbstractTest { protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenkinsRule, String s) { Mailer.DescriptorImpl descriptor = Mailer.descriptor(); assertNotNull("Mailer can not be found", descriptor); - assertEquals(String.format("Wrong authentication. Username expected %s but received %s", "fakeuser", descriptor.getAuthentication().getUsername()), "fakeuser", descriptor.getAuthentication().getUsername()); + assertEquals(String.format("Wrong authentication. Credential ID expected %s but received %s", "foo", descriptor.getAuthentication().getCredentialsId()), "foo", descriptor.getAuthentication().getCredentialsId()); assertEquals(String.format("Wrong charset. Expected %s but received %s", "UTF-8", descriptor.getCharset()), "UTF-8", descriptor.getCharset()); assertEquals(String.format("Wrong default suffix. Expected %s but received %s", "@mydomain.com", descriptor.getDefaultSuffix()), "@mydomain.com", descriptor.getDefaultSuffix()); assertEquals(String.format("Wrong ReplayTo address. Expected %s but received %s", "noreplay@mydomain.com", descriptor.getReplyToAddress()), "noreplay@mydomain.com", descriptor.getReplyToAddress()); @@ -27,6 +30,6 @@ protected void assertConfiguredAsExpected(RestartableJenkinsRule restartableJenk @Override protected String stringInLogExpected() { - return "Setting class hudson.tasks.SMTPAuthentication.username = fakeuser"; + return "Setting class hudson.tasks.SMTPAuthentication.credentialsId = foo"; } } diff --git a/src/test/resources/hudson/tasks/MailerTest/testMigrateOldData/hudson.tasks.Mailer.xml b/src/test/resources/hudson/tasks/MailerTest/testMigrateOldData/hudson.tasks.Mailer.xml index 580d1b6e..4621d6c1 100644 --- a/src/test/resources/hudson/tasks/MailerTest/testMigrateOldData/hudson.tasks.Mailer.xml +++ b/src/test/resources/hudson/tasks/MailerTest/testMigrateOldData/hudson.tasks.Mailer.xml @@ -1,8 +1,9 @@ @mydomain.com - olduser - {AQAAABAAAAAQ1UuHpGkqtUa56seSp+wJjfuiggZPi/D+t38985a5tXU=} + + foo + noreplay@mydomain.com old.data.smtp.host true diff --git a/src/test/resources/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest/hudson.tasks.Mailer.xml b/src/test/resources/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest/hudson.tasks.Mailer.xml new file mode 100644 index 00000000..fe9ca388 --- /dev/null +++ b/src/test/resources/hudson/tasks/migrations/AddCredentialsToSmtpAuthenticationTest/hudson.tasks.Mailer.xml @@ -0,0 +1,13 @@ + + + @mydomain.com + + olduser + {AQAAABAAAAAQ1UuHpGkqtUa56seSp+wJjfuiggZPi/D+t38985a5tXU=} + + noreplay@mydomain.com + old.data.smtp.host + true + 808080 + UTF-8 + \ No newline at end of file diff --git a/src/test/resources/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest/hudson.tasks.Mailer.xml b/src/test/resources/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest/hudson.tasks.Mailer.xml new file mode 100644 index 00000000..580d1b6e --- /dev/null +++ b/src/test/resources/hudson/tasks/migrations/AddSmtpAuthenticationToMailerTest/hudson.tasks.Mailer.xml @@ -0,0 +1,11 @@ + + + @mydomain.com + olduser + {AQAAABAAAAAQ1UuHpGkqtUa56seSp+wJjfuiggZPi/D+t38985a5tXU=} + noreplay@mydomain.com + old.data.smtp.host + true + 808080 + UTF-8 + \ No newline at end of file diff --git a/src/test/resources/jenkins/plugins/mailer/configuration-as-code.yaml b/src/test/resources/jenkins/plugins/mailer/configuration-as-code.yaml index c10400f5..7ff8c2b6 100644 --- a/src/test/resources/jenkins/plugins/mailer/configuration-as-code.yaml +++ b/src/test/resources/jenkins/plugins/mailer/configuration-as-code.yaml @@ -1,8 +1,7 @@ unclassified: mailer: authentication: - password: "{AQAAABAAAAAQerzX1sPSYnExkhL/IPqqen3IXjJDVYLe/PR1+30xdA8=}" - username: "fakeuser" + credentialsId: "foo" charset: "UTF-8" defaultSuffix: "@mydomain.com" replyToAddress: "noreplay@mydomain.com" @@ -10,3 +9,14 @@ unclassified: smtpPort: "808080" useSsl: true useTls: true + +credentials: + system: + domainCredentials: + - credentials: + - usernamePassword: + scope: SYSTEM + id: "foo" + username: "fakeuser" + password: "{AQAAABAAAAAQerzX1sPSYnExkhL/IPqqen3IXjJDVYLe/PR1+30xdA8=}" + description: "Mail SMTP auth" \ No newline at end of file