diff --git a/cli/pom.xml b/cli/pom.xml index b6771c7daf..6042680574 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.97-SNAPSHOT cli diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java index 8697026508..d5636f3bd7 100644 --- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java +++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java @@ -47,7 +47,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest(CLI.class) // When mocking new operator caller has to be @PreparedForTest, not class itself +@PrepareForTest({CLI.class, CLIConnectionFactory.class}) // When mocking new operator caller has to be @PreparedForTest, not class itself public class PrivateKeyProviderTest { @Test diff --git a/core/pom.xml b/core/pom.xml index a4b053eee5..4517bab996 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.97-SNAPSHOT jenkins-core @@ -39,7 +39,7 @@ THE SOFTWARE. true - 1.253 + 1.254 2.5.6.SEC03 2.4.11 @@ -796,25 +796,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - - - - - testCompile - - - - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - org.codehaus.mojo findbugs-maven-plugin @@ -889,41 +870,5 @@ THE SOFTWARE. true - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - ${project.basedir}/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - maven-surefire-plugin - - - true - - - - - diff --git a/core/src/build-script/Cobertura.groovy b/core/src/build-script/Cobertura.groovy deleted file mode 100644 index 49b773b49b..0000000000 --- a/core/src/build-script/Cobertura.groovy +++ /dev/null @@ -1,82 +0,0 @@ -import org.apache.maven.project.MavenProject; - -/** - * Cobertura invoker. - */ -public class Cobertura { - private final MavenProject project; - // maven helper - private def maven; - // ant builder - private def ant; - /** - * Cobertura data file. - */ - private final File ser; - - def Cobertura(project, maven, ant, ser) { - this.project = maven.project; - this.maven = maven; - this.ant = ant; - this.ser =ser; - - // define cobertura tasks - ant.taskdef(resource:"tasks.properties") - } - - // function that ensures that the given directory exists - private String dir(String dir) { - new File(project.basedir,dir).mkdirs(); - return dir; - } - - /** - * Instruments the given class dirs/jars by cobertura - * - * @param files - * List of jar files and class dirs to instrument. - */ - def instrument(files) { - ant."cobertura-instrument"(todir:dir("target/cobertura-classes"),datafile:ser) { - fileset(dir:"target/classes"); - files.each{ fileset(file:it) } - } - } - - def runTests() { - ant.junit(fork:true, forkMode:"once", failureproperty:"failed", printsummary:true) { - classpath { - junitClasspath() - } - batchtest(todir:dir("target/surefire-reports")) { - fileset(dir:"target/test-classes") { - include(name:"**/*Test.class") - } - formatter(type:"xml") - } - sysproperty(key:"net.sourceforge.cobertura.datafile",value:ser) - sysproperty(key:"hudson.ClassicPluginStrategy.useAntClassLoader",value:"true") - } - } - - def junitClasspath() { - ant.pathelement(path: "target/cobertura-classes") // put the instrumented classes first - ant.fileset(dir:"target/cobertura-classes",includes:"*.jar") // instrumented jar files - ant.pathelement(path: maven.resolveArtifact("net.sourceforge.cobertura:cobertura:1.9")) // cobertura runtime - project.getTestClasspathElements().each { ant.pathelement(path: it) } // the rest of the dependencies - } - - def report(dirs) { - maven.attachArtifact(ser,"ser","cobertura") - ["html","xml"].each { format -> - ant."cobertura-report"(format:format,datafile:ser,destdir:dir("target/cobertura-reports"),srcdir:"src/main/java") { - dirs.each{ fileset(dir:it) } - } - } - } - - def makeBuildFailIfTestFail() { - if(ant.project.getProperty("failed")!=null && !Boolean.getBoolean("testFailureIgnore")) - throw new Exception("Some unit tests failed"); - } -} diff --git a/core/src/build-script/readme.txt b/core/src/build-script/readme.txt deleted file mode 100644 index 6c53a01026..0000000000 --- a/core/src/build-script/readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -Parts of the build scripts that are written in Groovy. - -IntelliJ needs Groovy files to be in its source directory to provide completion and such, -so we need this to be in its own directory. \ No newline at end of file diff --git a/core/src/build-script/unitTest.groovy b/core/src/build-script/unitTest.groovy deleted file mode 100644 index 653391aad7..0000000000 --- a/core/src/build-script/unitTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -// run unit tests - -ant.project.setBaseDir(project.basedir) -ser=new File(project.basedir,"target/cobertura.ser"); // store cobertura data file in a module-specific location - -cob = new Cobertura(project,maven,ant,ser); - -cob.instrument([]) -cob.runTests() -cob.report([]) -cob.makeBuildFailIfTestFail(); diff --git a/core/src/main/groovy/hudson/util/LoadMonitor.groovy b/core/src/main/groovy/hudson/util/LoadMonitor.groovy deleted file mode 100644 index 97d7018161..0000000000 --- a/core/src/main/groovy/hudson/util/LoadMonitor.groovy +++ /dev/null @@ -1,116 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.util - -import hudson.model.Computer -import jenkins.util.Timer -import jenkins.model.Jenkins -import hudson.model.Label -import hudson.model.Queue.BlockedItem -import hudson.model.Queue.BuildableItem -import hudson.model.Queue.WaitingItem -import hudson.triggers.SafeTimerTask -import java.text.DateFormat -import java.util.concurrent.TimeUnit - -/** - * Spits out the load information. - * - *

- * I'm using this code to design the auto scaling feature. - * In future this might be useful data to expose to the UI. - * - * @author Kohsuke Kawaguchi - */ -public class LoadMonitorImpl extends SafeTimerTask { - - private final File dataFile; - private List labels; - - public LoadMonitorImpl(File dataFile) { - this.dataFile = dataFile; - labels = Jenkins.getInstance().labels*.name; - printHeaders(); - Timer.get().scheduleAtFixedRate(this,0,10*1000, TimeUnit.MILLISECONDS); - } - - private String quote(Object s) { "\"${s}\""; } - - protected void printHeaders() { - def headers = ["# of executors","# of busy executors","BuildableItems in Q","BuildableItem avg wait time"]; - def data = ["timestamp"]; - data += headers; - data += ["WaitingItems in Q","BlockedItems in Q"]; - - for( String label : labels) - data += headers.collect { "${it} (${label}}" } - - dataFile.append(data.collect({ quote(it) }).join(",")+"\n"); - } - - @Override - protected void doRun() { - def now = new Date(); - def data = []; - data.add(quote(FORMATTER.format(now))); - - def h = Jenkins.getInstance(); - - def items = h.queue.items; - def filterByType = {Class type -> items.findAll { type.isInstance(it) } } - - def builder = {List cs, Closure itemFilter -> - // number of total executor, number of busy executor - data.add(cs.sum { it.isOffline() ? 0 : it.numExecutors }); - data.add(cs.sum {Computer c -> - c.executors.findAll { !it.isIdle() }.size() - }); - - // queue statistics - def is = filterByType(BuildableItem).findAll(itemFilter); - data.add(is.size()); - data.add(is.sum {BuildableItem bi -> now.time - bi.buildableStartMilliseconds }?:0 / Math.max(1,is.size()) ); - }; - - - // for the whole thing - builder(Arrays.asList(h.computers),{ it->true }); - - data.add(filterByType(WaitingItem).size()); - data.add(filterByType(BlockedItem).size()); - - // per label stats - for (String label : labels) { - Label l = h.getLabel(label) - builder(l.nodes.collect { it.toComputer() }) { BuildableItem bi -> bi.task.assignedLabel==l }; - } - - dataFile.append(data.join(",")+"\n"); - } - - private static final DateFormat FORMATTER = DateFormat.getDateTimeInstance(); -} - -new LoadMonitorImpl(new File("/files/hudson/load.txt")); - diff --git a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy b/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy deleted file mode 100644 index 08cb1c23a3..0000000000 --- a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package jenkins.util.groovy - -import lib.FormTagLib -import lib.LayoutTagLib -import org.kohsuke.stapler.jelly.groovy.JellyBuilder -import org.kohsuke.stapler.jelly.groovy.Namespace -import lib.JenkinsTagLib - -/** - * Base class for utility classes for Groovy view scripts - *

- * Usage from script of a subclass, say ViewHelper: - *

- * new ViewHelper(delegate).method(); - *

- * see ModularizeViewScript in ui-samples for an example how to use this class. - * - * @author Stefan Wolf (wolfs) - */ -abstract class AbstractGroovyViewModule { - JellyBuilder builder - FormTagLib f - LayoutTagLib l - JenkinsTagLib t - Namespace st - - public AbstractGroovyViewModule(JellyBuilder b) { - builder = b - f= builder.namespace(FormTagLib) - l=builder.namespace(LayoutTagLib) - t=builder.namespace(JenkinsTagLib) - st=builder.namespace("jelly:stapler") - - } - - def methodMissing(String name, args) { - builder.invokeMethod(name,args) - } - - def propertyMissing(String name) { - builder.getProperty(name) - } - - def propertyMissing(String name, value) { - builder.setProperty(name, value) - } -} diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 197a27f60b..8d1233ab04 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -79,6 +79,7 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; @@ -125,8 +126,10 @@ import org.kohsuke.stapler.Stapler; import static hudson.FilePath.TarCompression.GZIP; import static hudson.Util.deleteFile; +import static hudson.Util.fileToPath; import static hudson.Util.fixEmpty; import static hudson.Util.isSymlink; + import java.util.Collections; /** @@ -1165,7 +1168,7 @@ public final class FilePath implements Serializable { // following Ant task to avoid possible race condition. Thread.sleep(10); - return f.mkdirs() || f.exists(); + return mkdirs(f) || f.exists(); } })) throw new IOException("Failed to mkdirs: "+remote); @@ -1615,7 +1618,7 @@ public final class FilePath implements Serializable { if (Util.NATIVE_CHMOD_MODE) { PosixAPI.jnr().chmod(f.getAbsolutePath(), mask); } else { - Files.setPosixFilePermissions(Util.fileToPath(f), Util.modeToPermissions(mask)); + Files.setPosixFilePermissions(fileToPath(f), Util.modeToPermissions(mask)); } } @@ -1962,7 +1965,7 @@ public final class FilePath implements Serializable { act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - reading(f).renameTo(creating(new File(target.remote))); + Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS); return null; } }); @@ -2024,8 +2027,8 @@ public final class FilePath implements Serializable { File targetFile = new File(target.remote); File targetDir = targetFile.getParentFile(); filterNonNull().mkdirs(targetDir); - Files.createDirectories(Util.fileToPath(targetDir)); - Files.copy(Util.fileToPath(reading(f)), Util.fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.createDirectories(fileToPath(targetDir)); + Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); return null; } }); @@ -2957,11 +2960,12 @@ public final class FilePath implements Serializable { return f; } - private boolean mkdirs(File dir) { + private boolean mkdirs(File dir) throws IOException { if (dir.exists()) return false; filterNonNull().mkdirs(dir); - return dir.mkdirs(); + Files.createDirectories(fileToPath(dir)); + return true; } private File mkdirsE(File dir) throws IOException { diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index c9c5e8545b..bf8aca4e47 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -26,6 +26,7 @@ package hudson; import hudson.model.Slave; +import hudson.security.*; import jenkins.util.SystemProperties; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; @@ -56,11 +57,6 @@ import hudson.model.View; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.search.SearchableModelObject; -import hudson.security.AccessControlled; -import hudson.security.AuthorizationStrategy; -import hudson.security.GlobalSecurityConfiguration; -import hudson.security.Permission; -import hudson.security.SecurityRealm; import hudson.security.captcha.CaptchaSupport; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.Cloud; @@ -137,6 +133,7 @@ import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; +import org.acegisecurity.Authentication; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; @@ -1582,7 +1579,7 @@ public class Functions { * Checks if the current user is anonymous. */ public static boolean isAnonymous() { - return Jenkins.getAuthentication() instanceof AnonymousAuthenticationToken; + return ACL.isAnonymous(Jenkins.getAuthentication()); } /** diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index ab203c20ec..2ff190fb72 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -120,7 +120,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -144,6 +143,7 @@ import hudson.util.FormValidation; import java.io.ByteArrayInputStream; import java.net.JarURLConnection; import java.net.URLConnection; +import java.util.ServiceLoader; import java.util.jar.JarEntry; import static java.util.logging.Level.FINE; @@ -651,7 +651,17 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas continue; } - String artifactId = dependencyToken.split(":")[0]; + String[] artifactIdVersionPair = dependencyToken.split(":"); + String artifactId = artifactIdVersionPair[0]; + VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]); + + PluginManager manager = Jenkins.getActiveInstance().getPluginManager(); + VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId); + if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) { + // Do not downgrade dependencies that are already installed. + continue; + } + URL dependencyURL = context.getResource(fromPath + "/" + artifactId + ".hpi"); if (dependencyURL == null) { @@ -680,9 +690,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * */ protected void loadDetachedPlugins() { - InstallState installState = Jenkins.getActiveInstance().getInstallState(); - if (InstallState.UPGRADE.equals(installState)) { - VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion()); + VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion()); + if (lastExecVersion.isNewerThan(InstallUtil.NEW_INSTALL_VERSION) && lastExecVersion.isOlderThan(Jenkins.getVersion())) { LOGGER.log(INFO, "Upgrading Jenkins. The last running version was {0}. This Jenkins is version {1}.", new Object[] {lastExecVersion, Jenkins.VERSION}); @@ -1199,7 +1208,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas /** * Discover all the service provider implementations of the given class, * via META-INF/services. + * @deprecated Use {@link ServiceLoader} instead, or (more commonly) {@link ExtensionList}. */ + @Deprecated public Collection> discover( Class spi ) { Set> result = new HashSet>(); diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 8ee5c7390a..1e13ea9957 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -28,7 +28,6 @@ import jenkins.util.SystemProperties; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.model.TaskListener; -import hudson.os.PosixAPI; import hudson.util.QuotedStringTokenizer; import hudson.util.VariableResolver; @@ -36,16 +35,12 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.time.FastDateFormat; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; -import org.apache.tools.ant.taskdefs.Chmod; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; -import jnr.posix.FileStat; -import jnr.posix.POSIX; - import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -70,6 +65,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.DosFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -257,16 +253,16 @@ public class Util { * * @param f * What to delete. If a directory, it'll need to be empty. - * @throws IOException if it exists but could not be successfully deleted + * @throws IOException if it exists but could not be successfully deleted, + * or if it represents an invalid {@link Path}. */ private static void tryOnceDeleteFile(File f) throws IOException { - if (!f.delete()) { - if(!f.exists()) - // we are trying to delete a file that no longer exists, so this is not an error - return; - + Path path = fileToPath(f); + try { + Files.deleteIfExists(path); + } catch (IOException e) { // perhaps this file is read-only? - makeWritable(f); + makeWritable(path); /* on Unix both the file and the directory that contains it has to be writable for a file deletion to be successful. (Confirmed on Solaris 9) @@ -280,56 +276,47 @@ public class Util { $ rm x rm: x not removed: Permission denied */ - - makeWritable(f.getParentFile()); - - if(!f.delete() && f.exists()) { - // trouble-shooting. - try { - Files.deleteIfExists(f.toPath()); - } catch (InvalidPathException e) { - throw new IOException(e); - } - + Path parent = path.getParent(); + if (parent != null) { + makeWritable(parent); + } + try { + Files.deleteIfExists(path); + } catch (IOException e2) { // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357 // I suspect other processes putting files in this directory File[] files = f.listFiles(); if(files!=null && files.length>0) - throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files)); - throw new IOException("Unable to delete " + f.getPath()); + throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files), e2); + throw e2; } } } /** - * Makes the given file writable by any means possible. + * Makes the file at the given path writable by any means possible. */ - private static void makeWritable(@Nonnull File f) { - if (f.setWritable(true)) { - return; - } - // TODO do we still need to try anything else? - - // try chmod. this becomes no-op if this is not Unix. - try { - Chmod chmod = new Chmod(); - chmod.setProject(new Project()); - chmod.setFile(f); - chmod.setPerm("u+w"); - chmod.execute(); - } catch (BuildException e) { - LOGGER.log(Level.INFO,"Failed to chmod "+f,e); - } - - try {// try libc chmod - POSIX posix = PosixAPI.jnr(); - String path = f.getAbsolutePath(); - FileStat stat = posix.stat(path); - posix.chmod(path, stat.mode()|0200); // u+w - } catch (Throwable t) { - LOGGER.log(Level.FINE,"Failed to chmod(2) "+f,t); + private static void makeWritable(@Nonnull Path path) throws IOException { + if (!Functions.isWindows()) { + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + Set newPermissions = ((PosixFileAttributes)attrs).permissions(); + newPermissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, newPermissions); + return; + } catch (NoSuchFileException e) { + return; + } catch (UnsupportedOperationException e) { + // PosixFileAttributes not supported, fall back to old IO. + } } + /** + * We intentionally do not check the return code of setWritable, because if it + * is false we prefer to rethrow the exception thrown by Files.deleteIfExists, + * which will have a more useful message than something we make up here. + */ + path.toFile().setWritable(true); } /** diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index d507662037..5efbaf3a33 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -30,6 +30,8 @@ import hudson.ExtensionPoint; import hudson.cli.declarative.CLIMethod; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; import hudson.Functions; +import hudson.security.ACL; +import jenkins.security.SecurityListener; import jenkins.util.SystemProperties; import hudson.cli.declarative.OptionHandlerExtension; import jenkins.model.Jenkins; @@ -44,6 +46,8 @@ import org.acegisecurity.Authentication; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; import org.apache.commons.discovery.ResourceClassIterator; import org.apache.commons.discovery.ResourceNameIterator; import org.apache.commons.discovery.resource.ClassLoaders; @@ -344,8 +348,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { @Deprecated protected Authentication loadStoredAuthentication() throws InterruptedException { try { - if (channel!=null) - return new ClientAuthenticationCache(channel).get(); + if (channel!=null){ + Authentication authLoadedFromCache = new ClientAuthenticationCache(channel).get(); + + if(!ACL.isAnonymous(authLoadedFromCache)){ + UserDetails userDetails = new CLIUserDetails(authLoadedFromCache); + SecurityListener.fireAuthenticated(userDetails); + } + + return authLoadedFromCache; + } } catch (IOException e) { stderr.println("Failed to access the stored credential"); Functions.printStackTrace(e, stderr); // recover @@ -642,4 +654,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } } } + + /** + * User details loaded from the CLI {@link ClientAuthenticationCache} + * The user is never anonymous since it must be authenticated to be stored in the cache + */ + @Deprecated + @Restricted(NoExternalUse.class) + private static class CLIUserDetails extends User { + private CLIUserDetails(Authentication auth) { + super(auth.getName(), "", true, true, true, true, auth.getAuthorities()); + } + } } diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java index cf5b5c113c..48b96f4686 100644 --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java @@ -22,6 +22,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.security.HMACConfidentialKey; +import javax.annotation.Nonnull; + /** * Represents the authentication credential store of the CLI client. * @@ -73,7 +75,7 @@ public class ClientAuthenticationCache implements Serializable { * * @return {@link jenkins.model.Jenkins#ANONYMOUS} if no such credential is found, or if the stored credential is invalid. */ - public Authentication get() { + public @Nonnull Authentication get() { Jenkins h = Jenkins.getActiveInstance(); String val = props.getProperty(getPropertyKey()); if (val == null) { @@ -100,6 +102,7 @@ public class ClientAuthenticationCache implements Serializable { LOGGER.log(Level.FINER, "Loaded stored CLI authentication for {0}", username); return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities()); } catch (AuthenticationException | DataAccessException e) { + //TODO there is no check to be consistent with User.ALLOW_NON_EXISTENT_USER_TO_LOGIN LOGGER.log(Level.FINE, "Stored CLI authentication did not correspond to a valid user: " + username, e); return Jenkins.ANONYMOUS; } diff --git a/core/src/main/java/hudson/cli/LoginCommand.java b/core/src/main/java/hudson/cli/LoginCommand.java index 8920ad4111..55efd9dbcb 100644 --- a/core/src/main/java/hudson/cli/LoginCommand.java +++ b/core/src/main/java/hudson/cli/LoginCommand.java @@ -3,6 +3,7 @@ package hudson.cli; import hudson.Extension; import java.io.PrintStream; import jenkins.model.Jenkins; +import jenkins.security.SecurityListener; import org.acegisecurity.Authentication; import org.kohsuke.args4j.CmdLineException; @@ -45,6 +46,8 @@ public class LoginCommand extends CLICommand { ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel()); store.set(a); + SecurityListener.fireLoggedIn(a.getName()); + return 0; } diff --git a/core/src/main/java/hudson/cli/LogoutCommand.java b/core/src/main/java/hudson/cli/LogoutCommand.java index 822c99d84e..8578a6eb04 100644 --- a/core/src/main/java/hudson/cli/LogoutCommand.java +++ b/core/src/main/java/hudson/cli/LogoutCommand.java @@ -1,6 +1,9 @@ package hudson.cli; import hudson.Extension; +import jenkins.security.SecurityListener; +import org.acegisecurity.Authentication; + import java.io.PrintStream; /** @@ -27,7 +30,13 @@ public class LogoutCommand extends CLICommand { @Override protected int run() throws Exception { ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel()); + + Authentication auth = store.get(); + store.remove(); + + SecurityListener.fireLoggedOut(auth.getName()); + return 0; } } diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 0d1e037e6a..9d7d287c43 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -17,7 +17,8 @@ import hudson.PluginManager; import jenkins.util.SystemProperties; import hudson.util.DirScanner; import hudson.util.FileVisitor; -import hudson.util.Service; +import java.util.Iterator; +import java.util.ServiceLoader; /** * Strategy pattern of the various key decision making during the Jenkins initialization. @@ -113,11 +114,12 @@ public class InitStrategy { * Obtains the instance to be used. */ public static InitStrategy get(ClassLoader cl) throws IOException { - List r = Service.loadInstances(cl, InitStrategy.class); - if (r.isEmpty()) return new InitStrategy(); // default - - InitStrategy s = r.get(0); - LOGGER.fine("Using "+s+" as InitStrategy"); + Iterator it = ServiceLoader.load(InitStrategy.class, cl).iterator(); + if (!it.hasNext()) { + return new InitStrategy(); // default + } + InitStrategy s = it.next(); + LOGGER.log(Level.FINE, "Using {0} as InitStrategy", s); return s; } diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index ffee6413d8..b6be75139f 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -407,6 +407,7 @@ public abstract class AbstractProject

,R extends A /** * Gets the textual representation of the assigned label as it was entered by the user. */ + @Exported(name="labelExpression") public String getAssignedLabelString() { if (canRoam || assignedNode==null) return null; try { diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java index ebfb0fc310..7b16918f04 100644 --- a/core/src/main/java/hudson/model/Cause.java +++ b/core/src/main/java/hudson/model/Cause.java @@ -402,17 +402,36 @@ public abstract class Cause { */ public static class UserIdCause extends Cause { + @CheckForNull private String userId; + /** + * Constructor, which uses the current {@link User}. + */ public UserIdCause() { User user = User.current(); this.userId = (user == null) ? null : user.getId(); } + /** + * Constructor. + * @param userId User ID. {@code null} if the user is unknown. + * @since TODO + */ + public UserIdCause(@CheckForNull String userId) { + this.userId = userId; + } + @Exported(visibility = 3) + @CheckForNull public String getUserId() { return userId; } + + @Nonnull + private String getUserIdOrUnknown() { + return userId != null ? userId : User.getUnknown().getId(); + } @Exported(visibility = 3) public String getUserName() { @@ -435,8 +454,8 @@ public abstract class Cause { @Override public void print(TaskListener listener) { listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription( - // TODO better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping - ModelHyperlinkNote.encodeTo("/user/"+getUserId(), getUserName()))); + // TODO JENKINS-48467 - better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping + ModelHyperlinkNote.encodeTo("/user/"+getUserIdOrUnknown(), getUserName()))); } @Override diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index f950cce781..3a968f54a9 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -776,6 +776,12 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces return "computer/" + Util.rawEncode(getName()) + "/"; } + @Exported + public Set getAssignedLabels() { + Node node = getNode(); + return (node != null) ? node.getAssignedLabels() : Collections.EMPTY_SET; + } + /** * Returns projects that are tied on this node. */ diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java index cf6fdbff8b..0c26057765 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -127,7 +127,7 @@ public abstract class Label extends Actionable implements Comparable

+ * Usage from script of a subclass, say ViewHelper: + *

+ * new ViewHelper(delegate).method(); + *

+ * see ModularizeViewScript in ui-samples for an example how to use + * this class. + */ +public abstract class AbstractGroovyViewModule extends GroovyObjectSupport { + + public JellyBuilder builder; + public FormTagLib f; + public LayoutTagLib l; + public JenkinsTagLib t; + public Namespace st; + + public AbstractGroovyViewModule(JellyBuilder b) { + builder = b; + f = builder.namespace(FormTagLib.class); + l = builder.namespace(LayoutTagLib.class); + t = builder.namespace(JenkinsTagLib.class); + st = builder.namespace("jelly:stapler"); + } + + public Object methodMissing(String name, Object args) { + return builder.invokeMethod(name, args); + } + + public Object propertyMissing(String name) { + return builder.getProperty(name); + } + + public void propertyMissing(String name, Object value) { + builder.setProperty(name, value); + } + +} diff --git a/core/src/main/resources/hudson/markup/Messages_zh_CN.properties b/core/src/main/resources/hudson/markup/Messages_zh_CN.properties new file mode 100644 index 0000000000..9f9f5caf2e --- /dev/null +++ b/core/src/main/resources/hudson/markup/Messages_zh_CN.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +EscapedMarkupFormatter.DisplayName=\u7EAF\u6587\u672C diff --git a/core/src/main/resources/hudson/model/BooleanParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/BooleanParameterDefinition/config_zh_CN.properties index 0703474e44..ce517528d3 100644 --- a/core/src/main/resources/hudson/model/BooleanParameterDefinition/config_zh_CN.properties +++ b/core/src/main/resources/hudson/model/BooleanParameterDefinition/config_zh_CN.properties @@ -21,4 +21,4 @@ # THE SOFTWARE. Name=\u540D\u79F0 Default\ Value=\u9ED8\u8BA4\u503C -Description=\u8BF4\u660E +Description=\u63CF\u8FF0 diff --git a/core/src/main/resources/hudson/model/ChoiceParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/ChoiceParameterDefinition/config_zh_CN.properties new file mode 100644 index 0000000000..628e80119f --- /dev/null +++ b/core/src/main/resources/hudson/model/ChoiceParameterDefinition/config_zh_CN.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name=\u540D\u79F0 +Choices=\u9009\u9879 +Description=\u63CF\u8FF0 diff --git a/core/src/main/resources/hudson/model/FileParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/FileParameterDefinition/config_zh_CN.properties new file mode 100644 index 0000000000..15c668fdb4 --- /dev/null +++ b/core/src/main/resources/hudson/model/FileParameterDefinition/config_zh_CN.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +File\ location=\u6587\u4EF6\u8DEF\u5F84 +Description=\u63CF\u8FF0 diff --git a/core/src/main/resources/hudson/model/Job/permalinks_zh_CN.properties b/core/src/main/resources/hudson/model/Job/permalinks_zh_CN.properties index a743d1c687..f7ce595cdf 100644 --- a/core/src/main/resources/hudson/model/Job/permalinks_zh_CN.properties +++ b/core/src/main/resources/hudson/model/Job/permalinks_zh_CN.properties @@ -21,3 +21,5 @@ # THE SOFTWARE. Permalinks=\u76F8\u5173\u8FDE\u63A5 +Restrict\ where\ this\ project\ can\ be\ run=\u9650\u5236\u9879\u76EE\u7684\u8FD0\u884C\u8282\u70B9 +Label\ Expression=\u6807\u7B7E\u8868\u8FBE\u5F0F diff --git a/core/src/main/resources/hudson/model/Messages_zh_CN.properties b/core/src/main/resources/hudson/model/Messages_zh_CN.properties index 379e26ac1d..7c3ccde407 100644 --- a/core/src/main/resources/hudson/model/Messages_zh_CN.properties +++ b/core/src/main/resources/hudson/model/Messages_zh_CN.properties @@ -26,6 +26,30 @@ FreeStyleProject.Description=\u8FD9\u662FJenkins\u7684\u4E3B\u8981\u529F\u80FD.J HealthReport.EmptyString= +BallColor.Aborted=\u5DF2\u7EC8\u6B62 +BallColor.Disabled=\u7981\u7528 +BallColor.Failed=\u5931\u8D25 +BallColor.InProgress=\u6267\u884C\u4E2D +BallColor.NotBuilt=\u672A\u6784\u5EFA +BallColor.Pending=\u7B49\u5F85 +BallColor.Success=\u6210\u529F +BallColor.Unstable=\u4E0D\u7A33\u5B9A + +Item.Permissions.Title=\u4EFB\u52A1 +Item.CREATE.description=\u521B\u5EFA\u65B0\u7684\u4EFB\u52A1\u3002 +Item.DELETE.description=\u5220\u9664\u4EFB\u52A1\u3002 +Item.CONFIGURE.description=\u4FEE\u6539\u4EFB\u52A1\u7684\u914D\u7F6E\u3002 + +ResultTrend.Aborted=\u5DF2\u7EC8\u6B62 +ResultTrend.Failure=\u5931\u8D25 +ResultTrend.Fixed=\u56FA\u5B9A +ResultTrend.NotBuilt=\u672A\u6784\u5EFA +ResultTrend.NowUnstable=\u4E0D\u7A33\u5B9A +ResultTrend.StillFailing=\u4ECD\u7136\u5931\u8D25 +ResultTrend.StillUnstable=\u4ECD\u7136\u4E0D\u7A33\u5B9A +ResultTrend.Success=\u6210\u529F +ResultTrend.Unstable=\u4E0D\u7A33\u5B9A + Node.Mode.NORMAL=\u5C3D\u53EF\u80FD\u7684\u4F7F\u7528\u8FD9\u4E2A\u8282\u70B9 Node.Mode.EXCLUSIVE=\u53EA\u5141\u8BB8\u8FD0\u884C\u7ED1\u5B9A\u5230\u8FD9\u53F0\u673A\u5668\u7684Job @@ -34,3 +58,13 @@ MyView.DisplayName=\u6211\u7684\u89C6\u56FE ManageJenkinsAction.DisplayName=\u7CFB\u7EDF\u7BA1\u7406 ParametersDefinitionProperty.DisplayName=\u53C2\u6570\u5316\u6784\u5EFA\u8FC7\u7A0B ListView.DisplayName=\u7B80\u5355\u89C6\u56FE + +ParameterAction.DisplayName=\u53C2\u6570 +StringParameterDefinition.DisplayName=\u5B57\u7B26\u53C2\u6570 +TextParameterDefinition.DisplayName=\u6587\u672C\u53C2\u6570 +FileParameterDefinition.DisplayName=\u6587\u4EF6\u53C2\u6570 +BooleanParameterDefinition.DisplayName=\u5E03\u5C14\u503C\u53C2\u6570 +ChoiceParameterDefinition.DisplayName=\u9009\u9879\u53C2\u6570 +ChoiceParameterDefinition.MissingChoices=\u9700\u8981\u9009\u9879\u3002 +RunParameterDefinition.DisplayName=\u8FD0\u884C\u65F6\u53C2\u6570 +PasswordParameterDefinition.DisplayName=\u5BC6\u7801\u53C2\u6570 diff --git a/core/src/main/resources/hudson/model/PasswordParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/PasswordParameterDefinition/config_zh_CN.properties new file mode 100644 index 0000000000..67cd3a0398 --- /dev/null +++ b/core/src/main/resources/hudson/model/PasswordParameterDefinition/config_zh_CN.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name=\u540D\u79F0 +Default\ Value=\u9ED8\u8BA4\u503C +Description=\u63CF\u8FF0 diff --git a/core/src/main/resources/hudson/model/RunParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/RunParameterDefinition/config_zh_CN.properties new file mode 100644 index 0000000000..22a181a57a --- /dev/null +++ b/core/src/main/resources/hudson/model/RunParameterDefinition/config_zh_CN.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name=\u540D\u79F0 +Project=\u9879\u76EE +Description=\u63CF\u8FF0 + +All\ Builds=\u6240\u6709\u6784\u5EFA +Successful\ Builds\ Only=\u6210\u529F\u7684\u6784\u5EFA +Completed\ Builds\ Only=\u6267\u884C\u5B8C\u6210\u7684\u6784\u5EFA +Stable\ Builds\ Only=\u7A33\u5B9A\u7684\u6784\u5EFA + diff --git a/core/src/main/resources/hudson/model/StringParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/StringParameterDefinition/config_zh_CN.properties index 43c49864a8..f051916103 100644 --- a/core/src/main/resources/hudson/model/StringParameterDefinition/config_zh_CN.properties +++ b/core/src/main/resources/hudson/model/StringParameterDefinition/config_zh_CN.properties @@ -2,4 +2,5 @@ Default\ Value=\u9ED8\u8BA4\u503C Description=\u63CF\u8FF0 -Name=\u540D\u5B57 +Name=\u540D\u79F0 +Trim\ the\ string=\u6E05\u9664\u7A7A\u767D\u5B57\u7B26 diff --git a/core/src/main/resources/hudson/model/TextParameterDefinition/config_zh_CN.properties b/core/src/main/resources/hudson/model/TextParameterDefinition/config_zh_CN.properties new file mode 100644 index 0000000000..67cd3a0398 --- /dev/null +++ b/core/src/main/resources/hudson/model/TextParameterDefinition/config_zh_CN.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Name=\u540D\u79F0 +Default\ Value=\u9ED8\u8BA4\u503C +Description=\u63CF\u8FF0 diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly new file mode 100644 index 0000000000..70e615b78e --- /dev/null +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.jelly @@ -0,0 +1,42 @@ + + + + + + + + +

${%This URL requires POST}

+

+ ${%blurb} +

+

${requestURL}

+

${%warning}

+ + + + + + diff --git a/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties new file mode 100644 index 0000000000..0e47fdc8d5 --- /dev/null +++ b/core/src/main/resources/hudson/security/csrf/CrumbFilter/retry.properties @@ -0,0 +1,4 @@ +blurb = The URL you're trying to access requires that requests be sent using POST (like a form submission). \ + The button below allows you to retry accessing this URL using POST. \ + URL being accessed: +warning = If you were sent here from an untrusted source, please proceed with caution. \ No newline at end of file diff --git a/core/src/main/resources/lib/form/advanced_fr.properties b/core/src/main/resources/lib/form/advanced_fr.properties index 7f82aecfd4..c998a30a84 100644 --- a/core/src/main/resources/lib/form/advanced_fr.properties +++ b/core/src/main/resources/lib/form/advanced_fr.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Advanced=Avancé +Advanced=Avanc\u00E9 customizedFields=Un ou plusieurs champs de ce bloc ont \u00E9t\u00E9 \u00E9dit\u00E9s. diff --git a/core/src/main/resources/lib/hudson/project/config-assignedLabel_zh_CN.properties b/core/src/main/resources/lib/hudson/project/config-assignedLabel_zh_CN.properties new file mode 100644 index 0000000000..b9384115a4 --- /dev/null +++ b/core/src/main/resources/lib/hudson/project/config-assignedLabel_zh_CN.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2017, suren +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +Restrict\ where\ this\ project\ can\ be\ run=\u9650\u5236\u9879\u76EE\u7684\u8FD0\u884C\u8282\u70B9 +Label\ Expression=\u6807\u7B7E\u8868\u8FBE\u5F0F diff --git a/core/src/main/resources/lib/hudson/scriptConsole_de.properties b/core/src/main/resources/lib/hudson/scriptConsole_de.properties index 4db58a7382..6505dca93e 100644 --- a/core/src/main/resources/lib/hudson/scriptConsole_de.properties +++ b/core/src/main/resources/lib/hudson/scriptConsole_de.properties @@ -26,7 +26,7 @@ Run=Ausf\u00FChren description=Geben Sie ein beliebiges Groovy-Skript \ ein und f\u00FChren Sie dieses auf dem Server aus. Dies ist n\u00FCtzlich bei der Fehlersuche und zur Diagnostik. \ Verwenden Sie den println-Befehl, um Ausgaben sichtbar zu machen (wenn Sie System.out \ - verwenden, gehen die Ausgaben auf die Standardausgabe (STDOUT) des Servers, die schwieriger + verwenden, gehen die Ausgaben auf die Standardausgabe (STDOUT) des Servers, die schwieriger \ einzusehen ist). Beispiel: description2=Alle Klassen aller Plugins sind verf\u00FCgbar. jenkins.*, jenkins.model.*, hudson.* sowie hudson.model.* werden standardm\u00E4\u00DFig importiert. It\ is\ not\ possible\ to\ run\ scripts\ when\ agent\ is\ offline.=Es ist nicht m\u00F6glich, Skripte auf einem Agenten, der offline ist, auszuf\u00FChren diff --git a/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy b/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy deleted file mode 100644 index a429bef54e..0000000000 --- a/core/src/test/groovy/hudson/util/SecretRewriterTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -package hudson.util - -import com.trilead.ssh2.crypto.Base64 -import hudson.FilePath -import hudson.Functions -import jenkins.security.ConfidentialStoreRule -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder - -import javax.crypto.Cipher - -import static org.junit.Assume.assumeFalse - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class SecretRewriterTest { - @Rule - public MockSecretRule mockSecretRule = new MockSecretRule() - - @Rule - public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); - - @Rule public TemporaryFolder tmp = new TemporaryFolder() - - def FOO_PATTERN = /\{[A-Za-z0-9+\/]+={0,2}}<\/foo>/ - def MSG_PATTERN = /\{[A-Za-z0-9+\/]+={0,2}}<\/msg>/ - def FOO_PATTERN2 = /(\{[A-Za-z0-9+\/]+={0,2}}<\/foo>){2}/ - def ABC_FOO_PATTERN = /\s\{[A-Za-z0-9+\/]+={0,2}}<\/foo>\s<\/abc>/ - - @Test - void singleFileRewrite() { - def o = encryptOld('foobar') // old - def n = encryptNew('foobar') // new - roundtrip "${o}", - {assert it ==~ FOO_PATTERN} - - - roundtrip "${o}${o}", - {assert it ==~ FOO_PATTERN2} - - roundtrip "${n}", - {assert it == "${n}"} - - roundtrip " thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret ", - {assert it == "thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret"} - - // to be rewritten, it needs to be between a tag - roundtrip "$o", {assert it == "$o"} - roundtrip "$o", {assert it == "$o"} - - // - roundtrip "\n$o\n", {assert it ==~ ABC_FOO_PATTERN} - } - - void roundtrip(String before, Closure check) { - def sr = new SecretRewriter(null); - def f = File.createTempFile("test", "xml", tmp.root) - f.text = before - sr.rewrite(f,null) - check(f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()) - //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() - } - - String encryptOld(str) { - def cipher = Secret.getCipher("AES"); - cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.legacyKey); - return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))) - } - - String encryptNew(str) { - return Secret.fromString(str).encryptedValue - } - - /** - * Directory rewrite and recursion detection - */ - @Test - void recursionDetection() { - assumeFalse("Symlinks don't work on Windows very well", Functions.isWindows()) - def sw = new SecretRewriter(); - def st = StreamTaskListener.fromStdout() - - def o = encryptOld("Hello world") - def n = encryptNew("Hello world") - def payload = "$o" - def answer = "$n" - - // set up some directories with stuff - def t = tmp.newFolder("t") - def dirs = ["a", "b", "c", "c/d", "c/d/e"] - dirs.each { p -> - def d = new File(t, p) - d.mkdir() - new File(d,"foo.xml").text = payload - } - - // stuff outside - def t2 = tmp.newFolder("t2") - new File(t2,"foo.xml").text = payload - - // some recursions as well as valid symlinks - new FilePath(t).child("c/symlink").symlinkTo("..",st) - new FilePath(t).child("b/symlink").symlinkTo(".",st) - new FilePath(t).child("a/symlink").symlinkTo(t2.absolutePath,st) - - assert 6==sw.rewriteRecursive(t, st) - - dirs.each { p-> - assert new File(t,"$p/foo.xml").text.trim() ==~ MSG_PATTERN - } - - // t2 is only reachable by following a symlink. this should be covered, too - assert new File(t2,"foo.xml").text.trim() ==~ MSG_PATTERN - } - -} diff --git a/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy deleted file mode 100644 index e3816a342c..0000000000 --- a/core/src/test/groovy/jenkins/security/CryptoConfidentialKeyTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class CryptoConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - def key = new CryptoConfidentialKey("test") - - @Test - void decryptGetsPlainTextBack() { - ["Hello world","","\u0000"].each { str -> - assert key.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes - } - } - - @Test - void multipleEncryptsAreIdempotent() { - def str = "Hello world".bytes - assert key.encrypt().doFinal(str)==key.encrypt().doFinal(str) - } - - @Test - void loadingExistingKey() { - def key2 = new CryptoConfidentialKey("test") // this will cause the key to be loaded from the disk - ["Hello world","","\u0000"].each { str -> - assert key2.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes - } - } -} diff --git a/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy b/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy deleted file mode 100644 index 83cdaf218c..0000000000 --- a/core/src/test/groovy/jenkins/security/DefaultConfidentialStoreTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package jenkins.security - -import hudson.FilePath -import hudson.Functions -import hudson.Util -import org.junit.After -import org.junit.Before -import org.junit.Test - -/** - * @author Kohsuke Kawaguchi - */ -public class DefaultConfidentialStoreTest { - - def tmp; - - @Before - void setup() { - tmp = Util.createTempDir() - } - - @After - void tearDown() { - Util.deleteRecursive(tmp) - } - - @Test - void roundtrip() { - tmp.deleteDir() // let ConfidentialStore create a directory - - def store = new DefaultConfidentialStore(tmp); - def key = new ConfidentialKey("test") {}; - - // basic roundtrip - def str = "Hello world!" - store.store(key, str.bytes) - assert new String(store.load(key))==str - - // data storage should have some stuff - assert new File(tmp,"test").exists() - assert new File(tmp,"master.key").exists() - - assert !new File(tmp,"test").text.contains("Hello") // the data shouldn't be a plain text, obviously - - if (!Functions.isWindows()) - assert (new FilePath(tmp).mode()&0777) == 0700 // should be read only - - // if the master key changes, we should gracefully fail to load the store - new File(tmp,"master.key").delete() - def store2 = new DefaultConfidentialStore(tmp) - assert new File(tmp,"master.key").exists() // we should have a new key now - assert store2.load(key)==null; - } -} diff --git a/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy deleted file mode 100644 index 118ff5b904..0000000000 --- a/core/src/test/groovy/jenkins/security/HMACConfidentialKeyTest.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class HMACConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - def key = new HMACConfidentialKey("test",16) - - @Test - void basics() { - def unique = [] as TreeSet - ["Hello world","","\u0000"].each { str -> - def mac = key.mac(str) - unique.add(mac) - assert mac =~ /[0-9A-Fa-f]{32}/ - assert key.checkMac(str,mac) - assert !key.checkMac("garbage",mac) - } - - assert unique.size()==3 // make sure all 3 MAC are different - } - - @Test - void loadingExistingKey() { - // this second key of the same ID will cause it to load the key from the disk - def key2 = new HMACConfidentialKey("test",16) - ["Hello world","","\u0000"].each { str -> - assert key.mac(str)==key2.mac(str) - } - } -} diff --git a/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy deleted file mode 100644 index 7062d2326f..0000000000 --- a/core/src/test/groovy/jenkins/security/HexStringConfidentialKeyTest.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package jenkins.security - -import org.junit.Rule -import org.junit.Test - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class HexStringConfidentialKeyTest { - @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() - - @Test - void hexStringShouldProduceHexString() { - def key = new HexStringConfidentialKey("test",8) - assert key.get() =~ /[A-Fa-f0-9]{8}/ - } - - @Test - void multipleGetsAreIdempotent() { - def key = new HexStringConfidentialKey("test",8) - assert key.get()==key.get() - } - - @Test - void specifyLengthAndMakeSureItTakesEffect() { - [8,16,32,256].each { n -> - new HexStringConfidentialKey("test"+n,n).get().length()==n - } - } - - @Test - void loadingExistingKey() { - def key1 = new HexStringConfidentialKey("test",8) - key1.get() // this causes the ke to be generated - - // this second key of the same ID will cause it to load the key from the disk - def key2 = new HexStringConfidentialKey("test",8) - assert key1.get()==key2.get() - } -} diff --git a/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy b/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy deleted file mode 100644 index e39addae77..0000000000 --- a/core/src/test/groovy/jenkins/util/AtmostOneTaskExecutorTest.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package jenkins.util - -import hudson.util.OneShotEvent -import org.junit.Test - -import java.util.concurrent.Callable -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class AtmostOneTaskExecutorTest { - def counter = new AtomicInteger() - - def lock = new OneShotEvent() - - @Test - public void doubleBooking() { - def f1,f2; - - def base = Executors.newCachedThreadPool() - def es = new AtmostOneTaskExecutor(base, - { -> - counter.incrementAndGet() - lock.block() - } as Callable); - f1 = es.submit() - while (counter.get() == 0) - ; // spin lock until executor gets to the choking point - - f2 = es.submit() // this should hang - Thread.sleep(500) // make sure the 2nd task is hanging - assert counter.get() == 1 - assert !f2.isDone() - - lock.signal() // let the first one go - - f1.get(); // first one should complete - - // now 2nd one gets going and hits the choke point - f2.get() - assert counter.get()==2 - } -} diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 21a8993de4..f1cf614055 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -36,6 +36,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import static org.hamcrest.CoreMatchers.allOf; @@ -287,12 +290,6 @@ public class UtilTest { @Test public void testDeleteFile_onWindows() throws Exception { Assume.assumeTrue(Functions.isWindows()); - Class c; - try { - c = Class.forName("java.nio.file.FileSystemException"); - } catch (ClassNotFoundException x) { - throw new AssumptionViolatedException("prior to JDK 7", x); - } final int defaultDeletionMax = Util.DELETION_MAX; try { File f = tmp.newFile(); @@ -304,7 +301,7 @@ public class UtilTest { Util.deleteFile(f); fail("should not have been deletable"); } catch (IOException x) { - assertThat(calcExceptionHierarchy(x), hasItem(c)); + assertThat(calcExceptionHierarchy(x), hasItem(FileSystemException.class)); assertThat(x.getMessage(), containsString(f.getPath())); } } finally { @@ -313,6 +310,27 @@ public class UtilTest { } } + @Test + public void testDeleteFileReadOnly() throws Exception { + // Removing the calls to Util#makeWritable in Util#tryOnceDeleteFile should cause this test to fail. + Path file = tmp.newFolder().toPath().resolve("file.tmp"); + Files.createDirectories(file.getParent()); + Files.createFile(file); + // Using old IO so the test can run on Windows. + file.getParent().toFile().setWritable(false); + file.toFile().setWritable(false); + Util.deleteFile(file.toFile()); + assertFalse(Files.exists(file)); + } + + @Test + public void testDeleteFileDoesNotExist() throws Exception { + Path file = tmp.newFolder().toPath().resolve("file.tmp"); + assertFalse(Files.exists(file)); + // Should not throw an exception. + Util.deleteFile(file.toFile()); + } + @Test public void testDeleteContentsRecursive() throws Exception { final File dir = tmp.newFolder(); diff --git a/core/src/test/java/hudson/util/SecretRewriterTest.java b/core/src/test/java/hudson/util/SecretRewriterTest.java new file mode 100644 index 0000000000..5ca97ff060 --- /dev/null +++ b/core/src/test/java/hudson/util/SecretRewriterTest.java @@ -0,0 +1,116 @@ +package hudson.util; + +import com.trilead.ssh2.crypto.Base64; +import hudson.FilePath; +import hudson.Functions; +import hudson.model.TaskListener; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import javax.crypto.Cipher; +import jenkins.security.ConfidentialStoreRule; +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class SecretRewriterTest { + + @Rule + public MockSecretRule mockSecretRule = new MockSecretRule(); + + @Rule + public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private static final Pattern FOO_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); + private static final Pattern MSG_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); + private static final Pattern FOO_PATTERN2 = Pattern.compile("([{][A-Za-z0-9+/]+={0,2}[}]){2}"); + private static final Pattern ABC_FOO_PATTERN = Pattern.compile("\\s[{][A-Za-z0-9+/]+={0,2}[}]\\s"); + + @Test + public void singleFileRewrite() throws Exception { + String o = encryptOld("foobar"); // old + String n = encryptNew("foobar"); // new + assertTrue(FOO_PATTERN.matcher(roundtrip("" + o + "")).matches()); + assertTrue(FOO_PATTERN2.matcher(roundtrip("" + o + "" + o + "")).matches()); + assertEquals("" + n + "", roundtrip("" + n + "")); + assertEquals("thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret", roundtrip(" thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret ")); + // to be rewritten, it needs to be between a tag + assertEquals("" + o, roundtrip("" + o)); + assertEquals(o + "", roundtrip(o + "")); + assertTrue(ABC_FOO_PATTERN.matcher(roundtrip("\n" + o + "\n")).matches()); + } + + private String roundtrip(String before) throws Exception { + SecretRewriter sr = new SecretRewriter(null); + File f = File.createTempFile("test", "xml", tmp.getRoot()); + FileUtils.write(f, before); + sr.rewrite(f, null); + //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() + return FileUtils.readFileToString(f).replaceAll(System.getProperty("line.separator"), "\n").trim(); + } + + private String encryptOld(String str) throws Exception { + Cipher cipher = Secret.getCipher("AES"); + cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.getLegacyKey()); + return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))); + } + + private String encryptNew(String str) { + return Secret.fromString(str).getEncryptedValue(); + } + + /** + * Directory rewrite and recursion detection + */ + @Test + public void recursionDetection() throws Exception { + assumeFalse("Symlinks don't work on Windows very well", Functions.isWindows()); + SecretRewriter sw = new SecretRewriter(); + TaskListener st = StreamTaskListener.fromStdout(); + + String o = encryptOld("Hello world"); + String n = encryptNew("Hello world"); + String payload = "" + o + ""; + String answer = "" + n + ""; + + // set up some directories with stuff + File t = tmp.newFolder("t"); + List dirs = Arrays.asList("a", "b", "c", "c/d", "c/d/e"); + for (String p : dirs) { + File d = new File(t, p); + d.mkdir(); + try { + FileUtils.write(new File(d, "foo.xml"), payload); + } catch (IOException x) { + assert false : x; + } + } + + // stuff outside + File t2 = tmp.newFolder("t2"); + FileUtils.write(new File(t2, "foo.xml"), payload); + + // some recursions as well as valid symlinks + new FilePath(t).child("c/symlink").symlinkTo("..", st); + new FilePath(t).child("b/symlink").symlinkTo(".", st); + new FilePath(t).child("a/symlink").symlinkTo(t2.getAbsolutePath(), st); + + assertEquals(6, sw.rewriteRecursive(t, st)); + + for (String p : dirs) { + assertTrue(MSG_PATTERN.matcher(FileUtils.readFileToString(new File(t, p + "/foo.xml")).trim()).matches()); + } + + // t2 is only reachable by following a symlink. this should be covered, too + assertTrue(MSG_PATTERN.matcher(FileUtils.readFileToString(new File(t2, "foo.xml")).trim()).matches()); + } + +} diff --git a/core/src/test/groovy/hudson/util/SecretTest.groovy b/core/src/test/java/hudson/util/SecretTest.java similarity index 55% rename from core/src/test/groovy/hudson/util/SecretTest.groovy rename to core/src/test/java/hudson/util/SecretTest.java index 8f39ae7ec0..0b9c13b9b6 100644 --- a/core/src/test/groovy/hudson/util/SecretTest.groovy +++ b/core/src/test/java/hudson/util/SecretTest.java @@ -21,51 +21,51 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.util + +package hudson.util; import com.trilead.ssh2.crypto.Base64; -import jenkins.model.Jenkins +import java.util.Random; +import java.util.regex.Pattern; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import jenkins.model.Jenkins; import jenkins.security.ConfidentialStoreRule; import org.apache.commons.lang.RandomStringUtils; -import org.junit.Rule -import org.junit.Test +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; -import java.util.Random; -import javax.crypto.Cipher -import java.util.regex.Pattern; - -/** - * @author Kohsuke Kawaguchi - */ public class SecretTest { - @Rule - public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule() @Rule - public MockSecretRule mockSecretRule = new MockSecretRule() + public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule(); - static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?"); + @Rule + public MockSecretRule mockSecretRule = new MockSecretRule(); + + private static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?"); @Test - void testEncrypt() { - def secret = Secret.fromString("abc"); - assert "abc"==secret.plainText; + public void encrypt() { + Secret secret = Secret.fromString("abc"); + assertEquals("abc", secret.getPlainText()); // make sure we got some encryption going - println secret.encryptedValue; - assert !"abc".equals(secret.encryptedValue); + assertNotEquals("abc", secret.getEncryptedValue()); // can we round trip? - assert secret==Secret.fromString(secret.encryptedValue); + assertEquals(secret, Secret.fromString(secret.getEncryptedValue())); //Two consecutive encryption requests of the same object should result in the same encrypted value - SECURITY-304 - assert secret.encryptedValue == secret.encryptedValue + assertEquals(secret.getEncryptedValue(), secret.getEncryptedValue()); //Two consecutive encryption requests of different objects with the same value should not result in the same encrypted value - SECURITY-304 - assert secret.encryptedValue != Secret.fromString(secret.plainText).encryptedValue + assertNotEquals(secret.getEncryptedValue(), Secret.fromString(secret.getPlainText()).getEncryptedValue()); } @Test - void testEncryptedValuePattern() { + public void encryptedValuePattern() { for (int i = 1; i < 100; i++) { String plaintext = RandomStringUtils.random(new Random().nextInt(i)); String ciphertext = Secret.fromString(plaintext).getEncryptedValue(); @@ -83,19 +83,20 @@ public class SecretTest { } @Test - void testDecrypt() { - assert "abc"==Secret.toString(Secret.fromString("abc")) + public void decrypt() { + assertEquals("abc", Secret.toString(Secret.fromString("abc"))); } @Test - void testSerialization() { - def s = Secret.fromString("Mr.Jenkins"); - def xml = Jenkins.XSTREAM.toXML(s); - assert !xml.contains(s.plainText) - assert xml ==~ /\{[A-Za-z0-9+\/]+={0,2}}<\/hudson\.util\.Secret>/ + public void serialization() { + Secret s = Secret.fromString("Mr.Jenkins"); + String xml = Jenkins.XSTREAM.toXML(s); + assertThat(xml, not(containsString(s.getPlainText()))); + // TODO MatchesPattern not available until Hamcrest 2.0 + assertTrue(xml, xml.matches("[{][A-Za-z0-9+/]+={0,2}[}]")); - def o = Jenkins.XSTREAM.fromXML(xml); - assert o==s : xml; + Object o = Jenkins.XSTREAM.fromXML(xml); + assertEquals(xml, s, o); } public static class Foo { @@ -106,27 +107,28 @@ public class SecretTest { * Makes sure the serialization form is backward compatible with String. */ @Test - void testCompatibilityFromString() { - def tagName = Foo.class.name.replace("\$","_-"); - def xml = "<$tagName>secret"; - def foo = new Foo(); + public void testCompatibilityFromString() { + String tagName = Foo.class.getName().replace("$", "_-"); + String xml = "<" + tagName + ">secret"; + Foo foo = new Foo(); Jenkins.XSTREAM.fromXML(xml, foo); - assert "secret"==Secret.toString(foo.password) + assertEquals("secret", Secret.toString(foo.password)); } /** * Secret persisted with Jenkins.getSecretKey() should still decrypt OK. */ @Test - void migrationFromLegacyKeyToConfidentialStore() { - def legacy = HistoricalSecrets.legacyKey - ["Hello world","","\u0000unprintable"].each { str -> - def cipher = Secret.getCipher("AES"); + public void migrationFromLegacyKeyToConfidentialStore() throws Exception { + SecretKey legacy = HistoricalSecrets.getLegacyKey(); + for (String str : new String[] {"Hello world", "", "\u0000unprintable"}) { + Cipher cipher = Secret.getCipher("AES"); cipher.init(Cipher.ENCRYPT_MODE, legacy); - def old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))) - def s = Secret.fromString(old) - assert s.plainText==str : "secret by the old key should decrypt" - assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use" + String old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8")))); + Secret s = Secret.fromString(old); + assertEquals("secret by the old key should decrypt", str, s.getPlainText()); + assertNotEquals("but when encrypting, ConfidentialKey should be in use", old, s.getEncryptedValue()); } } + } diff --git a/core/src/test/java/hudson/util/TextFileTest.java b/core/src/test/java/hudson/util/TextFileTest.java new file mode 100644 index 0000000000..cb689a080f --- /dev/null +++ b/core/src/test/java/hudson/util/TextFileTest.java @@ -0,0 +1,97 @@ +package hudson.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class TextFileTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void head() throws Exception { + File f = tmp.newFile(); + FileUtils.copyURLToFile(getClass().getResource("ascii.txt"), f); + + TextFile t = new TextFile(f); + String first35 = "Lorem ipsum dolor sit amet, consect"; + assertEquals(35, first35.length()); + assertEquals(first35, t.head(35)); + } + + @Test + public void shortHead() throws Exception { + File f = tmp.newFile(); + FileUtils.write(f, "hello"); + + TextFile t = new TextFile(f); + assertEquals("hello", t.head(35)); + } + + @Test + public void tail() throws Exception { + File f = tmp.newFile(); + FileUtils.copyURLToFile(getClass().getResource("ascii.txt"), f); + + TextFile t = new TextFile(f); + String tailStr = "la, vitae interdum quam rutrum id." + System.lineSeparator(); + assertEquals(34 + System.lineSeparator().length(), tailStr.length()); + assertEquals(tailStr, t.fastTail(tailStr.length())); + } + + @Test + public void shortTail() throws Exception { + File f = tmp.newFile(); + FileUtils.write(f, "hello"); + + TextFile t = new TextFile(f); + assertEquals("hello", t.fastTail(35)); + } + + /** + * Shift JIS is a multi-byte character encoding. + * + * In it, 0x82 0x83 is \u30e2, and 0x83 0x82 is \uFF43. So if aren't + * careful, we'll parse the text incorrectly. + */ + @Test + public void tailShiftJIS() throws Exception { + File f = tmp.newFile(); + + TextFile t = new TextFile(f); + + try (OutputStream o = new FileOutputStream(f)) { + for (int i = 0; i < 80; i++) { + for (int j = 0; j < 40; j++) { + o.write(0x83); + o.write(0x82); + } + o.write(0x0A); + } + } + + String tail = t.fastTail(35, Charset.forName("Shift_JIS")); + assertEquals(StringUtils.repeat("\u30e2", 34) + "\n", tail); + assertEquals(35, tail.length()); + + // add one more byte to force fastTail to read from one byte ahead + // between this and the previous case, it should start parsing text incorrectly, until it hits NL + // where it comes back in sync + try (OutputStream o = new FileOutputStream(f, true)) { + o.write(0x0A); + } + + tail = t.fastTail(35, Charset.forName("Shift_JIS")); + assertEquals(StringUtils.repeat("\u30e2", 33) + "\n\n", tail); + assertEquals(35, tail.length()); + } + +} diff --git a/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java b/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java new file mode 100644 index 0000000000..1b302a358b --- /dev/null +++ b/core/src/test/java/jenkins/security/CryptoConfidentialKeyTest.java @@ -0,0 +1,35 @@ +package jenkins.security; + +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class CryptoConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + private CryptoConfidentialKey key = new CryptoConfidentialKey("test"); + + @Test + public void decryptGetsPlainTextBack() throws Exception { + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertArrayEquals(str.getBytes(), key.decrypt().doFinal(key.encrypt().doFinal(str.getBytes()))); + } + } + + @Test + public void multipleEncryptsAreIdempotent() throws Exception { + byte[] str = "Hello world".getBytes(); + assertArrayEquals(key.encrypt().doFinal(str), key.encrypt().doFinal(str)); + } + + @Test + public void loadingExistingKey() throws Exception { + CryptoConfidentialKey key2 = new CryptoConfidentialKey("test"); // this will cause the key to be loaded from the disk + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertArrayEquals(str.getBytes(), key2.decrypt().doFinal(key.encrypt().doFinal(str.getBytes()))); + } + } + +} diff --git a/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java b/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java new file mode 100644 index 0000000000..539b72ddc4 --- /dev/null +++ b/core/src/test/java/jenkins/security/DefaultConfidentialStoreTest.java @@ -0,0 +1,47 @@ +package jenkins.security; + +import hudson.FilePath; +import hudson.Functions; +import java.io.File; +import org.apache.commons.io.FileUtils; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class DefaultConfidentialStoreTest { + + @Rule + public TemporaryFolder tmpRule = new TemporaryFolder(); + + @Test + public void roundtrip() throws Exception { + File tmp = new File(tmpRule.getRoot(), "tmp"); // let ConfidentialStore create a directory + + DefaultConfidentialStore store = new DefaultConfidentialStore(tmp); + ConfidentialKey key = new ConfidentialKey("test") {}; + + // basic roundtrip + String str = "Hello world!"; + store.store(key, str.getBytes()); + assertEquals(str, new String(store.load(key))); + + // data storage should have some stuff + assertTrue(new File(tmp, "test").exists()); + assertTrue(new File(tmp, "master.key").exists()); + + assertThat(FileUtils.readFileToString(new File(tmp, "test")), not(containsString("Hello"))); // the data shouldn't be a plain text, obviously + + if (!Functions.isWindows()) { + assertEquals(0700, new FilePath(tmp).mode() & 0777); // should be read only + } + + // if the master key changes, we should gracefully fail to load the store + new File(tmp, "master.key").delete(); + DefaultConfidentialStore store2 = new DefaultConfidentialStore(tmp); + assertTrue(new File(tmp, "master.key").exists()); // we should have a new key now + assertNull(store2.load(key)); + } + +} diff --git a/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java b/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java new file mode 100644 index 0000000000..93cb94cfdb --- /dev/null +++ b/core/src/test/java/jenkins/security/HMACConfidentialKeyTest.java @@ -0,0 +1,38 @@ +package jenkins.security; + +import java.util.Set; +import java.util.TreeSet; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class HMACConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + private HMACConfidentialKey key = new HMACConfidentialKey("test", 16); + + @Test + public void basics() { + Set unique = new TreeSet<>(); + for (String str : new String[] {"Hello world", "", "\u0000"}) { + String mac = key.mac(str); + unique.add(mac); + assertTrue(mac, mac.matches("[0-9A-Fa-f]{32}")); + assertTrue(key.checkMac(str, mac)); + assertFalse(key.checkMac("garbage", mac)); + } + assertEquals("all 3 MAC are different", 3, unique.size()); + } + + @Test + public void loadingExistingKey() { + // this second key of the same ID will cause it to load the key from the disk + HMACConfidentialKey key2 = new HMACConfidentialKey("test", 16); + for (String str : new String[] {"Hello world", "", "\u0000"}) { + assertEquals(key.mac(str), key2.mac(str)); + } + } + +} diff --git a/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java b/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java new file mode 100644 index 0000000000..e76ad914f9 --- /dev/null +++ b/core/src/test/java/jenkins/security/HexStringConfidentialKeyTest.java @@ -0,0 +1,41 @@ +package jenkins.security; + +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class HexStringConfidentialKeyTest { + + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule(); + + @Test + public void hexStringShouldProduceHexString() { + HexStringConfidentialKey key = new HexStringConfidentialKey("test", 8); + assertTrue(key.get().matches("[A-Fa-f0-9]{8}")); + } + + @Test + public void multipleGetsAreIdempotent() { + HexStringConfidentialKey key = new HexStringConfidentialKey("test", 8); + assertEquals(key.get(), key.get()); + } + + @Test + public void specifyLengthAndMakeSureItTakesEffect() { + for (int n : new int[] {8, 16, 32, 256}) { + assertEquals(n, new HexStringConfidentialKey("test" + n, n).get().length()); + } + } + + @Test + public void loadingExistingKey() { + HexStringConfidentialKey key1 = new HexStringConfidentialKey("test", 8); + key1.get(); // this causes the ke to be generated + + // this second key of the same ID will cause it to load the key from the disk + HexStringConfidentialKey key2 = new HexStringConfidentialKey("test", 8); + assertEquals(key1.get(), key2.get()); + } + +} diff --git a/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy b/core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java similarity index 74% rename from core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy rename to core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java index ed9ef183fe..7b4513940c 100644 --- a/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy +++ b/core/src/test/java/jenkins/security/RSAConfidentialKeyTest.java @@ -21,28 +21,26 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package jenkins.security +package jenkins.security; -import org.junit.Rule -import org.junit.Test +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; + +public class RSAConfidentialKeyTest { -/** - * - * - * @author Kohsuke Kawaguchi - */ -class RSAConfidentialKeyTest { @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() + public ConfidentialStoreRule store = new ConfidentialStoreRule(); - def key = new RSAConfidentialKey("test") {} + private RSAConfidentialKey key = new RSAConfidentialKey("test") {}; @Test - void loadingExistingKey() { + public void loadingExistingKey() { // this second key of the same ID will cause it to load the key from the disk - def key2 = new RSAConfidentialKey("test") {} + RSAConfidentialKey key2 = new RSAConfidentialKey("test") {}; - assert key.privateKey==key2.privateKey; - assert key.publicKey ==key2.publicKey; + assertEquals(key.getPrivateKey(), key2.getPrivateKey()); + assertEquals(key.getPublicKey(), key2.getPublicKey()); } + } diff --git a/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy b/core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java similarity index 65% rename from core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy rename to core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java index 7c31c13365..16e57e117f 100644 --- a/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy +++ b/core/src/test/java/jenkins/security/RSADigitalSignatureConfidentialKeyTest.java @@ -21,35 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package jenkins.security +package jenkins.security; -import hudson.remoting.Base64 -import org.junit.Rule -import org.junit.Test +import hudson.remoting.Base64; +import java.security.Signature; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; -import java.security.Signature +public class RSADigitalSignatureConfidentialKeyTest { -/** - * - * - * @author Kohsuke Kawaguchi - */ -class RSADigitalSignatureConfidentialKeyTest { @Rule - public ConfidentialStoreRule store = new ConfidentialStoreRule() + public ConfidentialStoreRule store = new ConfidentialStoreRule(); - def key = new RSADigitalSignatureConfidentialKey("test"); + private final RSADigitalSignatureConfidentialKey key = new RSADigitalSignatureConfidentialKey("test"); @Test - void dsigSignAndVerify() { - def plainText = "Hello world" - def msg = key.sign(plainText); - println msg; + public void dsigSignAndVerify() throws Exception { + String plainText = "Hello world"; + String msg = key.sign(plainText); + System.out.println(msg); - def sig = Signature.getInstance("SHA256withRSA"); - sig.initVerify(key.publicKey); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(key.getPublicKey()); sig.update(plainText.getBytes("UTF-8")); - assert sig.verify(Base64.decode(msg)) + assertTrue(sig.verify(Base64.decode(msg))); } + } diff --git a/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java b/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java new file mode 100644 index 0000000000..c3e8b8461a --- /dev/null +++ b/core/src/test/java/jenkins/util/AtmostOneTaskExecutorTest.java @@ -0,0 +1,49 @@ +package jenkins.util; + +import hudson.util.OneShotEvent; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.Assert.*; +import org.junit.Test; + +public class AtmostOneTaskExecutorTest { + + @SuppressWarnings("empty-statement") + @Test + public void doubleBooking() throws Exception { + AtomicInteger counter = new AtomicInteger(); + OneShotEvent lock = new OneShotEvent(); + Future f1, f2; + + ExecutorService base = Executors.newCachedThreadPool(); + AtmostOneTaskExecutor es = new AtmostOneTaskExecutor(base, () -> { + counter.incrementAndGet(); + try { + lock.block(); + } catch (InterruptedException x) { + assert false : x; + } + return null; + }); + f1 = es.submit(); + while (counter.get() == 0) { + // spin lock until executor gets to the choking point + } + + f2 = es.submit(); // this should hang + Thread.sleep(500); // make sure the 2nd task is hanging + assertEquals(1, counter.get()); + assertFalse(f2.isDone()); + + lock.signal(); // let the first one go + + f1.get(); // first one should complete + + // now 2nd one gets going and hits the choke point + f2.get(); + assertEquals(2, counter.get()); + } + +} diff --git a/test/src/test/resources/hudson/util/ascii.txt b/core/src/test/resources/hudson/util/ascii.txt similarity index 100% rename from test/src/test/resources/hudson/util/ascii.txt rename to core/src/test/resources/hudson/util/ascii.txt diff --git a/pom.xml b/pom.xml index 223c8a1ffc..6d88780f12 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.97-SNAPSHOT pom Jenkins main module @@ -94,7 +94,7 @@ THE SOFTWARE. ${skipTests} 3.0.4 true - 1.11 + 1.12 ${access-modifier.version} ${access-modifier.version} @@ -153,12 +153,17 @@ THE SOFTWARE. org.powermock powermock-module-junit4 - 1.6.2 + 1.6.6 org.powermock powermock-api-mockito - 1.6.2 + 1.6.6 + + + org.objenesis + objenesis + 2.6 @@ -339,39 +344,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - 1.5-jenkins-3 - - - org.apache.ant - ant-junit - 1.7.0 - - - net.sourceforge.cobertura - cobertura - 1.9 - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - org.codehaus.gmaven.runtime - gmaven-runtime-2.0 - 1.5-jenkins-3 - - - - - 2.0 - - org.apache.maven.plugins maven-deploy-plugin @@ -516,10 +488,6 @@ THE SOFTWARE. antlr-maven-plugin 2.1 - - org.codehaus.mojo - cobertura-maven-plugin - org.codehaus.mojo findbugs-maven-plugin @@ -584,20 +552,6 @@ THE SOFTWARE. - - - org.codehaus.gmaven - gmaven-plugin - 1.5-jenkins-1 - - execute - testCompile - - - - - - org.apache.maven.plugins @@ -722,34 +676,6 @@ THE SOFTWARE. org.jenkins-ci.tools maven-jenkins-dev-plugin - - - - org.codehaus.gmaven - gmaven-plugin - - - - generateStubs - compile - generateTestStubs - testCompile - - - - - - diff --git a/test/pom.xml b/test/pom.xml index 9dccd5a728..da6245605c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.97-SNAPSHOT test @@ -207,7 +207,6 @@ THE SOFTWARE. org.objenesis objenesis - 2.5.1 test @@ -241,34 +240,6 @@ THE SOFTWARE. ${concurrency} - - org.codehaus.gmaven - gmaven-plugin - - - - default - - - generateTestStubs - testCompile - - - - - - org.apache.ant - ant - 1.8.0 - - - - org.apache.ant - ant-launcher - 1.8.0 - - - maven-deploy-plugin @@ -320,37 +291,9 @@ THE SOFTWARE. true 4 + 100 - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - - ${project.basedir}/../core/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - jacoco diff --git a/test/src/build-script/unitTest.groovy b/test/src/build-script/unitTest.groovy deleted file mode 100644 index 73e5a56465..0000000000 --- a/test/src/build-script/unitTest.groovy +++ /dev/null @@ -1,16 +0,0 @@ -// run unit tests -import org.apache.commons.io.FileUtils - -ant.project.setBaseDir(project.basedir) - -// start from where the core left off, and build from there -ser=new File(project.basedir,"target/cobertura.ser"); -FileUtils.copyFile(maven.resolveArtifact("${project.groupId}:hudson-core:${project.version}:cobertura:ser"),ser) - -cob = new Cobertura(project,maven,ant,ser); - -// instrumenting remote.jar causes all native m2 job to fail, because forked Maven doesn't have cobertura runtime -cob.instrument(["hudson-core"].collect{ m -> maven.resolveArtifact("${project.groupId}:${m}:${project.version}") }) -cob.runTests() -cob.report(["../core/src/main/java"]) -cob.makeBuildFailIfTestFail(); diff --git a/test/src/test/groovy/hudson/RelativePathTest.groovy b/test/src/test/groovy/hudson/RelativePathTest.groovy deleted file mode 100644 index 25902f811c..0000000000 --- a/test/src/test/groovy/hudson/RelativePathTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package hudson - -import hudson.model.AbstractDescribableImpl -import hudson.model.Describable -import hudson.model.Descriptor -import hudson.util.ListBoxModel -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.HudsonTestCase -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.QueryParameter - -/** - * Regression test for JENKINS-18776 - * - * @author Kohsuke Kawaguchi - */ -class RelativePathTest extends HudsonTestCase implements Describable { - @Issue("JENKINS-18776") - void testRelativePath() { - // I was having trouble causing annotation processing on test stubs - jenkins.getDescriptorOrDie(RelativePathTest.class) - jenkins.getDescriptorOrDie(Model.class) - - createWebClient().goTo("/self/"); - assert jenkins.getDescriptorOrDie(Model.class).touched - } - - String getName() { - return "Alice"; - } - - Model getModel() { - return new Model(); - } - - DescriptorImpl getDescriptor() { - return jenkins.getDescriptorOrDie(getClass()); - } - - @TestExtension - static class DescriptorImpl extends Descriptor {} - - static class Model extends AbstractDescribableImpl { - @TestExtension - static class DescriptorImpl extends Descriptor { - boolean touched; - - ListBoxModel doFillAbcItems(@RelativePath("..") @QueryParameter String name) { - assert name=="Alice"; - touched = true; - return new ListBoxModel().add("foo").add("bar") - } - } - } -} diff --git a/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy b/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy deleted file mode 100644 index eb1582ff59..0000000000 --- a/test/src/test/groovy/hudson/cli/BuildCommandTest.groovy +++ /dev/null @@ -1,271 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.cli - -import org.apache.commons.io.output.TeeOutputStream - -import static org.hamcrest.Matchers.*; -import static hudson.cli.CLICommandInvoker.Matcher.*; -import static org.junit.Assert.* -import hudson.Extension - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.CaptureEnvironmentBuilder -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestBuilder -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.StaplerRequest - -import hudson.Launcher -import hudson.cli.CLICommandInvoker; -import hudson.model.AbstractBuild -import hudson.model.Action -import hudson.model.BuildListener -import hudson.model.Executor -import hudson.model.FreeStyleProject; -import hudson.model.ParameterDefinition.ParameterDescriptor -import hudson.model.ParameterValue -import hudson.model.ParametersAction -import hudson.model.ParametersDefinitionProperty -import hudson.model.Queue.QueueDecisionHandler -import hudson.model.Queue.Task; -import hudson.model.SimpleParameterDefinition -import hudson.model.StringParameterDefinition -import hudson.model.StringParameterValue -import hudson.model.labels.LabelAtom -import hudson.tasks.Shell -import hudson.util.OneShotEvent - -import java.util.concurrent.Executor - -import net.sf.json.JSONObject - -/** - * {@link BuildCommand} test. - * - * @author Kohsuke Kawaguchi - */ -public class BuildCommandTest { - - @Rule public JenkinsRule j = new JenkinsRule(); - - /** - * Tests synchronous execution. - */ - @Test void sync() { - def p = j.createFreeStyleProject(); - p.buildersList.add(new Shell("sleep 3")); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s",p.name]) - assertFalse(p.getBuildByNumber(1).isBuilding()) - } finally { - cli.close(); - } - } - - /** - * Tests synchronous execution with retried verbose output - */ - @Test void syncWOutputStreaming() { - def p = j.createFreeStyleProject(); - p.buildersList.add(new Shell("sleep 3")); - - def cli =new CLI(j.URL) - try { - cli.execute(["build","-s","-v","-r","5",p.name]) - assertFalse(p.getBuildByNumber(1).isBuilding()) - } finally { - cli.close(); - } - } - - @Test void parameters() { - def p = j.createFreeStyleProject(); - p.addProperty(new ParametersDefinitionProperty([new StringParameterDefinition("key",null)])); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s","-p","key=foobar",p.name]) - def b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertEquals("foobar",b.getAction(ParametersAction.class).getParameter("key").value) - } finally { - cli.close(); - } - } - - @Test void defaultParameters() { - def p = j.createFreeStyleProject(); - p.addProperty(new ParametersDefinitionProperty([new StringParameterDefinition("key","default"), new StringParameterDefinition("key2","default2") ])); - - def cli = new CLI(j.URL) - try { - cli.execute(["build","-s","-p","key=foobar",p.name]) - def b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertEquals("foobar",b.getAction(ParametersAction.class).getParameter("key").value) - assertEquals("default2",b.getAction(ParametersAction.class).getParameter("key2").value) - } finally { - cli.close(); - } - } - - // TODO randomly fails: Started test0 #1 - @Test void consoleOutput() { - def p = j.createFreeStyleProject() - def cli = new CLI(j.URL) - try { - def o = new ByteArrayOutputStream() - cli.execute(["build","-s","-v",p.name],System.in,new TeeOutputStream(System.out,o),System.err) - j.assertBuildStatusSuccess(p.getBuildByNumber(1)) - assertTrue(o.toString(), o.toString().contains("Started from command line by anonymous")) - assertTrue(o.toString().contains("Finished: SUCCESS")) - } finally { - cli.close() - } - } - - // TODO randomly fails: Started test0 #1 - @Test void consoleOutputWhenBuildSchedulingRefused() { - def p = j.createFreeStyleProject() - def cli = new CLI(j.URL) - try { - def o = new ByteArrayOutputStream() - cli.execute(["build","-s","-v",p.name],System.in,System.out,new TeeOutputStream(System.err,o)) - assertTrue(o.toString(), o.toString().contains(BuildCommand.BUILD_SCHEDULING_REFUSED)) - } finally { - cli.close() - } - } - // <=> - @TestExtension("consoleOutputWhenBuildSchedulingRefused") - static class UnschedulingVetoer extends QueueDecisionHandler { - public boolean shouldSchedule(Task task, List actions) { - return false; - } - } - - @Test void refuseToBuildDisabledProject() { - - def project = j.createFreeStyleProject("the-project"); - project.disable(); - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("the-project"); - - assertThat(result, failedWith(4)); - assertThat(result.stderr(), containsString("ERROR: Cannot build the-project because it is disabled.")); - assertNull("Project should not be built", project.getBuildByNumber(1)); - } - - @Test void refuseToBuildNewlyCopiedProject() { - - def original = j.createFreeStyleProject("original"); - def newOne = (FreeStyleProject) j.jenkins.copy(original, "new-one"); - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("new-one"); - - assertThat(result, failedWith(4)); - assertThat(result.stderr(), containsString("ERROR: Cannot build new-one because its configuration has not been saved.")); - assertNull("Project should not be built", newOne.getBuildByNumber(1)); - } - - @Test void correctlyParseMapValuesContainingEqualsSign() { - - def project = j.createFreeStyleProject("the-project"); - project.addProperty(new ParametersDefinitionProperty([ - new StringParameterDefinition("expr", null) - ])); - - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("the-project", "-p", "expr=a=b", "-s"); - - assertThat(result, succeeded()); - assertEquals("a=b", project.getBuildByNumber(1).getBuildVariables().get("expr")); - } - - @Issue("JENKINS-15094") - @Test public void executorsAliveOnParameterWithNullDefaultValue() throws Exception { - def slave = j.createSlave(); - FreeStyleProject project = j.createFreeStyleProject("foo"); - project.setAssignedNode(slave); - - // Create test parameter with Null default value - def nullDefaultDefinition = new NullDefaultValueParameterDefinition(); - ParametersDefinitionProperty pdp = new ParametersDefinitionProperty( - new StringParameterDefinition("string", "defaultValue", "description"), - nullDefaultDefinition); - project.addProperty(pdp); - CaptureEnvironmentBuilder builder = new CaptureEnvironmentBuilder(); - project.getBuildersList().add(builder); - - // Warmup - j.buildAndAssertSuccess(project); - - for (def exec : slave.toComputer().getExecutors()) { - assertTrue("Executor has died before the test start: "+exec, exec.isActive()); - } - - // Create CLI & run command - def invoker = new CLICommandInvoker(j, new BuildCommand()); - def result = invoker.invokeWithArgs("foo","-p","string=value"); - assertThat(result, failedWith(2)); - assertThat(result.stderr(), containsString("ERROR: No default value for the parameter \'FOO\'.")); - - Thread.sleep(5000); // Give the job 5 seconds to be submitted - assertNull("Build should not be scheduled", j.jenkins.getQueue().getItem(project)); - assertNull("Build should not be scheduled", project.getBuildByNumber(2)); - - // Check executors health after a timeout - for (def exec : slave.toComputer().getExecutors()) { - assertTrue("Executor is dead: "+exec, exec.isActive()); - } - } - - public static final class NullDefaultValueParameterDefinition extends SimpleParameterDefinition { - - /*package*/ NullDefaultValueParameterDefinition() { - super("FOO", "Always null default value"); - } - - @Override - public ParameterValue createValue(String value) { - return new StringParameterValue("FOO", "BAR"); - } - - @Override - public ParameterValue createValue(StaplerRequest req, JSONObject jo) { - return createValue("BAR"); - } - - @Override - public ParameterValue getDefaultParameterValue() { - return null; // Equals to super.getDefaultParameterValue(); - } - - @Extension - public static class DescriptorImpl extends ParameterDescriptor {} - } -} diff --git a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy b/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy deleted file mode 100644 index dcfba02ed8..0000000000 --- a/test/src/test/groovy/hudson/cli/SetBuildParameterCommandTest.groovy +++ /dev/null @@ -1,82 +0,0 @@ -package hudson.cli - -import hudson.Functions -import hudson.Launcher -import hudson.model.AbstractBuild -import hudson.model.BuildListener -import hudson.model.ParametersAction -import hudson.model.ParametersDefinitionProperty -import hudson.model.ParameterDefinition -import hudson.model.Result -import hudson.model.StringParameterDefinition -import hudson.tasks.BatchFile -import hudson.tasks.Builder -import hudson.tasks.Shell -import jenkins.model.JenkinsLocationConfiguration -import org.junit.Assert -import org.junit.ClassRule -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.BuildWatcher -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestBuilder - -/** - * @author Kohsuke Kawaguchi - */ -public class SetBuildParameterCommandTest { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @ClassRule - public static BuildWatcher buildWatcher = new BuildWatcher(); - - @Test - public void cli() { - JenkinsLocationConfiguration.get().url = j.URL; - - def p = j.createFreeStyleProject(); - p.buildersList.add(new TestBuilder() { - @Override - boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - def jar = j.jenkins.servletContext.getResource("/WEB-INF/jenkins-cli.jar") - build.workspace.child("cli.jar").copyFrom(jar); - - return true; - } - }); - List pd = [new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")]; - p.addProperty(new ParametersDefinitionProperty(pd)) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x")) - p.buildersList.add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y")) - - def r = [:]; - - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - b.getAction(ParametersAction.class).parameters.each { v -> r[v.name]=v.value } - - assert r.equals(["a":"x", "b":"y"]); - - if (Functions.isWindows()) { - p.buildersList.add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - } else { - p.buildersList.add(new Shell("BUILD_NUMBER=1 java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")) - } - def b2 = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); - j.assertLogContains("#1 is not currently being built", b2) - r = [:]; - b.getAction(ParametersAction.class).parameters.each { v -> r[v.name]=v.value } - assert r.equals(["a":"x", "b":"y"]); - } - - //TODO: determine if this should be pulled out into JenkinsRule or something - /** - * Create a script based builder (either Shell or BatchFile) depending on platform - * @param script the contents of the script to run - * @return A Builder instance of either Shell or BatchFile - */ - private Builder createScriptBuilder(String script) { - return Functions.isWindows() ? new BatchFile(script) : new Shell(script); - } -} diff --git a/test/src/test/groovy/hudson/model/AbstractItemTest.groovy b/test/src/test/groovy/hudson/model/AbstractItemTest.groovy deleted file mode 100644 index 8c7cbe8ef4..0000000000 --- a/test/src/test/groovy/hudson/model/AbstractItemTest.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package hudson.model - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class AbstractItemTest { - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Tests the reload functionality - */ - @Test - void reload() { - def jenkins = j.jenkins; - def p = jenkins.createProject(FreeStyleProject.class,"foo"); - p.description = "Hello World"; - - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - b.description = "This is my build" - - // update on disk representation - def f = p.configFile.file - f.text = f.text.replaceAll("Hello World","Good Evening"); - - // reload away - p.doReload() - - assert p.description == "Good Evening"; - - def b2 = p.getBuildByNumber(1) - - assert !b.is(b2); // should be different object - assert b.description == b2.description // but should have the same properties - } -} diff --git a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy b/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy deleted file mode 100644 index a8941e0c66..0000000000 --- a/test/src/test/groovy/hudson/model/AbstractProjectTest.groovy +++ /dev/null @@ -1,596 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.model; - -import com.gargoylesoftware.htmlunit.ElementNotFoundException -import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; -import com.gargoylesoftware.htmlunit.HttpMethod -import com.gargoylesoftware.htmlunit.WebRequest; -import hudson.tasks.BatchFile; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Recorder; -import com.gargoylesoftware.htmlunit.html.HtmlPage -import hudson.maven.MavenModuleSet; -import hudson.security.*; -import hudson.tasks.Shell; -import hudson.scm.NullSCM; -import hudson.scm.SCM -import hudson.scm.SCMDescriptor -import hudson.Launcher; -import hudson.FilePath; -import hudson.Functions; -import hudson.Util; -import hudson.tasks.ArtifactArchiver -import hudson.triggers.SCMTrigger; -import hudson.triggers.TimerTrigger -import hudson.triggers.Trigger -import hudson.triggers.TriggerDescriptor; -import hudson.util.StreamTaskListener; -import hudson.util.OneShotEvent -import jenkins.model.Jenkins -import org.junit.Rule -import org.junit.Test; -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestExtension; -import org.jvnet.hudson.test.recipes.PresetData; -import org.jvnet.hudson.test.recipes.PresetData.DataSet -import org.apache.commons.io.FileUtils; -import org.junit.Assume; -import org.jvnet.hudson.test.MockFolder -import org.kohsuke.args4j.CmdLineException - -import static org.junit.Assert.fail -import static org.junit.Assert.assertEquals; - -/** - * @author Kohsuke Kawaguchi - */ -public class AbstractProjectTest { - - @Rule public JenkinsRule j = new JenkinsRule(); - - @Test - public void testConfigRoundtrip() { - def project = j.createFreeStyleProject(); - def l = j.jenkins.getLabel("foo && bar"); - project.assignedLabel = l; - j.configRoundtrip((Item) project); - - assert l == project.getAssignedLabel(); - } - - /** - * Tests the workspace deletion. - */ - @Test - public void testWipeWorkspace() { - def project = j.createFreeStyleProject(); - project.buildersList.add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); - - def b = project.scheduleBuild2(0).get(); - - assert b.workspace.exists(): "Workspace should exist by now"; - - project.doDoWipeOutWorkspace(); - - assert !b.workspace.exists(): "Workspace should be gone by now"; - } - - /** - * Makes sure that the workspace deletion is protected. - */ - @Test - @PresetData(DataSet.NO_ANONYMOUS_READACCESS) - public void testWipeWorkspaceProtected() { - def project = j.createFreeStyleProject(); - project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); - - def b = project.scheduleBuild2(0).get(); - - assert b.getWorkspace().exists(): "Workspace should exist by now"; - - // make sure that the action link is protected - com.gargoylesoftware.htmlunit.WebClient wc = j.createWebClient(); - try { - wc.getPage(new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST)); - fail("Expected HTTP status code 403") - } catch (FailingHttpStatusCodeException e) { - assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); - } - } - - /** - * Makes sure that the workspace deletion link is not provided - * when the user doesn't have an access. - */ - @Test - @PresetData(DataSet.ANONYMOUS_READONLY) - public void testWipeWorkspaceProtected2() { - ((GlobalMatrixAuthorizationStrategy) j.jenkins.getAuthorizationStrategy()).add(AbstractProject.WORKSPACE,"anonymous"); - - // make sure that the deletion is protected in the same way - testWipeWorkspaceProtected(); - - // there shouldn't be any "wipe out workspace" link for anonymous user - def webClient = j.createWebClient(); - HtmlPage page = webClient.getPage(j.jenkins.getItem("test0")); - - page = (HtmlPage)page.getAnchorByText("Workspace").click(); - try { - String wipeOutLabel = ResourceBundle.getBundle("hudson/model/AbstractProject/sidepanel").getString("Wipe Out Workspace"); - page.getAnchorByText(wipeOutLabel); - fail("shouldn't find a link"); - } catch (ElementNotFoundException e) { - // OK - } - } - - /** - * Tests the <optionalBlock @field> round trip behavior by using {@link AbstractProject#concurrentBuild} - */ - @Test - public void testOptionalBlockDataBindingRoundtrip() { - def p = j.createFreeStyleProject(); - [true,false].each { b -> - p.concurrentBuild = b; - j.submit(j.createWebClient().getPage(p,"configure").getFormByName("config")); - assert b==p.isConcurrentBuild(); - } - } - - /** - * Tests round trip configuration of the blockBuildWhenUpstreamBuilding field - */ - @Test - @Issue("JENKINS-4423") - public void testConfiguringBlockBuildWhenUpstreamBuildingRoundtrip() { - def p = j.createFreeStyleProject(); - p.blockBuildWhenUpstreamBuilding = false; - - def form = j.createWebClient().getPage(p, "configure").getFormByName("config"); - def input = form.getInputByName("blockBuildWhenUpstreamBuilding"); - assert !input.isChecked(): "blockBuildWhenUpstreamBuilding check box is checked."; - - input.setChecked(true); - j.submit(form); - assert p.blockBuildWhenUpstreamBuilding: "blockBuildWhenUpstreamBuilding was not updated from configuration form"; - - form = j.createWebClient().getPage(p, "configure").getFormByName("config"); - input = form.getInputByName("blockBuildWhenUpstreamBuilding"); - assert input.isChecked(): "blockBuildWhenUpstreamBuilding check box is not checked."; - } - - /** - * Unless the concurrent build option is enabled, polling and build should be mutually exclusive - * to avoid allocating unnecessary workspaces. - */ - @Test - @Issue("JENKINS-4202") - public void testPollingAndBuildExclusion() { - final OneShotEvent sync = new OneShotEvent(); - - final FreeStyleProject p = j.createFreeStyleProject(); - def b1 = j.buildAndAssertSuccess(p); - - p.scm = new NullSCM() { - @Override - public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) { - try { - sync.block(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return true; - } - - /** - * Don't write 'this', so that subtypes can be implemented as anonymous class. - */ - private Object writeReplace() { return new Object(); } - - @Override public boolean requiresWorkspaceForPolling() { - return true; - } - @Override public SCMDescriptor getDescriptor() { - return new SCMDescriptor(null) {}; - } - }; - Thread t = new Thread() { - @Override public void run() { - p.pollSCMChanges(StreamTaskListener.fromStdout()); - } - }; - try { - t.start(); - def f = p.scheduleBuild2(0); - - // add a bit of delay to make sure that the blockage is happening - Thread.sleep(3000); - - // release the polling - sync.signal(); - - def b2 = j.assertBuildStatusSuccess(f); - - // they should have used the same workspace. - assert b1.workspace == b2.workspace; - } finally { - t.interrupt(); - } - } - - @Test - @Issue("JENKINS-1986") - public void testBuildSymlinks() { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - def job = j.createFreeStyleProject(); - job.buildersList.add(new Shell("echo \"Build #\$BUILD_NUMBER\"\n")); - def build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - File lastSuccessful = new File(job.rootDir, "lastSuccessful"), - lastStable = new File(job.rootDir, "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - FreeStyleBuild build2 = job.scheduleBuild2(0, new Cause.UserCause()).get(); - // Another build updates links - assertSymlinkForBuild(lastSuccessful, 2); - assertSymlinkForBuild(lastStable, 2); - // Delete latest build should update links - build2.delete(); - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Delete all builds should remove links - build.delete(); - assert !lastSuccessful.exists(): "lastSuccessful link should be removed"; - assert !lastStable.exists(): "lastStable link should be removed"; - } - - private static void assertSymlinkForBuild(File file, int buildNumber) - throws IOException, InterruptedException { - assert file.exists(): "should exist and point to something that exists"; - assert Util.isSymlink(file): "should be symlink"; - String s = FileUtils.readFileToString(new File(file, "log")); - assert s.contains("Build #" + buildNumber + "\n") : "link should point to build #$buildNumber, but link was: ${Util.resolveSymlink(file, TaskListener.NULL)}\nand log was:\n$s"; - } - - @Test - @Issue("JENKINS-2543") - public void testSymlinkForPostBuildFailure() { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - // Links should be updated after post-build actions when final build result is known - def job = j.createFreeStyleProject(); - job.buildersList.add(new Shell("echo \"Build #\$BUILD_NUMBER\"\n")); - def build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.SUCCESS == build.result; - File lastSuccessful = new File(job.rootDir, "lastSuccessful"), - lastStable = new File(job.rootDir, "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Archive artifacts that don't exist to create failure in post-build action - job.publishersList.add(new ArtifactArchiver("*.foo", "", false, false)); - build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.FAILURE == build.getResult(); - // Links should not be updated since build failed - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - } - - /* TODO too slow, seems capable of causing testWorkspaceLock to time out: - @Test - @Issue("JENKINS-15156") - public void testGetBuildAfterGC() { - FreeStyleProject job = j.createFreeStyleProject(); - job.scheduleBuild2(0, new Cause.UserIdCause()).get(); - j.jenkins.queue.clearLeftItems(); - MemoryAssert.assertGC(new WeakReference(job.getLastBuild())); - assert job.lastBuild != null; - } - */ - - @Test - @Issue("JENKINS-17137") - public void testExternalBuildDirectorySymlinks() { - Assume.assumeFalse(Functions.isWindows()); // symlinks may not be available - def form = j.createWebClient().goTo("configure").getFormByName("config"); - def builds = j.createTmpDir(); - form.getInputByName("_.rawBuildsDir").valueAttribute = builds.toString() + "/\${ITEM_FULL_NAME}"; - j.submit(form); - assert builds.toString() + "/\${ITEM_FULL_NAME}" == j.jenkins.getRawBuildsDir(); - def p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "p"); - def b1 = p.scheduleBuild2(0).get(); - def link = new File(p.rootDir, "lastStable"); - assert link.exists(); - assert b1.rootDir.absolutePath == resolveAll(link).absolutePath; - def b2 = p.scheduleBuild2(0).get(); - assert link.exists(); - assert b2.rootDir.absolutePath == resolveAll(link).absolutePath; - b2.delete(); - assert link.exists(); - assert b1.rootDir.absolutePath == resolveAll(link).absolutePath; - b1.delete(); - assert !link.exists(); - } - - private File resolveAll(File link) throws InterruptedException, IOException { - while (true) { - File f = Util.resolveSymlinkToFile(link); - if (f==null) return link; - link = f; - } - } - - @Test - @Issue("JENKINS-17138") - public void testExternalBuildDirectoryRenameDelete() { - def form = j.createWebClient().goTo("configure").getFormByName("config"); - def builds = j.createTmpDir(); - form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/\${ITEM_FULL_NAME}"); - j.submit(form); - assert builds.toString() + "/\${ITEM_FULL_NAME}" == j.jenkins.rawBuildsDir; - def p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "prj"); - def b = p.scheduleBuild2(0).get(); - def oldBuildDir = new File(builds, "d/prj"); - assert new File(oldBuildDir, b.id) == b.rootDir; - assert b.getRootDir().isDirectory(); - p.renameTo("proj"); - def newBuildDir = new File(builds, "d/proj"); - assert new File(newBuildDir, b.id) == b.rootDir; - assert b.rootDir.isDirectory(); - p.delete(); - assert !b.rootDir.isDirectory(); - } - - @Test - @Issue("JENKINS-18678") - public void testRenameJobLostBuilds() throws Exception { - def p = j.createFreeStyleProject("initial"); - j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - assertEquals(1, p.getBuilds().size()); - p.renameTo("edited"); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - def d = j.jenkins.createProject(MockFolder.class, "d"); - Items.move(p, d); - assertEquals(p, j.jenkins.getItemByFullName("d/edited")); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - d.renameTo("d2"); - p = j.jenkins.getItemByFullName("d2/edited"); - p._getRuns().purgeCache(); - assertEquals(1, p.getBuilds().size()); - } - - @Test - @Issue("JENKINS-17575") - public void testDeleteRedirect() { - j.createFreeStyleProject("j1"); - assert "" == deleteRedirectTarget("job/j1"); - j.createFreeStyleProject("j2"); - Jenkins.getInstance().addView(new AllView("v1")); - assert "view/v1/" == deleteRedirectTarget("view/v1/job/j2"); - MockFolder d = Jenkins.getInstance().createProject(MockFolder.class, "d"); - d.addView(new AllView("v2")); - ["j3","j4","j5"].each { n -> d.createProject(FreeStyleProject.class, n) } - assert "job/d/" == deleteRedirectTarget("job/d/job/j3"); - assert "job/d/view/v2/" == deleteRedirectTarget("job/d/view/v2/job/j4"); - assert "view/v1/job/d/" == deleteRedirectTarget("view/v1/job/d/job/j5"); - assert "view/v1/" == deleteRedirectTarget("view/v1/job/d"); // JENKINS-23375 - } - - private String deleteRedirectTarget(String job) { - def wc = j.createWebClient(); - String base = wc.getContextPath(); - String loc = wc.getPage(wc.addCrumb(new WebRequest(new URL(base + job + "/doDelete"), HttpMethod.POST))).getUrl().toString(); - assert loc.startsWith(base): loc; - return loc.substring(base.length()); - } - - @Test - @Issue("JENKINS-18407") - public void testQueueSuccessBehavior() { - // prevent any builds to test the behaviour - j.jenkins.numExecutors = 0; - - def p = j.createFreeStyleProject() - def f = p.scheduleBuild2(0) - assert f!=null; - def g = p.scheduleBuild2(0) - assert f==g; - - p.makeDisabled(true) - assert p.scheduleBuild2(0)==null - } - - /** - * Do the same as {@link #testQueueSuccessBehavior()} but over HTTP - */ - @Test - @Issue("JENKINS-18407") - public void testQueueSuccessBehaviorOverHTTP() { - // prevent any builds to test the behaviour - j.jenkins.numExecutors = 0; - - def p = j.createFreeStyleProject() - def wc = j.createWebClient(); - - def rsp = wc.getPage("${j.getURL()}${p.url}build").webResponse - assert rsp.statusCode==201; - assert rsp.getResponseHeaderValue("Location")!=null; - - def rsp2 = wc.getPage("${j.getURL()}${p.url}build").webResponse - assert rsp2.statusCode==201; - assert rsp.getResponseHeaderValue("Location")==rsp2.getResponseHeaderValue("Location") - - p.makeDisabled(true) - - try { - wc.getPage("${j.getURL()}${p.url}build") - fail(); - } catch (FailingHttpStatusCodeException e) { - // request should fail - } - } - - /** - * We used to store {@link AbstractProject#triggers} as {@link Vector}, so make sure - * we can still read back the configuration from that. - */ - @Test - public void testVectorTriggers() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - assert p.triggers().size()==1 - def t = p.triggers()[0] - assert t.class==SCMTrigger.class; - assert t.spec=="*/10 * * * *" - } - - @Test - @Issue("JENKINS-18813") - public void testRemoveTrigger() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - TriggerDescriptor SCM_TRIGGER_DESCRIPTOR = j.jenkins.getDescriptorOrDie(SCMTrigger.class) - p.removeTrigger(SCM_TRIGGER_DESCRIPTOR); - assert p.triggers().size()==0 - } - - @Test - @Issue("JENKINS-18813") - public void testAddTriggerSameType() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - def newTrigger = new SCMTrigger("H/5 * * * *") - p.addTrigger(newTrigger); - - assert p.triggers().size()==1 - def t = p.triggers()[0] - assert t.class==SCMTrigger.class; - assert t.spec=="H/5 * * * *" - } - - @Test - @Issue("JENKINS-18813") - public void testAddTriggerDifferentType() { - AbstractProject p = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")) - - def newTrigger = new TimerTrigger("20 * * * *") - p.addTrigger(newTrigger); - - assert p.triggers().size()==2 - def t = p.triggers()[1] - assert t == newTrigger - } - - /** - * Trying to POST to config.xml by a different job type should fail. - */ - @Test - public void testConfigDotXmlSubmissionToDifferentType() { - j.jenkins.pluginManager.installDetachedPlugin("javadoc") - j.jenkins.pluginManager.installDetachedPlugin("junit") - j.jenkins.pluginManager.installDetachedPlugin("display-url-api") - j.jenkins.pluginManager.installDetachedPlugin("mailer") - j.jenkins.pluginManager.installDetachedPlugin("maven-plugin") - - j.jenkins.crumbIssuer = null - def p = j.createFreeStyleProject() - - HttpURLConnection con = postConfigDotXml(p, "") - - // this should fail with a type mismatch error - // the error message should report both what was submitted and what was expected - assert con.responseCode == 500 - def msg = con.errorStream.text - println msg - assert msg.contains(FreeStyleProject.class.name) - assert msg.contains(MavenModuleSet.class.name) - - // control. this should work - con = postConfigDotXml(p, "") - assert con.responseCode == 200 - } - - private HttpURLConnection postConfigDotXml(FreeStyleProject p, String xml) { - HttpURLConnection con = new URL(j.getURL(), "job/${p.name}/config.xml").openConnection() - con.requestMethod = "POST" - con.setRequestProperty("Content-Type", "application/xml") - con.doOutput = true - con.outputStream.withStream { s -> - s.write(xml.bytes) - } - return con - } - - @Test - @Issue("JENKINS-27549") - public void testLoadingWithNPEOnTriggerStart() { - AbstractProject project = j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/npeTrigger.xml")) - - assert project.triggers().size() == 1 - } - - @Test - @Issue("JENKINS-30742") - public void testResolveForCLI() { - try { - AbstractProject not_found = AbstractProject.resolveForCLI("never_created"); - fail("Exception should occur before!"); - } catch (CmdLineException e) { - assert e.getMessage().contentEquals("No such job \u2018never_created\u2019 exists."); - } - - AbstractProject project = j.jenkins.createProject(FreeStyleProject.class, "never_created"); - try { - AbstractProject not_found = AbstractProject.resolveForCLI("never_created1"); - fail("Exception should occur before!"); - } catch (CmdLineException e) { - assert e.getMessage().contentEquals("No such job \u2018never_created1\u2019 exists. Perhaps you meant \u2018never_created\u2019?") - } - - } - - static class MockBuildTriggerThrowsNPEOnStart extends Trigger { - @Override - public void start(hudson.model.Item project, boolean newInstance) { throw new NullPointerException(); } - - @Override - public TriggerDescriptor getDescriptor() { - return DESCRIPTOR; - } - - public static final TriggerDescriptor DESCRIPTOR = new DescriptorImpl() - - @TestExtension("testLoadingWithNPEOnTriggerStart") - static class DescriptorImpl extends TriggerDescriptor { - - public boolean isApplicable(hudson.model.Item item) { - return false; - } - } - } -} diff --git a/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy b/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy deleted file mode 100644 index fab4c865f6..0000000000 --- a/test/src/test/groovy/hudson/model/EnvironmentContributorTest.groovy +++ /dev/null @@ -1,41 +0,0 @@ -package hudson.model - -import hudson.EnvVars -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.CaptureEnvironmentBuilder -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class EnvironmentContributorTest { - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Makes sure that the project-scoped environment variables are getting consulted. - */ - @Test - public void testProjectScoped() { - def p = j.createFreeStyleProject() - def c = new CaptureEnvironmentBuilder() - p.buildersList.add(c) - p.description = "Issac Newton"; - j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - assert c.envVars["ABC"]=="Issac Newton"; - assert c.envVars["NODE_NAME"]=="master"; - } - - @TestExtension("testProjectScoped") - public static class JobScopedInjection extends EnvironmentContributor { - @Override - void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) { - envs.put("ABC",j.description) - } - } -} diff --git a/test/src/test/groovy/hudson/model/SlaveTest.groovy b/test/src/test/groovy/hudson/model/SlaveTest.groovy deleted file mode 100644 index 35299a7e4f..0000000000 --- a/test/src/test/groovy/hudson/model/SlaveTest.groovy +++ /dev/null @@ -1,104 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.model - -import static hudson.util.FormValidation.Kind.ERROR -import static hudson.util.FormValidation.Kind.WARNING -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertEquals - -import hudson.security.csrf.CrumbIssuer -import hudson.slaves.DumbSlave -import hudson.slaves.JNLPLauncher -import hudson.util.FormValidation -import org.junit.Rule -import org.junit.Test -import org.apache.commons.io.IOUtils -import org.jvnet.hudson.test.GroovyJenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class SlaveTest { - - @Rule - public GroovyJenkinsRule j = new GroovyJenkinsRule() - - /** - * Makes sure that a form validation method gets inherited. - */ - @Test - void formValidation() { - j.executeOnServer { - assertNotNull(j.jenkins.getDescriptor(DumbSlave).getCheckUrl("remoteFS")) - } - } - - /** - * Programmatic config.xml submission. - */ - @Test - void slaveConfigDotXml() { - DumbSlave s = j.createSlave() - def wc = j.createWebClient() - def p = wc.goTo("computer/${s.name}/config.xml", "application/xml") - def xml = p.webResponse.contentAsString - new XmlSlurper().parseText(xml) // verify that it is XML - - // make sure it survives the roundtrip - post("computer/${s.name}/config.xml",xml); - - assertNotNull(j.jenkins.getNode(s.name)) - - xml = IOUtils.toString(getClass().getResource("SlaveTest/slave.xml").openStream()); - xml = xml.replace("NAME",s.name) - post("computer/${s.name}/config.xml",xml); - - s = j.jenkins.getNode(s.name) - assertNotNull(s) - assertEquals("some text",s.nodeDescription) - assertEquals(JNLPLauncher.class,s.launcher.class) - } - - def post(url,String xml) { - HttpURLConnection con = new URL(j.getURL(),url).openConnection(); - con.requestMethod = "POST" - con.setRequestProperty("Content-Type","application/xml;charset=UTF-8") - con.setRequestProperty(CrumbIssuer.DEFAULT_CRUMB_NAME,"test") - con.doOutput = true; - con.outputStream.write(xml.getBytes("UTF-8")) - con.outputStream.close(); - IOUtils.copy(con.inputStream,System.out) - } - - @Test - void remoteFsCheck() { - def d = j.jenkins.getDescriptorByType(DumbSlave.DescriptorImpl.class) - assert d.doCheckRemoteFS("c:\\")==FormValidation.ok(); - assert d.doCheckRemoteFS("/tmp")==FormValidation.ok(); - assert d.doCheckRemoteFS("relative/path").kind==WARNING; - assert d.doCheckRemoteFS("/net/foo/bar/zot").kind==WARNING; - assert d.doCheckRemoteFS("\\\\machine\\folder\\foo").kind==WARNING; - } -} diff --git a/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy b/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy deleted file mode 100644 index d0c6ae2880..0000000000 --- a/test/src/test/groovy/hudson/security/TokenBasedRememberMeServices2Test.groovy +++ /dev/null @@ -1,129 +0,0 @@ -package hudson.security - -import jenkins.model.Jenkins -import org.acegisecurity.AuthenticationException -import org.acegisecurity.BadCredentialsException -import org.acegisecurity.GrantedAuthority -import org.acegisecurity.GrantedAuthorityImpl -import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices -import org.acegisecurity.userdetails.User -import org.acegisecurity.userdetails.UserDetails -import org.acegisecurity.userdetails.UsernameNotFoundException -import org.junit.Before -import com.gargoylesoftware.htmlunit.util.Cookie -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.LoggerRule -import org.springframework.dao.DataAccessException - -import java.util.logging.Handler -import java.util.logging.LogRecord -import java.util.logging.Logger - -import static java.util.logging.Level.FINEST - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TokenBasedRememberMeServices2Test { - @Rule - public JenkinsRule j = new JenkinsRule(); - @Rule - public LoggerRule logging = new LoggerRule() - - private static boolean failureInduced; - - @Before - public void resetFailureInduced() {failureInduced = false} - - @Test - public void rememberMeAutoLoginFailure() { - j.jenkins.securityRealm = new InvalidUserWhenLoggingBackInRealm() - - def wc = j.createWebClient() - wc.login("alice","alice",true) - - // we should see a remember me cookie - def c = getRememberMeCookie(wc) - assert c!=null - - // start a new session and attempt to access Jenkins, - // which should cause autoLogin failures - wc = j.createWebClient() - wc.cookieManager.addCookie(c); - - // even if SecurityRealm chokes, it shouldn't kill the page - logging.capture(1000).record(TokenBasedRememberMeServices.class, FINEST) - wc.goTo("") - - // make sure that the server recorded this failure - assert failureInduced - assert logging.messages.find { it.contains("contained username 'alice' but was not found")}!=null - // and the problematic cookie should have been removed - assert getRememberMeCookie(wc)==null - } - - private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) { - wc.cookieManager.getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY) - } - - private static class InvalidUserWhenLoggingBackInRealm extends AbstractPasswordBasedSecurityRealm { - @Override - protected UserDetails authenticate(String username, String password) throws AuthenticationException { - if (username==password) - return new User(username,password,true,[new GrantedAuthorityImpl("myteam")] as GrantedAuthority[]) - throw new BadCredentialsException(username); - } - - @Override - GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { - throw new UnsupportedOperationException() - } - - @Override - UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { - failureInduced = true - throw new UsernameNotFoundException("intentionally not working"); - } - } - - - @Test - public void basicFlow() { - j.jenkins.securityRealm = new StupidRealm() - - def wc = j.createWebClient() - wc.login("bob","bob",true) - - // we should see a remember me cookie - def c = getRememberMeCookie(wc) - assert c!=null - - // start a new session and attempt to access Jenkins, - wc = j.createWebClient() - wc.cookieManager.addCookie(c); - - // this will trigger remember me - wc.goTo("") - - // make sure that our security realm failed to report the info correctly - assert failureInduced - // but we should have logged in - wc.executeOnServer { - def a = Jenkins.getAuthentication() - assert a.name=="bob" - assert a.authorities*.authority.join(":")=="authenticated:myteam" - } - } - - private static class StupidRealm extends InvalidUserWhenLoggingBackInRealm { - @Override - UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { - failureInduced = true - throw new UserMayOrMayNotExistException("I cannot tell"); - } - } -} diff --git a/test/src/test/groovy/hudson/util/TextFileTest.groovy b/test/src/test/groovy/hudson/util/TextFileTest.groovy deleted file mode 100644 index 0299132fc8..0000000000 --- a/test/src/test/groovy/hudson/util/TextFileTest.groovy +++ /dev/null @@ -1,102 +0,0 @@ -package hudson.util - -import org.junit.After -import org.junit.Test - -import java.nio.charset.Charset - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TextFileTest { - List files = []; - - @After - void tearDown() { - files*.delete() - } - - @Test - public void head() { - def f = newFile() - f.text = getClass().getResource("ascii.txt").text - - def t = new TextFile(f) - def first35 = "Lorem ipsum dolor sit amet, consect" - assert t.head(35).equals(first35) - assert first35.length()==35 - } - - @Test - public void shortHead() { - def f = newFile() - f.text = "hello" - - def t = new TextFile(f) - assert t.head(35).equals("hello") - } - - @Test - public void tail() { - def f = newFile() - f.text = getClass().getResource("ascii.txt").text - - def t = new TextFile(f) - def tailStr = "la, vitae interdum quam rutrum id." + System.lineSeparator() - assert t.fastTail(tailStr.length()).equals(tailStr) - assert tailStr.length()==(34 + System.lineSeparator().length()) - } - - @Test - public void shortTail() { - def f = newFile() - f.text = "hello" - - def t = new TextFile(f) - assert t.fastTail(35).equals("hello") - } - - /** - * Shift JIS is a multi-byte character encoding. - * - * In it, 0x82 0x83 is \u30e2, and 0x83 0x82 is \uFF43. - * So if aren't careful, we'll parse the text incorrectly. - */ - @Test - public void tailShiftJIS() { - def f = newFile() - - def t = new TextFile(f) - - f.withOutputStream { o -> - (1..80).each { - (1..40).each { - o.write(0x83) - o.write(0x82) - } - o.write(0x0A); - } - } - - def tail = t.fastTail(35, Charset.forName("Shift_JIS")) - assert tail.equals("\u30e2"*34+"\n") - assert tail.length()==35 - - // add one more byte to force fastTail to read from one byte ahead - // between this and the previous case, it should start parsing text incorrectly, until it hits NL - // where it comes back in sync - f.append([0x0A] as byte[]) - - tail = t.fastTail(35, Charset.forName("Shift_JIS")) - assert tail.equals("\u30e2"*33+"\n\n") - assert tail.length()==35 - } - - def newFile() { - def f = File.createTempFile("foo", "txt") - files.add(f) - return f - } -} diff --git a/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy b/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy deleted file mode 100644 index 9d32acfa6b..0000000000 --- a/test/src/test/groovy/hudson/widgets/HistoryWidgetTest.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package hudson.widgets - -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class HistoryWidgetTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - @Test - @Issue("JENKINS-15499") - void moreLink() { - def p = j.createFreeStyleProject() - for (x in 1..3) { - j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - } - - def wc = j.createWebClient() - wc.javaScriptEnabled = false - wc.goTo("job/${p.name}/buildHistory/all") - } -} diff --git a/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy b/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy deleted file mode 100644 index 352b3144f0..0000000000 --- a/test/src/test/groovy/jenkins/bugs/Jenkins19124Test.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package jenkins.bugs - -import com.gargoylesoftware.htmlunit.WebClientUtil -import hudson.Launcher -import hudson.model.AbstractBuild -import hudson.model.AbstractProject -import hudson.model.BuildListener -import hudson.tasks.BuildStepDescriptor -import hudson.tasks.Builder -import hudson.util.FormValidation -import hudson.util.ListBoxModel -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.QueryParameter - -import javax.inject.Inject - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class Jenkins19124Test { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @Inject - DescriptorImpl d; - - @Issue("JENKINS-19124") - @Test - public void interrelatedFormValidation() { - j.jenkins.injector.injectMembers(this); - - def p = j.createFreeStyleProject(); - p.buildersList.add(new Foo()); - - def wc = j.createWebClient(); - def c = wc.getPage(p, "configure"); - c.getElementByName("_.alpha").valueAttribute = "hello"; - WebClientUtil.waitForJSExec(wc); - assert d.alpha=="hello"; - assert d.bravo=="2"; - - c.getElementByName("_.bravo").setSelectedAttribute("1",true); - WebClientUtil.waitForJSExec(wc); - assert d.alpha=="hello"; - assert d.bravo=="1"; - } - - public static class Foo extends Builder { - String getAlpha() { return "alpha"; } - String getBravo() { return "2"; } - - @Override - boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - return true; - } - - private Object writeReplace() { return new Object(); } - } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - String alpha,bravo; - - public DescriptorImpl() { - super(Foo.class) - } - - FormValidation doCheckAlpha(@QueryParameter String value, @QueryParameter String bravo) { - this.alpha = value; - this.bravo = bravo; - return FormValidation.ok(); - } - - ListBoxModel doFillBravoItems() { - return new ListBoxModel().add("1").add("2").add("3"); - } - - @Override - boolean isApplicable(Class jobType) { - return true; - } - } -} diff --git a/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy b/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy deleted file mode 100644 index a500677a9f..0000000000 --- a/test/src/test/groovy/jenkins/model/PeepholePermalinkTest.groovy +++ /dev/null @@ -1,100 +0,0 @@ -package jenkins.model - -import static org.junit.Assert.assertTrue - -import org.junit.Assume; - -import hudson.Functions -import hudson.Util -import hudson.model.Run -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.FailureBuilder -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * @author Kohsuke Kawaguchi - */ -class PeepholePermalinkTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - /** - * Basic operation of the permalink generation. - */ - @Test - void basics() { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - - def p = j.createFreeStyleProject() - def b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - def lsb = new File(p.buildDir, "lastSuccessfulBuild") - def lfb = new File(p.buildDir, "lastFailedBuild") - - assertLink(lsb,b1) - - // now another build that fails - p.buildersList.add(new FailureBuilder()) - def b2 = p.scheduleBuild2(0).get() - - assertLink(lsb,b1) - assertLink(lfb,b2) - - // one more build and this time it succeeds - p.buildersList.clear() - def b3 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - assertLink(lsb,b3) - assertLink(lfb,b2) - - // delete b3 and symlinks should update properly - b3.delete() - assertLink(lsb,b1) - assertLink(lfb,b2) - - b1.delete() - assertLink(lsb,null) - assertLink(lfb,b2) - - b2.delete() - assertLink(lsb,null) - assertLink(lfb,null) - } - - def assertLink(File symlink, Run build) { - assert Util.resolveSymlink(symlink)==(build==null ? "-1" : build.number as String); - } - - /** - * job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we used to generate should still work - */ - @Test - void legacyCompatibility() { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - - def p = j.createFreeStyleProject() - def b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - ["lastStable","lastSuccessful"].each { n -> - // test if they both point to b1 - assert new File(p.rootDir,"$n/build.xml").length() == new File(b1.rootDir,"build.xml").length() - } - } - - @Test - @Issue("JENKINS-19034") - void rebuildBuildNumberPermalinks() { - def p = j.createFreeStyleProject() - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - File f = new File(p.getBuildDir(), "1") - // assertTrue(Util.isSymlink(f)) - f.delete() - PeepholePermalink link = p.getPermalinks().find({l -> l instanceof PeepholePermalink}) - println(link) - link.updateCache(p, b) - assertTrue("build symlink hasn't been restored", f.exists()) - } -} diff --git a/test/src/test/groovy/lib/form/ApplyButtonTest.groovy b/test/src/test/groovy/lib/form/ApplyButtonTest.groovy deleted file mode 100644 index ceae5ed940..0000000000 --- a/test/src/test/groovy/lib/form/ApplyButtonTest.groovy +++ /dev/null @@ -1,37 +0,0 @@ -package lib.form - -import hudson.markup.RawHtmlMarkupFormatter -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class ApplyButtonTest { - @Rule public JenkinsRule j = new JenkinsRule(); - - /** - * Editing code mirror should still gets reflected when you click apply. - */ - @Test @Issue("JENKINS-18436") - public void editDescription() { - j.jenkins.markupFormatter = RawHtmlMarkupFormatter.INSTANCE // need something using CodeMirror - def p = j.createFreeStyleProject() - def b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)) - - def config = j.createWebClient().getPage(b, "configure") - def form = config.getFormByName("config") - // HtmlUnit doesn't have JSON, so we need to emulate one - config.executeJavaScript(getClass().getResource("JSON.js").text) - // it's hard to emulate the keytyping, so we just set the value into codemirror and test if this gets - // reflected back into TEXTAREA - config.executeJavaScript("document.getElementsByTagName('TEXTAREA')[0].codemirrorObject.setLine(0,'foobar')") - j.getButtonByCaption(form,"Apply").click() - - assert "foobar"==b.description - } -} diff --git a/test/src/test/groovy/lib/form/TextAreaTest.groovy b/test/src/test/groovy/lib/form/TextAreaTest.groovy deleted file mode 100644 index f5edc5cac3..0000000000 --- a/test/src/test/groovy/lib/form/TextAreaTest.groovy +++ /dev/null @@ -1,128 +0,0 @@ -package lib.form - -import hudson.model.AbstractProject -import hudson.tasks.BuildStepDescriptor -import hudson.tasks.Builder -import hudson.util.FormValidation -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.Issue -import org.jvnet.hudson.test.JenkinsRule -import org.jvnet.hudson.test.TestExtension -import org.kohsuke.stapler.DataBoundConstructor -import org.kohsuke.stapler.QueryParameter - -import javax.inject.Inject - -import static org.junit.Assert.* - -/** - * - * - * @author Kohsuke Kawaguchi - */ -class TextAreaTest { - @Rule - public JenkinsRule j = new JenkinsRule(); - - @Inject - TestBuilder.DescriptorImpl d; - - @Test @Issue("JENKINS-19457") - public void validation() { - j.jenkins.injector.injectMembers(this) - def p = j.createFreeStyleProject() - p.buildersList.add(new TestBuilder()) - j.configRoundtrip(p) - assert d.text1=="This is text1" - assert d.text2=="Received This is text1" - } - - public static class TestBuilder extends Builder { - @DataBoundConstructor - TestBuilder() { - } - - public String getText1() { return "This is text1" } - public String getText2() { return "This is text2" } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - def text1,text2; - - @Override - boolean isApplicable(Class jobType) { - return true; - } - - FormValidation doCheckText1(@QueryParameter String value) { - this.text1 = value; - return FormValidation.ok(); - } - - FormValidation doCheckText2(@QueryParameter String text1) { - this.text2 = "Received "+text1; - return FormValidation.ok(); - } - } - - } - - @Issue("JENKINS-27505") - @Test - public void testText() { - T1:{ - def TEXT_TO_TEST = "some\nvalue\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - - // test for a textarea beginning with a empty line. - T2:{ - def TEXT_TO_TEST = "\nbegin\n\nwith\nempty\nline\n\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - - // test for a textarea beginning with two empty lines. - T3:{ - def TEXT_TO_TEST = "\n\nbegin\n\nwith\ntwo\nempty\nline\n\n"; - def p = j.createFreeStyleProject(); - def target = new TextareaTestBuilder(TEXT_TO_TEST); - p.buildersList.add(target); - j.configRoundtrip(p); - j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); - } - } - - public static class TextareaTestBuilder extends Builder { - private text; - - @DataBoundConstructor - TextareaTestBuilder(String text) { - this.text = text; - } - - public String getText() { return text; } - - @TestExtension - public static class DescriptorImpl extends BuildStepDescriptor { - @Override - boolean isApplicable(Class jobType) { - return true; - } - - @Override - String getDisplayName() { - return this.class.name; - } - } - - } -} diff --git a/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy b/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy deleted file mode 100644 index a50e3e8fea..0000000000 --- a/test/src/test/groovy/org/jvnet/hudson/test/SleepBuilderTest.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package org.jvnet.hudson.test - -import org.junit.Rule -import org.junit.Test - -/** - * @author schristou@cloudbees.com - * Date: 9/17/13 - * Time: 11:43 AM - * To change this template use File | Settings | File Templates. - */ -class SleepBuilderTest { - @Rule public JenkinsRule j = new JenkinsRule(); - @Test - void testPerform() { - def project = j.createFreeStyleProject(); - def builder = new SleepBuilder(30) - project.buildersList.add(builder); - j.configRoundtrip(project); - j.assertEqualDataBoundBeans(project.buildersList.get(SleepBuilder.class), builder); - } -} diff --git a/test/src/test/java/hudson/PluginManagerUtil.java b/test/src/test/java/hudson/PluginManagerUtil.java index 2263622b4c..423213b7d6 100644 --- a/test/src/test/java/hudson/PluginManagerUtil.java +++ b/test/src/test/java/hudson/PluginManagerUtil.java @@ -26,6 +26,8 @@ package hudson; import jenkins.RestartRequiredException; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; +import org.junit.runner.Description; +import org.jvnet.hudson.test.RestartableJenkinsRule; import org.jvnet.hudson.test.JenkinsRule; import java.io.File; @@ -47,6 +49,15 @@ public class PluginManagerUtil { }; } + public static RestartableJenkinsRule newRestartableJenkinsRule() { + return new RestartableJenkinsRule() { + @Override + public JenkinsRule createJenkinsRule(Description description) { + return newJenkinsRule(); + } + }; + } + public static void dynamicLoad(String plugin, Jenkins jenkins) throws IOException, InterruptedException, RestartRequiredException { dynamicLoad(plugin, jenkins, false); } diff --git a/test/src/test/java/hudson/RelativePathTest.java b/test/src/test/java/hudson/RelativePathTest.java new file mode 100644 index 0000000000..494a37f9e9 --- /dev/null +++ b/test/src/test/java/hudson/RelativePathTest.java @@ -0,0 +1,58 @@ +package hudson; + +import hudson.model.AbstractDescribableImpl; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.util.ListBoxModel; +import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.QueryParameter; + +public class RelativePathTest extends HudsonTestCase implements Describable { + + @Issue("JENKINS-18776") + public void testRelativePath() throws Exception { + // I was having trouble causing annotation processing on test stubs +// jenkins.getDescriptorOrDie(RelativePathTest.class); +// jenkins.getDescriptorOrDie(Model.class); + + createWebClient().goTo("/self/"); + assertTrue(((Model.DescriptorImpl) jenkins.getDescriptorOrDie(Model.class)).touched); + } + + @Override // TODO this is horrible, should change the property here and in @QueryParameter and in RelativePathTest/index.groovy to, say, personName + public String getName() { + return "Alice"; + } + + public Model getModel() { + return new Model(); + } + + @Override // TODO would suffice to extend AbstractDescribableImpl + public DescriptorImpl getDescriptor() { + return (DescriptorImpl) jenkins.getDescriptorOrDie(getClass()); + } + + @TestExtension + public static class DescriptorImpl extends Descriptor {} + + public static class Model extends AbstractDescribableImpl { + + @TestExtension + public static class DescriptorImpl extends Descriptor { + + boolean touched; + + public ListBoxModel doFillAbcItems(@RelativePath("..") @QueryParameter String name) { + assertEquals("Alice", name); + touched = true; + return new ListBoxModel().add("foo").add("bar"); + } + + } + + } + +} diff --git a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java index 8c970e8cc9..906adcf6e7 100644 --- a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java +++ b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java @@ -31,6 +31,7 @@ import hudson.Proc; import hudson.cli.util.ScriptLoader; import hudson.model.Node.Mode; import hudson.model.Slave; +import hudson.model.User; import hudson.remoting.Channel; import hudson.slaves.JNLPLauncher; import hudson.slaves.RetentionStrategy; @@ -86,7 +87,7 @@ public class JnlpAccessWithSecuredHudsonTest { public void anonymousCanAlwaysLoadJARs() throws Exception { r.jenkins.setNodes(Collections.singletonList(createNewJnlpSlave("test"))); JenkinsRule.WebClient wc = r.createWebClient(); - HtmlPage p = wc.login("alice").goTo("computer/test/"); + HtmlPage p = wc.withBasicApiToken(User.getById("alice", true)).goTo("computer/test/"); // this fresh WebClient doesn't have a login cookie and represent JNLP launcher JenkinsRule.WebClient jnlpAgent = r.createWebClient(); diff --git a/test/src/test/java/hudson/cli/BuildCommand2Test.java b/test/src/test/java/hudson/cli/BuildCommand2Test.java deleted file mode 100644 index 6b6a03743e..0000000000 --- a/test/src/test/java/hudson/cli/BuildCommand2Test.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * The MIT License - * - * Copyright 2017 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package hudson.cli; - -import hudson.Launcher; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; -import hudson.model.FileParameterDefinition; -import hudson.model.FreeStyleBuild; -import hudson.model.FreeStyleProject; -import hudson.model.ParametersDefinitionProperty; -import hudson.util.OneShotEvent; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import org.junit.ClassRule; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.jvnet.hudson.test.BuildWatcher; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestBuilder; - -public class BuildCommand2Test { - - @ClassRule - public static BuildWatcher buildWatcher = new BuildWatcher(); - - @Rule - public JenkinsRule r = new JenkinsRule(); - - @Issue("JENKINS-41745") - @Test - public void fileParameter() throws Exception { - FreeStyleProject p = r.createFreeStyleProject("myjob"); - p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("file", null))); - p.getBuildersList().add(new TestBuilder() { - @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - listener.getLogger().println("Found in my workspace: " + build.getWorkspace().child("file").readToString()); - return true; - } - }); - assertThat(new CLICommandInvoker(r, "build"). - withStdin(new ByteArrayInputStream("uploaded content here".getBytes())). - invokeWithArgs("-f", "-p", "file=", "myjob"), - CLICommandInvoker.Matcher.succeeded()); - FreeStyleBuild b = p.getBuildByNumber(1); - assertNotNull(b); - r.assertLogContains("uploaded content here", b); - } - - /** - * Just schedules a build and return. - */ - @Test - public void async() throws Exception { - FreeStyleProject p = r.createFreeStyleProject(); - OneShotEvent started = new OneShotEvent(); - OneShotEvent completed = new OneShotEvent(); - p.getBuildersList().add(new TestBuilder() { - @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - started.signal(); - completed.block(); - return true; - } - }); - - // this should be asynchronous - CLI cli = new CLI(r.getURL()); - try { - assertEquals(0, cli.execute("build", p.getName())); - started.block(); - assertTrue(p.getBuildByNumber(1).isBuilding()); - completed.signal(); - } finally { - cli.close(); - } - } - -} diff --git a/test/src/test/java/hudson/cli/BuildCommandTest.java b/test/src/test/java/hudson/cli/BuildCommandTest.java new file mode 100644 index 0000000000..a55d7c1fe9 --- /dev/null +++ b/test/src/test/java/hudson/cli/BuildCommandTest.java @@ -0,0 +1,313 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.cli; + +import hudson.Extension; +import hudson.Launcher; +import static hudson.cli.CLICommandInvoker.Matcher.*; +import hudson.model.AbstractBuild; +import hudson.model.Action; +import hudson.model.BuildListener; +import hudson.model.Executor; +import hudson.model.FileParameterDefinition; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParameterDefinition.ParameterDescriptor; +import hudson.model.ParameterValue; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.Queue.QueueDecisionHandler; +import hudson.model.Queue.Task; +import hudson.model.SimpleParameterDefinition; +import hudson.model.StringParameterDefinition; +import hudson.model.StringParameterValue; +import hudson.model.TopLevelItem; +import hudson.slaves.DumbSlave; +import hudson.tasks.Shell; +import hudson.util.OneShotEvent; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import net.sf.json.JSONObject; +import org.apache.commons.io.output.TeeOutputStream; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.StaplerRequest; + +/** + * {@link BuildCommand} test. + */ +public class BuildCommandTest { + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Just schedules a build and return. + */ + @Test + public void async() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + OneShotEvent started = new OneShotEvent(); + OneShotEvent completed = new OneShotEvent(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + started.signal(); + completed.block(); + return true; + } + }); + + // this should be asynchronous + try (CLI cli = new CLI(j.getURL())) { + assertEquals(0, cli.execute("build", p.getName())); + started.block(); + assertTrue(p.getBuildByNumber(1).isBuilding()); + completed.signal(); + } + } + + /** + * Tests synchronous execution. + */ + @Test + public void sync() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Shell("sleep 3")); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", p.getName()); + assertFalse(p.getBuildByNumber(1).isBuilding()); + } + } + + /** + * Tests synchronous execution with retried verbose output + */ + @Test + public void syncWOutputStreaming() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Shell("sleep 3")); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-v", "-r", "5", p.getName()); + assertFalse(p.getBuildByNumber(1).isBuilding()); + } + } + + @Test + public void parameters() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", null))); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-p", "key=foobar", p.getName()); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertEquals("foobar", b.getAction(ParametersAction.class).getParameter("key").getValue()); + } + } + + @Test + public void defaultParameters() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", "default"), new StringParameterDefinition("key2", "default2"))); + + try (CLI cli = new CLI(j.getURL())) { + cli.execute("build", "-s", "-p", "key=foobar", p.getName()); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertEquals("foobar", b.getAction(ParametersAction.class).getParameter("key").getValue()); + assertEquals("default2", b.getAction(ParametersAction.class).getParameter("key2").getValue()); + } + } + + // TODO randomly fails: Started test0 #1 + @Test + public void consoleOutput() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + try (CLI cli = new CLI(j.getURL())) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + cli.execute(Arrays.asList("build", "-s", "-v", p.getName()), System.in, new TeeOutputStream(System.out, o), System.err); + j.assertBuildStatusSuccess(p.getBuildByNumber(1)); + assertThat(o.toString(), allOf(containsString("Started from command line by anonymous"), containsString("Finished: SUCCESS"))); + } + } + + // TODO randomly fails: Started test0 #1 + @Test + public void consoleOutputWhenBuildSchedulingRefused() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + try (CLI cli = new CLI(j.getURL())) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + cli.execute(Arrays.asList("build", "-s", "-v", p.getName()), System.in, System.out, new TeeOutputStream(System.err, o)); + assertThat(o.toString(), containsString(BuildCommand.BUILD_SCHEDULING_REFUSED)); + } + } + // <=> + @TestExtension("consoleOutputWhenBuildSchedulingRefused") + public static class UnschedulingVetoer extends QueueDecisionHandler { + @Override + public boolean shouldSchedule(Task task, List actions) { + return false; + } + } + + @Test + public void refuseToBuildDisabledProject() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("the-project"); + project.disable(); + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("the-project"); + + assertThat(result, failedWith(4)); + assertThat(result.stderr(), containsString("ERROR: Cannot build the-project because it is disabled.")); + assertNull("Project should not be built", project.getBuildByNumber(1)); + } + + @Test + public void refuseToBuildNewlyCopiedProject() throws Exception { + FreeStyleProject original = j.createFreeStyleProject("original"); + FreeStyleProject newOne = (FreeStyleProject) j.jenkins.copy(original, "new-one"); + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("new-one"); + + assertThat(result, failedWith(4)); + assertThat(result.stderr(), containsString("ERROR: Cannot build new-one because its configuration has not been saved.")); + assertNull("Project should not be built", newOne.getBuildByNumber(1)); + } + + @Test + public void correctlyParseMapValuesContainingEqualsSign() throws Exception { + FreeStyleProject project = j.createFreeStyleProject("the-project"); + project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("expr", null))); + + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("the-project", "-p", "expr=a=b", "-s"); + + assertThat(result, succeeded()); + assertEquals("a=b", project.getBuildByNumber(1).getBuildVariables().get("expr")); + } + + @Issue("JENKINS-15094") + @Test + public void executorsAliveOnParameterWithNullDefaultValue() throws Exception { + DumbSlave slave = j.createSlave(); + FreeStyleProject project = j.createFreeStyleProject("foo"); + project.setAssignedNode(slave); + + // Create test parameter with Null default value + NullDefaultValueParameterDefinition nullDefaultDefinition = new NullDefaultValueParameterDefinition(); + ParametersDefinitionProperty pdp = new ParametersDefinitionProperty( + new StringParameterDefinition("string", "defaultValue", "description"), + nullDefaultDefinition); + project.addProperty(pdp); + CaptureEnvironmentBuilder builder = new CaptureEnvironmentBuilder(); + project.getBuildersList().add(builder); + + // Warmup + j.buildAndAssertSuccess(project); + + for (Executor exec : slave.toComputer().getExecutors()) { + assertTrue("Executor has died before the test start: " + exec, exec.isActive()); + } + + // Create CLI & run command + CLICommandInvoker invoker = new CLICommandInvoker(j, new BuildCommand()); + CLICommandInvoker.Result result = invoker.invokeWithArgs("foo", "-p", "string=value"); + assertThat(result, failedWith(2)); + assertThat(result.stderr(), containsString("ERROR: No default value for the parameter \'FOO\'.")); + + Thread.sleep(5000); // Give the job 5 seconds to be submitted + assertNull("Build should not be scheduled", j.jenkins.getQueue().getItem(project)); + assertNull("Build should not be scheduled", project.getBuildByNumber(2)); + + // Check executors health after a timeout + for (Executor exec : slave.toComputer().getExecutors()) { + assertTrue("Executor is dead: " + exec, exec.isActive()); + } + } + + public static final class NullDefaultValueParameterDefinition extends SimpleParameterDefinition { + + /*package*/ NullDefaultValueParameterDefinition() { + super("FOO", "Always null default value"); + } + + @Override + public ParameterValue createValue(String value) { + return new StringParameterValue("FOO", "BAR"); + } + + @Override + public ParameterValue createValue(StaplerRequest req, JSONObject jo) { + return createValue("BAR"); + } + + @Override + public ParameterValue getDefaultParameterValue() { + return null; // Equals to super.getDefaultParameterValue(); + } + + @Extension + public static class DescriptorImpl extends ParameterDescriptor {} + + } + + @Issue("JENKINS-41745") + @Test + public void fileParameter() throws Exception { + FreeStyleProject p = j.createFreeStyleProject("myjob"); + p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("file", null))); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + listener.getLogger().println("Found in my workspace: " + build.getWorkspace().child("file").readToString()); + return true; + } + }); + assertThat(new CLICommandInvoker(j, "build"). + withStdin(new ByteArrayInputStream("uploaded content here".getBytes())). + invokeWithArgs("-f", "-p", "file=", "myjob"), + CLICommandInvoker.Matcher.succeeded()); + FreeStyleBuild b = p.getBuildByNumber(1); + assertNotNull(b); + j.assertLogContains("uploaded content here", b); + } + +} diff --git a/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java b/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java new file mode 100644 index 0000000000..937abe4628 --- /dev/null +++ b/test/src/test/java/hudson/cli/SetBuildParameterCommandTest.java @@ -0,0 +1,88 @@ +package hudson.cli; + +import com.google.common.collect.ImmutableMap; +import hudson.Functions; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.ParameterDefinition; +import hudson.model.Result; +import hudson.model.StringParameterDefinition; +import hudson.tasks.BatchFile; +import hudson.tasks.Builder; +import hudson.tasks.Shell; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestBuilder; + +public class SetBuildParameterCommandTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @ClassRule + public static BuildWatcher buildWatcher = new BuildWatcher(); + + @Test + public void cli() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + URL jar = j.jenkins.servletContext.getResource("/WEB-INF/jenkins-cli.jar"); + build.getWorkspace().child("cli.jar").copyFrom(jar); + + return true; + } + }); + List pd = Arrays.asList(new StringParameterDefinition("a", ""), new StringParameterDefinition("b", "")); + p.addProperty(new ParametersDefinitionProperty(pd)); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter a x")); + p.getBuildersList().add(createScriptBuilder("java -jar cli.jar -remoting -noKeyAuth set-build-parameter b y")); + + Map r = new TreeMap<>(); + + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + b.getAction(ParametersAction.class).getParameters().forEach(v -> r.put(v.getName(), v.getValue())); + + assertEquals(ImmutableMap.of("a", "x", "b", "y"), r); + + if (Functions.isWindows()) { + p.getBuildersList().add(new BatchFile("set BUILD_NUMBER=1\r\njava -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + } else { + p.getBuildersList().add(new Shell("BUILD_NUMBER=1 java -jar cli.jar -remoting -noKeyAuth set-build-parameter a b")); + } + FreeStyleBuild b2 = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + j.assertLogContains("#1 is not currently being built", b2); + r.clear(); + b.getAction(ParametersAction.class).getParameters().forEach(v -> r.put(v.getName(), v.getValue())); + assertEquals(ImmutableMap.of("a", "x", "b", "y"), r); + } + + //TODO: determine if this should be pulled out into JenkinsRule or something + /** + * Create a script based builder (either Shell or BatchFile) depending on + * platform + * @param script the contents of the script to run + * @return A Builder instance of either Shell or BatchFile + */ + private Builder createScriptBuilder(String script) { + return Functions.isWindows() ? new BatchFile(script) : new Shell(script); + } + +} diff --git a/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java b/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java index d416b3e451..3b3da3b177 100644 --- a/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java +++ b/test/src/test/java/hudson/diagnosis/HudsonHomeDiskUsageMonitorTest.java @@ -11,10 +11,7 @@ import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.util.NameValuePair; import hudson.model.User; import hudson.security.GlobalMatrixAuthorizationStrategy; -import hudson.security.HudsonPrivateSecurityRealm; -import hudson.security.Permission; import jenkins.model.Jenkins; -import org.acegisecurity.context.SecurityContextHolder; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -25,6 +22,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.ElementNotFoundException; import java.io.IOException; +import java.net.URL; import java.util.Collections; /** @@ -73,28 +71,33 @@ public class HudsonHomeDiskUsageMonitorTest { auth.add(Jenkins.READ, "users"); j.jenkins.setAuthorizationStrategy(auth); - WebRequest request = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST); + User bob = User.getById("bob", true); + User administrator = User.getById("administrator", true); + + WebRequest request = new WebRequest(new URL(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST); NameValuePair param = new NameValuePair("no", "true"); request.setRequestParameters(Collections.singletonList(param)); HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get(); + wc.withBasicApiToken(bob); try { - wc.login("bob"); wc.getPage(request); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(403, e.getStatusCode()); } assertTrue(mon.isEnabled()); + WebRequest requestReadOnly = new WebRequest(new URL(wc.getContextPath() + "administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET); try { - WebRequest getIndex = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET); - wc.getPage(getIndex); + wc.getPage(requestReadOnly); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(403, e.getStatusCode()); } - wc.login("administrator"); + wc.withBasicApiToken(administrator); wc.getPage(request); assertFalse(mon.isEnabled()); diff --git a/test/src/test/java/hudson/model/AbstractItemTest.java b/test/src/test/java/hudson/model/AbstractItemTest.java new file mode 100644 index 0000000000..267f8f3524 --- /dev/null +++ b/test/src/test/java/hudson/model/AbstractItemTest.java @@ -0,0 +1,43 @@ +package hudson.model; + +import java.io.File; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class AbstractItemTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Tests the reload functionality + */ + @Test + public void reload() throws Exception { + Jenkins jenkins = j.jenkins; + FreeStyleProject p = jenkins.createProject(FreeStyleProject.class, "foo"); + p.setDescription("Hello World"); + + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + b.setDescription("This is my build"); + + // update on disk representation + File f = p.getConfigFile().getFile(); + FileUtils.writeStringToFile(f, FileUtils.readFileToString(f).replaceAll("Hello World", "Good Evening")); + + // reload away + p.doReload(); + + assertEquals("Good Evening", p.getDescription()); + + FreeStyleBuild b2 = p.getBuildByNumber(1); + + assertNotEquals(b, b2); // should be different object + assertEquals(b.getDescription(), b2.getDescription()); // but should have the same properties + } + +} diff --git a/test/src/test/java/hudson/model/AbstractProject2Test.java b/test/src/test/java/hudson/model/AbstractProject2Test.java deleted file mode 100644 index a5f330ca43..0000000000 --- a/test/src/test/java/hudson/model/AbstractProject2Test.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * The MIT License - * - * Copyright 2017 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package hudson.model; - -import hudson.tasks.BuildTrigger; -import java.util.Collections; -import jenkins.model.Jenkins; -import static org.hamcrest.Matchers.*; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.MockAuthorizationStrategy; - -// TODO merge with AbstractProjectTest when converted from Groovy to Java -public class AbstractProject2Test { - - @Rule - public JenkinsRule r = new JenkinsRule(); - - @Issue("SECURITY-617") - @Test - public void upstreamDownstreamExportApi() throws Exception { - FreeStyleProject us = r.createFreeStyleProject("upstream-project"); - FreeStyleProject ds = r.createFreeStyleProject("downstream-project"); - us.getPublishersList().add(new BuildTrigger(Collections.singleton(ds), Result.SUCCESS)); - r.jenkins.rebuildDependencyGraph(); - assertEquals(Collections.singletonList(ds), us.getDownstreamProjects()); - assertEquals(Collections.singletonList(us), ds.getUpstreamProjects()); - r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); - r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). - grant(Jenkins.READ).everywhere().toEveryone(). - grant(Item.READ).everywhere().to("alice"). - grant(Item.READ).onItems(us).to("bob"). - grant(Item.READ).onItems(ds).to("charlie")); - String api = r.createWebClient().login("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, containsString("downstream-project")); - api = r.createWebClient().login("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, containsString("upstream-project")); - api = r.createWebClient().login("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, not(containsString("downstream-project"))); - api = r.createWebClient().login("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); - System.out.println(api); - assertThat(api, not(containsString("upstream-project"))); - } - -} diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java new file mode 100644 index 0000000000..8ba1982b11 --- /dev/null +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -0,0 +1,646 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model; + +import com.gargoylesoftware.htmlunit.ElementNotFoundException; +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.FilePath; +import hudson.Functions; +import hudson.Launcher; +import hudson.Util; +import hudson.maven.MavenModuleSet; +import hudson.scm.NullSCM; +import hudson.scm.SCM; +import hudson.scm.SCMDescriptor; +import hudson.security.GlobalMatrixAuthorizationStrategy; +import hudson.tasks.ArtifactArchiver; +import hudson.tasks.BatchFile; +import hudson.tasks.BuildTrigger; +import hudson.tasks.Shell; +import hudson.triggers.SCMTrigger; +import hudson.triggers.TimerTrigger; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import hudson.util.OneShotEvent; +import hudson.util.StreamTaskListener; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.ResourceBundle; +import java.util.Vector; +import java.util.concurrent.Future; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.MockFolder; +import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.TestPluginManager; +import org.jvnet.hudson.test.recipes.PresetData; +import org.jvnet.hudson.test.recipes.PresetData.DataSet; +import org.kohsuke.args4j.CmdLineException; + +public class AbstractProjectTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void configRoundtrip() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + Label l = j.jenkins.getLabel("foo && bar"); + project.setAssignedLabel(l); + j.configRoundtrip(project); + + assertEquals(l, project.getAssignedLabel()); + } + + /** + * Tests the workspace deletion. + */ + @Test + public void wipeWorkspace() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); + + FreeStyleBuild b = project.scheduleBuild2(0).get(); + + assertTrue("Workspace should exist by now", b.getWorkspace().exists()); + + project.doDoWipeOutWorkspace(); + + assertFalse("Workspace should be gone by now", b.getWorkspace().exists()); + } + + /** + * Makes sure that the workspace deletion is protected. + */ + @Test + @PresetData(DataSet.NO_ANONYMOUS_READACCESS) + public void wipeWorkspaceProtected() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + project.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello")); + + FreeStyleBuild b = project.scheduleBuild2(0).get(); + + assert b.getWorkspace().exists() : "Workspace should exist by now"; + + // make sure that the action link is protected + JenkinsRule.WebClient wc = j.createWebClient(); + try { + wc.getPage(new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST)); + fail("Expected HTTP status code 403"); + } catch (FailingHttpStatusCodeException e) { + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); + } + } + + /** + * Makes sure that the workspace deletion link is not provided when the user + * doesn't have an access. + */ + @Test + @PresetData(DataSet.ANONYMOUS_READONLY) + public void wipeWorkspaceProtected2() throws Exception { + ((GlobalMatrixAuthorizationStrategy) j.jenkins.getAuthorizationStrategy()).add(AbstractProject.WORKSPACE, "anonymous"); + + // make sure that the deletion is protected in the same way + wipeWorkspaceProtected(); + + // there shouldn't be any "wipe out workspace" link for anonymous user + JenkinsRule.WebClient webClient = j.createWebClient(); + HtmlPage page = webClient.getPage(j.jenkins.getItem("test0")); + + page = (HtmlPage) page.getAnchorByText("Workspace").click(); + try { + String wipeOutLabel = ResourceBundle.getBundle("hudson/model/AbstractProject/sidepanel").getString("Wipe Out Workspace"); + page.getAnchorByText(wipeOutLabel); + fail("shouldn't find a link"); + } catch (ElementNotFoundException e) { + // OK + } + } + + /** + * Tests the <optionalBlock @field> round trip behavior by using + * {@link AbstractProject#concurrentBuild} + */ + @Test + public void optionalBlockDataBindingRoundtrip() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + for (boolean b : new boolean[] {true, false}) { + p.setConcurrentBuild(b); + j.submit(j.createWebClient().getPage(p, "configure").getFormByName("config")); + assertEquals(b, p.isConcurrentBuild()); + } + } + + /** + * Tests round trip configuration of the blockBuildWhenUpstreamBuilding + * field + */ + @Test + @Issue("JENKINS-4423") + public void configuringBlockBuildWhenUpstreamBuildingRoundtrip() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.setBlockBuildWhenUpstreamBuilding(false); + + HtmlForm form = j.createWebClient().getPage(p, "configure").getFormByName("config"); + HtmlInput input = form.getInputByName("blockBuildWhenUpstreamBuilding"); + assertFalse("blockBuildWhenUpstreamBuilding check box is checked.", input.isChecked()); + + input.setChecked(true); + j.submit(form); + assertTrue("blockBuildWhenUpstreamBuilding was not updated from configuration form", p.blockBuildWhenUpstreamBuilding()); + + form = j.createWebClient().getPage(p, "configure").getFormByName("config"); + input = form.getInputByName("blockBuildWhenUpstreamBuilding"); + assert input.isChecked() : "blockBuildWhenUpstreamBuilding check box is not checked."; + } + + /** + * Unless the concurrent build option is enabled, polling and build should + * be mutually exclusive to avoid allocating unnecessary workspaces. + */ + @Test + @Issue("JENKINS-4202") + public void pollingAndBuildExclusion() throws Exception { + final OneShotEvent sync = new OneShotEvent(); + + final FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.buildAndAssertSuccess(p); + + p.setScm(new NullSCM() { + @Override + public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) { + try { + sync.block(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + } + + /** + * Don't write 'this', so that subtypes can be implemented as + * anonymous class. + */ + private Object writeReplace() { + return new Object(); + } + + @Override + public boolean requiresWorkspaceForPolling() { + return true; + } + @Override + public SCMDescriptor getDescriptor() { + return new SCMDescriptor(null) { + }; + } + }); + Thread t = new Thread() { + @Override + public void run() { + p.pollSCMChanges(StreamTaskListener.fromStdout()); + } + }; + try { + t.start(); + Future f = p.scheduleBuild2(0); + + // add a bit of delay to make sure that the blockage is happening + Thread.sleep(3000); + + // release the polling + sync.signal(); + + FreeStyleBuild b2 = j.assertBuildStatusSuccess(f); + + // they should have used the same workspace. + assertEquals(b1.getWorkspace(), b2.getWorkspace()); + } finally { + t.interrupt(); + } + } + + @Test + @Issue("JENKINS-1986") + public void buildSymlinks() throws Exception { + Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); + + FreeStyleProject job = j.createFreeStyleProject(); + job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); + FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), + lastStable = new File(job.getRootDir(), "lastStable"); + // First build creates links + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + FreeStyleBuild build2 = job.scheduleBuild2(0, new Cause.UserCause()).get(); + // Another build updates links + assertSymlinkForBuild(lastSuccessful, 2); + assertSymlinkForBuild(lastStable, 2); + // Delete latest build should update links + build2.delete(); + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + // Delete all builds should remove links + build.delete(); + assertFalse("lastSuccessful link should be removed", lastSuccessful.exists()); + assertFalse("lastStable link should be removed", lastStable.exists()); + } + + private static void assertSymlinkForBuild(File file, int buildNumber) + throws IOException, InterruptedException { + assert file.exists() : "should exist and point to something that exists"; + assert Util.isSymlink(file) : "should be symlink"; + String s = FileUtils.readFileToString(new File(file, "log")); + assert s.contains("Build #" + buildNumber + "\n") : "link should point to build #$buildNumber, but link was: ${Util.resolveSymlink(file, TaskListener.NULL)}\nand log was:\n$s"; + } + + @Test + @Issue("JENKINS-2543") + public void symlinkForPostBuildFailure() throws Exception { + Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); + + // Links should be updated after post-build actions when final build result is known + FreeStyleProject job = j.createFreeStyleProject(); + job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); + FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + assert Result.SUCCESS == build.getResult(); + File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), + lastStable = new File(job.getRootDir(), "lastStable"); + // First build creates links + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + // Archive artifacts that don't exist to create failure in post-build action + job.getPublishersList().add(new ArtifactArchiver("*.foo", "", false, false)); + build = job.scheduleBuild2(0, new Cause.UserCause()).get(); + assert Result.FAILURE == build.getResult(); + // Links should not be updated since build failed + assertSymlinkForBuild(lastSuccessful, 1); + assertSymlinkForBuild(lastStable, 1); + } + + /* TODO too slow, seems capable of causing testWorkspaceLock to time out: + @Test + @Issue("JENKINS-15156") + public void testGetBuildAfterGC() { + FreeStyleProject job = j.createFreeStyleProject(); + job.scheduleBuild2(0, new Cause.UserIdCause()).get(); + j.jenkins.queue.clearLeftItems(); + MemoryAssert.assertGC(new WeakReference(job.getLastBuild())); + assert job.lastBuild != null; + } + */ + @Test + @Issue("JENKINS-17137") + public void externalBuildDirectorySymlinks() throws Exception { + Assume.assumeFalse(Functions.isWindows()); // symlinks may not be available + HtmlForm form = j.createWebClient().goTo("configure").getFormByName("config"); + File builds = j.createTmpDir(); + form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/${ITEM_FULL_NAME}"); + j.submit(form); + assertEquals(builds.toString() + "/${ITEM_FULL_NAME}", j.jenkins.getRawBuildsDir()); + FreeStyleProject p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "p"); + FreeStyleBuild b1 = p.scheduleBuild2(0).get(); + File link = new File(p.getRootDir(), "lastStable"); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); + FreeStyleBuild b2 = p.scheduleBuild2(0).get(); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b2.getRootDir().getAbsolutePath()); + b2.delete(); + assertTrue(link.exists()); + assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); + b1.delete(); + assertFalse(link.exists()); + } + + private File resolveAll(File link) throws InterruptedException, IOException { + while (true) { + File f = Util.resolveSymlinkToFile(link); + if (f == null) { + return link; + } + link = f; + } + } + + @Test + @Issue("JENKINS-17138") + public void externalBuildDirectoryRenameDelete() throws Exception { + HtmlForm form = j.createWebClient().goTo("configure").getFormByName("config"); + File builds = j.createTmpDir(); + form.getInputByName("_.rawBuildsDir").setValueAttribute(builds.toString() + "/${ITEM_FULL_NAME}"); + j.submit(form); + assertEquals(builds.toString() + "/${ITEM_FULL_NAME}", j.jenkins.getRawBuildsDir()); + FreeStyleProject p = j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "prj"); + FreeStyleBuild b = p.scheduleBuild2(0).get(); + File oldBuildDir = new File(builds, "d/prj"); + assertEquals(new File(oldBuildDir, b.getId()), b.getRootDir()); + assertTrue(b.getRootDir().isDirectory()); + p.renameTo("proj"); + File newBuildDir = new File(builds, "d/proj"); + assertEquals(new File(newBuildDir, b.getId()), b.getRootDir()); + assertTrue(b.getRootDir().isDirectory()); + p.delete(); + assertFalse(b.getRootDir().isDirectory()); + } + + @Test + @Issue("JENKINS-18678") + public void renameJobLostBuilds() throws Exception { + FreeStyleProject p = j.createFreeStyleProject("initial"); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + assertEquals(1, p.getBuilds().size()); + p.renameTo("edited"); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + MockFolder d = j.jenkins.createProject(MockFolder.class, "d"); + Items.move(p, d); + assertEquals(p, j.jenkins.getItemByFullName("d/edited")); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + d.renameTo("d2"); + p = j.jenkins.getItemByFullName("d2/edited", FreeStyleProject.class); + p._getRuns().purgeCache(); + assertEquals(1, p.getBuilds().size()); + } + + @Test + @Issue("JENKINS-17575") + public void deleteRedirect() throws Exception { + j.createFreeStyleProject("j1"); + assertEquals("", deleteRedirectTarget("job/j1")); + j.createFreeStyleProject("j2"); + Jenkins.getInstance().addView(new AllView("v1")); + assertEquals("view/v1/", deleteRedirectTarget("view/v1/job/j2")); + MockFolder d = Jenkins.getInstance().createProject(MockFolder.class, "d"); + d.addView(new AllView("v2")); + for (String n : new String[] {"j3", "j4", "j5"}) { + d.createProject(FreeStyleProject.class, n); + } + assertEquals("job/d/", deleteRedirectTarget("job/d/job/j3")); + assertEquals("job/d/view/v2/", deleteRedirectTarget("job/d/view/v2/job/j4")); + assertEquals("view/v1/job/d/", deleteRedirectTarget("view/v1/job/d/job/j5")); + assertEquals("view/v1/", deleteRedirectTarget("view/v1/job/d")); // JENKINS-23375 + } + + private String deleteRedirectTarget(String job) throws Exception { + JenkinsRule.WebClient wc = j.createWebClient(); + String base = wc.getContextPath(); + String loc = wc.getPage(wc.addCrumb(new WebRequest(new URL(base + job + "/doDelete"), HttpMethod.POST))).getUrl().toString(); + assert loc.startsWith(base) : loc; + return loc.substring(base.length()); + } + + @Test + @Issue("JENKINS-18407") + public void queueSuccessBehavior() throws Exception { + // prevent any builds to test the behaviour + j.jenkins.setNumExecutors(0); + + FreeStyleProject p = j.createFreeStyleProject(); + Future f = p.scheduleBuild2(0); + assertNotNull(f); + Future g = p.scheduleBuild2(0); + assertEquals(f, g); + + p.makeDisabled(true); + assertNull(p.scheduleBuild2(0)); + } + + /** + * Do the same as {@link #testQueueSuccessBehavior()} but over HTTP + */ + @Test + @Issue("JENKINS-18407") + public void queueSuccessBehaviorOverHTTP() throws Exception { + // prevent any builds to test the behaviour + j.jenkins.setNumExecutors(0); + + FreeStyleProject p = j.createFreeStyleProject(); + JenkinsRule.WebClient wc = j.createWebClient(); + + WebResponse rsp = wc.getPage(j.getURL() + p.getUrl() + "build").getWebResponse(); + assertEquals(201, rsp.getStatusCode()); + assertNotNull(rsp.getResponseHeaderValue("Location")); + + WebResponse rsp2 = wc.getPage(j.getURL() + p.getUrl() + "build").getWebResponse(); + assertEquals(201, rsp2.getStatusCode()); + assertEquals(rsp.getResponseHeaderValue("Location"), rsp2.getResponseHeaderValue("Location")); + + p.makeDisabled(true); + + try { + wc.getPage(j.getURL() + p.getUrl() + "build"); + fail(); + } catch (FailingHttpStatusCodeException e) { + // request should fail + } + } + + /** + * We used to store {@link AbstractProject#triggers} as {@link Vector}, so + * make sure we can still read back the configuration from that. + */ + @Test + public void vectorTriggers() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + assertEquals(1, p.triggers().size()); + Trigger t = p.triggers().get(0); + assertEquals(SCMTrigger.class, t.getClass()); + assertEquals("*/10 * * * *", t.getSpec()); + } + + @Test + @Issue("JENKINS-18813") + public void removeTrigger() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + TriggerDescriptor SCM_TRIGGER_DESCRIPTOR = (TriggerDescriptor) j.jenkins.getDescriptorOrDie(SCMTrigger.class); + p.removeTrigger(SCM_TRIGGER_DESCRIPTOR); + assertEquals(0, p.triggers().size()); + } + + @Test + @Issue("JENKINS-18813") + public void addTriggerSameType() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + SCMTrigger newTrigger = new SCMTrigger("H/5 * * * *"); + p.addTrigger(newTrigger); + + assertEquals(1, p.triggers().size()); + Trigger t = p.triggers().get(0); + assertEquals(SCMTrigger.class, t.getClass()); + assertEquals("H/5 * * * *", t.getSpec()); + } + + @Test + @Issue("JENKINS-18813") + public void addTriggerDifferentType() throws Exception { + AbstractProject p = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/vectorTriggers.xml")); + + TimerTrigger newTrigger = new TimerTrigger("20 * * * *"); + p.addTrigger(newTrigger); + + assertEquals(2, p.triggers().size()); + Trigger t = p.triggers().get(1); + assertEquals(newTrigger, t); + } + + /** + * Trying to POST to config.xml by a different job type should fail. + */ + @Test + public void configDotXmlSubmissionToDifferentType() throws Exception { + TestPluginManager tpm = (TestPluginManager) j.jenkins.pluginManager; + tpm.installDetachedPlugin("javadoc"); + tpm.installDetachedPlugin("junit"); + tpm.installDetachedPlugin("display-url-api"); + tpm.installDetachedPlugin("mailer"); + tpm.installDetachedPlugin("maven-plugin"); + + j.jenkins.setCrumbIssuer(null); + FreeStyleProject p = j.createFreeStyleProject(); + + HttpURLConnection con = postConfigDotXml(p, ""); + + // this should fail with a type mismatch error + // the error message should report both what was submitted and what was expected + assertEquals(500, con.getResponseCode()); + String msg = IOUtils.toString(con.getErrorStream()); + System.out.println(msg); + assertThat(msg, allOf(containsString(FreeStyleProject.class.getName()), containsString(MavenModuleSet.class.getName()))); + + // control. this should work + con = postConfigDotXml(p, ""); + assertEquals(200, con.getResponseCode()); + } + + private HttpURLConnection postConfigDotXml(FreeStyleProject p, String xml) throws Exception { + HttpURLConnection con = (HttpURLConnection) new URL(j.getURL(), "job/" + p.getName() + "/config.xml").openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/xml"); + con.setDoOutput(true); + try (OutputStream s = con.getOutputStream()) { + s.write(xml.getBytes()); + } + return con; + } + + @Test + @Issue("JENKINS-27549") + public void loadingWithNPEOnTriggerStart() throws Exception { + AbstractProject project = (AbstractProject) j.jenkins.createProjectFromXML("foo", getClass().getResourceAsStream("AbstractProjectTest/npeTrigger.xml")); + + assertEquals(1, project.triggers().size()); + } + + @Test + @Issue("JENKINS-30742") + public void resolveForCLI() throws Exception { + try { + AbstractProject not_found = AbstractProject.resolveForCLI("never_created"); + fail("Exception should occur before!"); + } catch (CmdLineException e) { + assertEquals("No such job \u2018never_created\u2019 exists.", e.getMessage()); + } + + AbstractProject project = j.jenkins.createProject(FreeStyleProject.class, "never_created"); + try { + AbstractProject not_found = AbstractProject.resolveForCLI("never_created1"); + fail("Exception should occur before!"); + } catch (CmdLineException e) { + assertEquals("No such job \u2018never_created1\u2019 exists. Perhaps you meant \u2018never_created\u2019?", e.getMessage()); + } + + } + + public static class MockBuildTriggerThrowsNPEOnStart extends Trigger { + @Override + public void start(hudson.model.Item project, boolean newInstance) { + throw new NullPointerException(); + } + + @TestExtension("loadingWithNPEOnTriggerStart") + public static class DescriptorImpl extends TriggerDescriptor { + + @Override + public boolean isApplicable(hudson.model.Item item) { + return false; + } + } + + } + + @Issue("SECURITY-617") + @Test + public void upstreamDownstreamExportApi() throws Exception { + FreeStyleProject us = j.createFreeStyleProject("upstream-project"); + FreeStyleProject ds = j.createFreeStyleProject("downstream-project"); + us.getPublishersList().add(new BuildTrigger(Collections.singleton(ds), Result.SUCCESS)); + j.jenkins.rebuildDependencyGraph(); + assertEquals(Collections.singletonList(ds), us.getDownstreamProjects()); + assertEquals(Collections.singletonList(us), ds.getUpstreamProjects()); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). + grant(Jenkins.READ).everywhere().toEveryone(). + grant(Item.READ).everywhere().to("alice"). + grant(Item.READ).onItems(us).to("bob"). + grant(Item.READ).onItems(ds).to("charlie")); + String api = j.createWebClient().withBasicCredentials("alice").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, containsString("downstream-project")); + api = j.createWebClient().withBasicCredentials("alice").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, containsString("upstream-project")); + api = j.createWebClient().withBasicCredentials("bob").goTo(us.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, not(containsString("downstream-project"))); + api = j.createWebClient().withBasicCredentials("charlie").goTo(ds.getUrl() + "api/json?pretty", null).getWebResponse().getContentAsString(); + System.out.println(api); + assertThat(api, not(containsString("upstream-project"))); + } + +} diff --git a/test/src/test/java/hudson/model/EnvironmentContributorTest.java b/test/src/test/java/hudson/model/EnvironmentContributorTest.java new file mode 100644 index 0000000000..f7a99814e8 --- /dev/null +++ b/test/src/test/java/hudson/model/EnvironmentContributorTest.java @@ -0,0 +1,40 @@ +package hudson.model; + +import hudson.EnvVars; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +public class EnvironmentContributorTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Makes sure that the project-scoped environment variables are getting + * consulted. + */ + @Test + public void projectScoped() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + CaptureEnvironmentBuilder c = new CaptureEnvironmentBuilder(); + p.getBuildersList().add(c); + p.setDescription("Issac Newton"); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + assertEquals("Issac Newton", c.getEnvVars().get("ABC")); + assertEquals("master", c.getEnvVars().get("NODE_NAME")); + } + + @TestExtension("projectScoped") + public static class JobScopedInjection extends EnvironmentContributor { + @Override + public void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) { + envs.put("ABC", j.getDescription()); + } + } + +} diff --git a/test/src/test/java/hudson/model/ExecutorTest.java b/test/src/test/java/hudson/model/ExecutorTest.java index e13ff1c345..70254456fd 100644 --- a/test/src/test/java/hudson/model/ExecutorTest.java +++ b/test/src/test/java/hudson/model/ExecutorTest.java @@ -136,10 +136,16 @@ public class ExecutorTest { grant(Jenkins.READ).everywhere().toEveryone(). grant(Item.READ).onItems(publicProject).toEveryone(). grant(Item.READ).onItems(secretProject).to("has-security-clearance")); - String api = j.createWebClient().login("has-security-clearance").goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials("has-security-clearance"); + String api = wc.goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, allOf(containsString("public-project"), containsString("secret-project"))); - api = j.createWebClient().login("regular-joe").goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); + + wc = j.createWebClient(); + wc.withBasicCredentials("regular-joe"); + api = wc.goTo(slave.toComputer().getUrl() + "api/json?pretty&depth=1", null).getWebResponse().getContentAsString(); System.out.println(api); assertThat(api, allOf(containsString("public-project"), not(containsString("secret-project")))); } diff --git a/test/src/test/java/hudson/model/ItemsTest.java b/test/src/test/java/hudson/model/ItemsTest.java index 32ec8e6e47..40dfd2fa8d 100644 --- a/test/src/test/java/hudson/model/ItemsTest.java +++ b/test/src/test/java/hudson/model/ItemsTest.java @@ -45,6 +45,7 @@ import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.apache.commons.httpclient.HttpStatus; +import org.junit.Before; import org.junit.Test; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -123,6 +124,8 @@ public class ItemsTest { // TODO would be more efficient to run these all as a single test case, but after a few Jetty seems to stop serving new content and new requests just hang. private void overwriteTargetSetUp() throws Exception { + User.getById("attacker", true); + // A fully visible item: r.createFreeStyleProject("visible").setDescription("visible"); // An item known to exist but not visible: @@ -185,7 +188,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // redirect perversely counts as a failure - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { throw new FailingHttpStatusCodeException(webResponse); } @@ -198,7 +201,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse(); r.jenkins.getItem("dupe").delete(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { throw new FailingHttpStatusCodeException(webResponse); @@ -209,7 +212,7 @@ public class ItemsTest { REST_CREATE { @Override void run(JenkinsRule r, String target) throws Exception { JenkinsRule.WebClient wc = wc(r); - WebRequest req = new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target), HttpMethod.POST); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + "createItem?name=" + target), HttpMethod.POST); req.setAdditionalHeader("Content-Type", "application/xml"); req.setRequestBody(""); wc.getPage(req); @@ -222,7 +225,7 @@ public class ItemsTest { JenkinsRule.WebClient wc = wc(r); wc.getOptions().setRedirectEnabled(false); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); - WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse(); + WebResponse webResponse = wc.getPage(new WebRequest(new URL(wc.getContextPath() + "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse(); if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { r.jenkins.getItem("dupe").delete(); throw new FailingHttpStatusCodeException(webResponse); @@ -275,14 +278,7 @@ public class ItemsTest { }; abstract void run(JenkinsRule r, String target) throws Exception; private static final JenkinsRule.WebClient wc(JenkinsRule r) throws Exception { - return r.createWebClient().login("attacker"); - } - // TODO replace with standard version once it is fixed to detect an existing query string - private static URL createCrumbedUrl(JenkinsRule r, JenkinsRule.WebClient wc, String relativePath) throws IOException { - CrumbIssuer issuer = r.jenkins.getCrumbIssuer(); - String crumbName = issuer.getDescriptor().getCrumbRequestField(); - String crumb = issuer.getCrumb(null); - return new URL(wc.getContextPath() + relativePath + (relativePath.contains("?") ? "&" : "?") + crumbName + "=" + crumb); + return r.createWebClient().withBasicApiToken("attacker"); } } diff --git a/test/src/test/java/hudson/model/JobTest.java b/test/src/test/java/hudson/model/JobTest.java index d20b9c28a6..58e14cdd33 100644 --- a/test/src/test/java/hudson/model/JobTest.java +++ b/test/src/test/java/hudson/model/JobTest.java @@ -202,7 +202,7 @@ public class JobTest { JenkinsRule.WebClient wc = j.createWebClient(); wc.assertFails("job/testJob/", HttpURLConnection.HTTP_NOT_FOUND); wc.assertFails("jobCaseInsensitive/testJob/", HttpURLConnection.HTTP_NOT_FOUND); - wc.login("joe"); // Has Item.READ permission + wc.withBasicCredentials("joe"); // Has Item.READ permission // Verify we can access both URLs: wc.goTo("job/testJob/"); wc.goTo("jobCaseInsensitive/TESTJOB/"); @@ -216,11 +216,14 @@ public class JobTest { Item.EXTENDED_READ.setEnabled(true); try { wc.assertFails("job/testJob/config.xml", HttpURLConnection.HTTP_FORBIDDEN); - wc.login("alice"); // Has CONFIGURE and EXTENDED_READ permission + + wc.withBasicApiToken(User.getById("alice", true)); // Has CONFIGURE and EXTENDED_READ permission tryConfigDotXml(wc, 500, "Both perms; should get 500"); - wc.login("bob"); // Has only CONFIGURE permission (this should imply EXTENDED_READ) + + wc.withBasicApiToken(User.getById("bob", true)); // Has only CONFIGURE permission (this should imply EXTENDED_READ) tryConfigDotXml(wc, 500, "Config perm should imply EXTENDED_READ"); - wc.login("charlie"); // Has only EXTENDED_READ permission + + wc.withBasicApiToken(User.getById("charlie", true)); // Has only EXTENDED_READ permission tryConfigDotXml(wc, 403, "No permission, should get 403"); } finally { Item.EXTENDED_READ.setEnabled(saveEnabled); diff --git a/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java index 2129c9fce9..4fab18d601 100644 --- a/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java +++ b/test/src/test/java/hudson/model/PasswordParameterDefinitionTest.java @@ -62,16 +62,21 @@ public class PasswordParameterDefinitionTest { return true; } }); + + User admin = User.getById("admin", true); + User dev = User.getById("dev", true); + JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // ParametersDefinitionProperty/index.jelly sends a 405 but really it is OK // Control case: admin can use default value. - j.submit(wc.login("admin").getPage(p, "build?delay=0sec").getFormByName("parameters")); + j.submit(wc.withBasicApiToken(admin).getPage(p, "build?delay=0sec").getFormByName("parameters")); j.waitUntilNoActivity(); FreeStyleBuild b1 = p.getLastBuild(); assertEquals(1, b1.getNumber()); j.assertLogContains("I heard about a s3cr3t!", j.assertBuildStatusSuccess(b1)); + // Another control case: anyone can enter a different value. - HtmlForm form = wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters"); + HtmlForm form = wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters"); HtmlPasswordInput input = form.getInputByName("value"); input.setText("rumor"); j.submit(form); @@ -79,14 +84,16 @@ public class PasswordParameterDefinitionTest { FreeStyleBuild b2 = p.getLastBuild(); assertEquals(2, b2.getNumber()); j.assertLogContains("I heard about a rumor!", j.assertBuildStatusSuccess(b2)); + // Test case: anyone can use default value. - j.submit(wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters")); + j.submit(wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters")); j.waitUntilNoActivity(); FreeStyleBuild b3 = p.getLastBuild(); assertEquals(3, b3.getNumber()); j.assertLogContains("I heard about a s3cr3t!", j.assertBuildStatusSuccess(b3)); + // Another control case: blank values. - form = wc.login("dev").getPage(p, "build?delay=0sec").getFormByName("parameters"); + form = wc.withBasicApiToken(dev).getPage(p, "build?delay=0sec").getFormByName("parameters"); input = form.getInputByName("value"); input.setText(""); j.submit(form); diff --git a/test/src/test/java/hudson/model/ProjectTest.java b/test/src/test/java/hudson/model/ProjectTest.java index a2d5bf3f67..f966f4e56b 100644 --- a/test/src/test/java/hudson/model/ProjectTest.java +++ b/test/src/test/java/hudson/model/ProjectTest.java @@ -32,9 +32,12 @@ import hudson.tasks.*; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.GlobalMatrixAuthorizationStrategy; +import java.io.Closeable; +import java.net.HttpURLConnection; import java.net.URL; import java.util.Collections; +import hudson.util.Scrambler; import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.test.FakeChangeLogSCM; import hudson.scm.SCMRevisionState; @@ -553,9 +556,8 @@ public class ProjectTest { GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy(); j.jenkins.setAuthorizationStrategy(auth); j.jenkins.setCrumbIssuer(null); - HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); - j.jenkins.setSecurityRealm(realm); - User user = realm.createAccount("John Smith", "password"); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + User user = User.getById("john", true); try (ACLContext as = ACL.as(user)) { project.doDoDelete(null, null); fail("User should not have permission to build project"); @@ -568,7 +570,13 @@ public class ProjectTest { auth.add(Jenkins.READ, user.getId()); auth.add(Job.READ, user.getId()); auth.add(Job.DELETE, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl() + "delete").getForms(); + + // use Basic to speedup the test, normally it's pure UI testing + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId()); + HtmlPage p = wc.goTo(project.getUrl() + "delete"); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("doDelete".equals(form.getAttribute("action"))){ j.submit(form); @@ -605,9 +613,13 @@ public class ProjectTest { String cmd = "echo hello > change.log"; project.getBuildersList().add(Functions.isWindows()? new BatchFile(cmd) : new Shell(cmd)); j.buildAndAssertSuccess(project); - JenkinsRule.WebClient wc = j.createWebClient().login(user.getId(), "password"); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); WebRequest request = new WebRequest(new URL(wc.getContextPath() + project.getUrl() + "doWipeOutWorkspace"), HttpMethod.POST); HtmlPage p = wc.getPage(request); + assertEquals(p.getWebResponse().getStatusCode(), 200); + Thread.sleep(500); assertFalse("Workspace should not exist.", project.getSomeWorkspace().exists()); } @@ -633,7 +645,12 @@ public class ProjectTest { auth.add(Job.READ, user.getId()); auth.add(Job.CONFIGURE, user.getId()); auth.add(Jenkins.READ, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl()).getForms(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); + HtmlPage p = wc.goTo(project.getUrl()); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("disable".equals(form.getAttribute("action"))){ j.submit(form); @@ -666,7 +683,12 @@ public class ProjectTest { auth.add(Job.READ, user.getId()); auth.add(Job.CONFIGURE, user.getId()); auth.add(Jenkins.READ, user.getId()); - List forms = j.createWebClient().login(user.getId(), "password").goTo(project.getUrl()).getForms(); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.withBasicCredentials(user.getId(), "password"); + HtmlPage p = wc.goTo(project.getUrl()); + + List forms = p.getForms(); for(HtmlForm form:forms){ if("enable".equals(form.getAttribute("action"))){ j.submit(form); diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index 5e626aea3c..689b63e5ae 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -943,8 +943,12 @@ public class QueueTest { project.getBuildersList().add(new SleepBuilder(10)); project.scheduleBuild2(0); + User alice = User.getById("alice", true); + User bob = User.getById("bob", true); + User james = User.getById("james", true); + JenkinsRule.WebClient webClient = r.createWebClient(); - webClient.login("bob", "bob"); + webClient.withBasicApiToken(bob); XmlPage p = webClient.goToXml("queue/api/xml"); //bob has permission on the project and will be able to see it in the queue together with information such as the URL and the name. @@ -959,13 +963,15 @@ public class QueueTest { } } } + webClient = r.createWebClient(); - webClient.login("alice"); + webClient.withBasicApiToken(alice); XmlPage p2 = webClient.goToXml("queue/api/xml"); //alice does not have permission on the project and will not see it in the queue. assertTrue(p2.getByXPath("/queue/node()").isEmpty()); + webClient = r.createWebClient(); - webClient.login("james"); + webClient.withBasicApiToken(james); XmlPage p3 = webClient.goToXml("queue/api/xml"); //james has DISCOVER permission on the project and will only be able to see the task name. @@ -975,9 +981,9 @@ public class QueueTest { // Also check individual item exports. String url = project.getQueueItem().getUrl() + "api/xml"; - r.createWebClient().login("bob").goToXml(url); // OK, 200 - r.createWebClient().login("james").assertFails(url, HttpURLConnection.HTTP_FORBIDDEN); // only DISCOVER → AccessDeniedException - r.createWebClient().login("alice").assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); // not even DISCOVER + r.createWebClient().withBasicApiToken(bob).goToXml(url); // OK, 200 + r.createWebClient().withBasicApiToken(james).assertFails(url, HttpURLConnection.HTTP_FORBIDDEN); // only DISCOVER → AccessDeniedException + r.createWebClient().withBasicApiToken(alice).assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); // not even DISCOVER } //we force the project not to be executed so that it stays in the queue diff --git a/test/src/test/java/hudson/model/Slave2Test.java b/test/src/test/java/hudson/model/SlaveTest.java similarity index 69% rename from test/src/test/java/hudson/model/Slave2Test.java rename to test/src/test/java/hudson/model/SlaveTest.java index 6ab40e3eed..76823820c3 100644 --- a/test/src/test/java/hudson/model/Slave2Test.java +++ b/test/src/test/java/hudson/model/SlaveTest.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2015 CloudBees, Inc. + * Copyright (c) 2004-2009, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,59 +23,117 @@ */ package hudson.model; +import com.gargoylesoftware.htmlunit.Page; +import groovy.util.XmlSlurper; import hudson.DescriptorExtensionList; import hudson.ExtensionList; +import hudson.security.csrf.CrumbIssuer; import hudson.slaves.ComputerLauncher; import hudson.slaves.DumbSlave; +import hudson.slaves.JNLPLauncher; import hudson.slaves.NodeProperty; import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.RetentionStrategy; +import hudson.util.FormValidation; +import static hudson.util.FormValidation.Kind.WARNING; +import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URL; import java.net.URLEncoder; import java.util.HashSet; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; - +import org.apache.commons.io.IOUtils; import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeThat; - +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; -/** - * Tests for the {@link Slave} class. - * There is also a Groovy implementation of such test file, hence the class name - * has an index. - * @author Oleg Nenashev - */ -public class Slave2Test { - +public class SlaveTest { + @Rule - public JenkinsRule rule = new JenkinsRule(); - + public JenkinsRule j = new JenkinsRule(); + + /** + * Makes sure that a form validation method gets inherited. + */ + @Test + public void formValidation() throws Exception { + j.executeOnServer(() -> { + assertNotNull(j.jenkins.getDescriptor(DumbSlave.class).getCheckUrl("remoteFS")); + return null; + }); + } + + /** + * Programmatic config.xml submission. + */ + @Test + public void slaveConfigDotXml() throws Exception { + DumbSlave s = j.createSlave(); + JenkinsRule.WebClient wc = j.createWebClient(); + Page p = wc.goTo("computer/" + s.getNodeName() + "/config.xml", "application/xml"); + String xml = p.getWebResponse().getContentAsString(); + new XmlSlurper().parseText(xml); // verify that it is XML + + // make sure it survives the roundtrip + post("computer/" + s.getNodeName() + "/config.xml", xml); + + assertNotNull(j.jenkins.getNode(s.getNodeName())); + + xml = IOUtils.toString(getClass().getResource("SlaveTest/slave.xml").openStream()); + xml = xml.replace("NAME", s.getNodeName()); + post("computer/" + s.getNodeName() + "/config.xml", xml); + + s = (DumbSlave) j.jenkins.getNode(s.getNodeName()); + assertNotNull(s); + assertEquals("some text", s.getNodeDescription()); + assertEquals(JNLPLauncher.class, s.getLauncher().getClass()); + } + + private void post(String url, String xml) throws Exception { + HttpURLConnection con = (HttpURLConnection) new URL(j.getURL(), url).openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/xml;charset=UTF-8"); + con.setRequestProperty(CrumbIssuer.DEFAULT_CRUMB_NAME, "test"); + con.setDoOutput(true); + con.getOutputStream().write(xml.getBytes("UTF-8")); + con.getOutputStream().close(); + IOUtils.copy(con.getInputStream(), System.out); + } + + @Test + public void remoteFsCheck() throws Exception { + DumbSlave.DescriptorImpl d = j.jenkins.getDescriptorByType(DumbSlave.DescriptorImpl.class); + assertEquals(FormValidation.ok(), d.doCheckRemoteFS("c:\\")); + assertEquals(FormValidation.ok(), d.doCheckRemoteFS("/tmp")); + assertEquals(WARNING, d.doCheckRemoteFS("relative/path").kind); + assertEquals(WARNING, d.doCheckRemoteFS("/net/foo/bar/zot").kind); + assertEquals(WARNING, d.doCheckRemoteFS("\\\\machine\\folder\\foo").kind); + } + @Test @Issue("SECURITY-195") public void shouldNotEscapeJnlpSlavesResources() throws Exception { - Slave slave = rule.createSlave(); - + Slave slave = j.createSlave(); + // Spot-check correct requests assertJnlpJarUrlIsAllowed(slave, "agent.jar"); assertJnlpJarUrlIsAllowed(slave, "slave.jar"); assertJnlpJarUrlIsAllowed(slave, "remoting.jar"); assertJnlpJarUrlIsAllowed(slave, "jenkins-cli.jar"); assertJnlpJarUrlIsAllowed(slave, "hudson-cli.jar"); - + // Check that requests to other WEB-INF contents fail assertJnlpJarUrlFails(slave, "web.xml"); assertJnlpJarUrlFails(slave, "web.xml"); @@ -83,7 +141,7 @@ public class Slave2Test { assertJnlpJarUrlFails(slave, "classes/dependencies.txt"); assertJnlpJarUrlFails(slave, "plugins/ant.hpi"); assertJnlpJarUrlFails(slave, "nonexistentfolder/something.txt"); - + // Try various kinds of folder escaping (SECURITY-195) assertJnlpJarUrlFails(slave, "../"); assertJnlpJarUrlFails(slave, ".."); @@ -93,7 +151,7 @@ public class Slave2Test { assertJnlpJarUrlFails(slave, "foo/../../bar"); assertJnlpJarUrlFails(slave, "./../foo/bar"); } - + private void assertJnlpJarUrlFails(@Nonnull Slave slave, @Nonnull String url) throws Exception { // Raw access to API Slave.JnlpJar jnlpJar = slave.getComputer().getJnlpJars(url); @@ -105,15 +163,15 @@ public class Slave2Test { } fail("Expected the MalformedURLException for " + url); } - + private void assertJnlpJarUrlIsAllowed(@Nonnull Slave slave, @Nonnull String url) throws Exception { // Raw access to API Slave.JnlpJar jnlpJar = slave.getComputer().getJnlpJars(url); assertNotNull(jnlpJar.getURL()); - - + + // Access from a Web client - JenkinsRule.WebClient client = rule.createWebClient(); + JenkinsRule.WebClient client = j.createWebClient(); assertEquals(200, client.getPage(client.getContextPath() + "jnlpJars/" + URLEncoder.encode(url, "UTF-8")).getWebResponse().getStatusCode()); assertEquals(200, client.getPage(jnlpJar.getURL()).getWebResponse().getStatusCode()); } @@ -122,9 +180,9 @@ public class Slave2Test { @Issue("JENKINS-36280") public void launcherFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList> descriptors = - rule.getInstance().getDescriptorList(ComputerLauncher.class); + j.getInstance().getDescriptorList(ComputerLauncher.class); assumeThat("we need at least two launchers to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.computerLauncherDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -140,7 +198,7 @@ public class Slave2Test { @Issue("JENKINS-36280") public void retentionFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList, Descriptor>> descriptors = RetentionStrategy.all(); assumeThat("we need at least two retention strategies to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.retentionStrategyDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -157,7 +215,7 @@ public class Slave2Test { @Issue("JENKINS-36280") public void propertyFiltering() throws Exception { DumbSlave.DescriptorImpl descriptor = - rule.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); + j.getInstance().getDescriptorByType(DumbSlave.DescriptorImpl.class); DescriptorExtensionList, NodePropertyDescriptor> descriptors = NodeProperty.all(); assumeThat("we need at least two node properties to test this", descriptors.size(), not(anyOf(is(0), is(1)))); assertThat(descriptor.nodePropertyDescriptors(null), containsInAnyOrder(descriptors.toArray(new Descriptor[descriptors.size()]))); @@ -170,8 +228,6 @@ public class Slave2Test { assertThat(descriptor.nodePropertyDescriptors(null), hasItem(victim)); } - - @TestExtension public static class DynamicFilter extends DescriptorVisibilityFilter { @@ -191,4 +247,5 @@ public class Slave2Test { return !descriptors.contains(descriptor); } } + } diff --git a/test/src/test/java/hudson/model/UserTest.java b/test/src/test/java/hudson/model/UserTest.java index b81105fe55..34bb26a1c9 100644 --- a/test/src/test/java/hudson/model/UserTest.java +++ b/test/src/test/java/hudson/model/UserTest.java @@ -415,7 +415,7 @@ public class UserTest { auth.add(Jenkins.ADMINISTER, user.getId()); auth.add(Jenkins.READ, user2.getId()); SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - HtmlForm form = j.createWebClient().login(user.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + HtmlForm form = j.createWebClient().withBasicCredentials(user.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); form.getInputByName("_.fullName").setValueAttribute("Alice Smith"); j.submit(form); assertEquals("User should have full name Alice Smith.", "Alice Smith", user2.getFullName()); @@ -429,7 +429,7 @@ public class UserTest { fail("AccessDeniedException should be thrown."); } } - form = j.createWebClient().login(user2.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + form = j.createWebClient().withBasicCredentials(user2.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); form.getInputByName("_.fullName").setValueAttribute("John"); j.submit(form); diff --git a/test/src/test/java/hudson/model/ViewTest.java b/test/src/test/java/hudson/model/ViewTest.java index 7a70d87df2..21aa56fc38 100644 --- a/test/src/test/java/hudson/model/ViewTest.java +++ b/test/src/test/java/hudson/model/ViewTest.java @@ -529,7 +529,7 @@ public class ViewTest { return null; } }); - JenkinsRule.WebClient wc = j.createWebClient().login("admin"); + JenkinsRule.WebClient wc = j.createWebClient().withBasicCredentials("admin"); assertEquals("original ${rootURL}/checkJobName still supported", "
", wc.goTo("checkJobName?value=stuff").getWebResponse().getContentAsString()); assertEquals("but now possible on a view in a folder", "
", wc.goTo("job/d1/view/All/checkJobName?value=stuff").getWebResponse().getContentAsString()); } diff --git a/test/src/test/java/hudson/security/ACLTest.java b/test/src/test/java/hudson/security/ACLTest.java new file mode 100644 index 0000000000..c97f01198b --- /dev/null +++ b/test/src/test/java/hudson/security/ACLTest.java @@ -0,0 +1,75 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.security; + +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import java.util.Collection; +import java.util.Collections; +import org.acegisecurity.Authentication; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class ACLTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Issue("JENKINS-20474") + @Test + public void bypassStrategyOnSystem() throws Exception { + FreeStyleProject p = r.createFreeStyleProject(); + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new DoNotBotherMe()); + assertTrue(p.hasPermission(Item.CONFIGURE)); + assertTrue(p.hasPermission(ACL.SYSTEM, Item.CONFIGURE)); + p.checkPermission(Item.CONFIGURE); + p.checkAbortPermission(); + assertEquals(Collections.singletonList(p), r.jenkins.getAllItems()); + } + + private static class DoNotBotherMe extends AuthorizationStrategy { + + @Override + public ACL getRootACL() { + return new ACL() { + @Override + public boolean hasPermission(Authentication a, Permission permission) { + throw new AssertionError("should not have needed to check " + permission + " for " + a); + } + }; + } + + @Override + public Collection getGroups() { + return Collections.emptySet(); + } + + } + +} diff --git a/test/src/test/java/hudson/security/CliAuthenticationTest.java b/test/src/test/java/hudson/security/CliAuthenticationTest.java index bdddf14407..c02d085586 100644 --- a/test/src/test/java/hudson/security/CliAuthenticationTest.java +++ b/test/src/test/java/hudson/security/CliAuthenticationTest.java @@ -1,24 +1,34 @@ package hudson.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - +import hudson.ExtensionList; import hudson.cli.CLI; import hudson.cli.CLICommand; import jenkins.model.Jenkins; +import jenkins.security.SecurityListener; +import jenkins.security.SpySecurityListener; import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.io.input.NullInputStream; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.For; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; +import org.springframework.dao.DataAccessException; import java.io.ByteArrayOutputStream; import java.util.Arrays; +import static org.junit.Assert.*; + /** * @author Kohsuke Kawaguchi */ @@ -27,6 +37,14 @@ public class CliAuthenticationTest { @Rule public JenkinsRule j = new JenkinsRule(); + + private SpySecurityListener spySecurityListener; + + @Before + public void prepareListeners(){ + //TODO simplify using #3021 into ExtensionList.lookupSingleton(SpySecurityListener.class) + this.spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + } @Test public void test() throws Exception { @@ -40,6 +58,10 @@ public class CliAuthenticationTest { assertEquals(0, command(args)); } + private void unsuccessfulCommand(String... args) throws Exception { + assertNotEquals(0, command(args)); + } + private int command(String... args) throws Exception { try (CLI cli = new CLI(j.getURL())) { return cli.execute(args); @@ -91,9 +113,86 @@ public class CliAuthenticationTest { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); successfulCommand("login","--username","abc","--password","abc"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedInCalls.assertLastEventIsAndThenRemoveIt("abc"); + successfulCommand("test"); // now we can run without an explicit credential + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedInCalls.assertNoNewEvents(); + successfulCommand("logout"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(userDetails -> userDetails.getUsername().equals("abc")); + spySecurityListener.loggedOutCalls.assertLastEventIsAndThenRemoveIt("abc"); + successfulCommand("anonymous"); // now we should run as anonymous + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); + spySecurityListener.loggedOutCalls.assertNoNewEvents(); + } + + @Test + @Issue("JENKINS-27026") + public void loginAsALegitimateUserButUnknown() throws Exception { + j.jenkins.setSecurityRealm(new MockSecurityRealm()); + + String username = "alice"; + + unsuccessfulCommand("login","--username",username,"--password","badCredentials"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + unsuccessfulCommand("login","--username",username,"--password","usernameNotFound"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + unsuccessfulCommand("login","--username",username,"--password","mayOrMayNotExist"); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + + // in case of authentication that throw a DataAccessException (see impersonatingUserDetailsService) + // the CLI login command does not work as expected + unsuccessfulCommand("login","--username",username,"--password","dataAccess"); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); + spySecurityListener.failedToLogInCalls.assertNoNewEvents(); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.loggedInCalls.assertNoNewEvents(); +// spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt(username); +// spySecurityListener.failedToLogInCalls.assertLastEventIsAndThenRemoveIt(username); + } + + private static class MockSecurityRealm extends AbstractPasswordBasedSecurityRealm { + @Override protected UserDetails authenticate(String username, String password) throws AuthenticationException { + switch(password){ + case "badCredentials": throw new BadCredentialsException("BadCredentials requested"); + case "usernameNotFound": throw new UsernameNotFoundException("UsernameNotFound requested"); + case "mayOrMayNotExist": throw new UserMayOrMayNotExistException("MayOrMayNotExist requested"); + case "dataAccess": throw new DataAccessException("DataAccess requested"){}; + default: + return new User( + username, password, + true, true, true, + new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY} + ); + } + } + + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return null; + } + + @Override public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { + return null; + } } /** @@ -112,4 +211,7 @@ public class CliAuthenticationTest { assertTrue(out1.contains("Bad Credentials. Search the server log for")); assertTrue(out2.contains("Bad Credentials. Search the server log for")); } + + @TestExtension + public static class SpySecurityListenerImpl extends SpySecurityListener {} } diff --git a/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java b/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java index f989e1439a..04d3820e21 100644 --- a/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java +++ b/test/src/test/java/hudson/security/ExtendedReadPermissionTest.java @@ -52,7 +52,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertTrue("Charlie should have extended read for this test", gas.hasExplicitPermission("charlie",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("charlie","charlie"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("charlie"); + HtmlPage page = wc.goTo("job/a/configure"); HtmlForm form = page.getFormByName("config"); HtmlButton saveButton = r.getButtonByCaption(form,"Save"); @@ -68,7 +70,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertFalse("Charlie should not have extended read for this test", gas.hasExplicitPermission("charlie",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("charlie","charlie"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("charlie"); + wc.assertFails("job/a/configure", HttpURLConnection.HTTP_FORBIDDEN); } @@ -81,7 +85,9 @@ public class ExtendedReadPermissionTest { GlobalMatrixAuthorizationStrategy gas = (GlobalMatrixAuthorizationStrategy)as; assertFalse("Bob should not have extended read for this test", gas.hasExplicitPermission("bob",Item.EXTENDED_READ)); - JenkinsRule.WebClient wc = r.createWebClient().login("bob","bob"); + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicCredentials("bob"); + wc.assertFails("job/a/configure", HttpURLConnection.HTTP_FORBIDDEN); } diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java index 966b82c2ff..e58fb48d0b 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java @@ -25,33 +25,57 @@ package hudson.security; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.util.NameValuePair; import com.gargoylesoftware.htmlunit.xml.XmlPage; +import hudson.ExtensionList; import hudson.model.User; import hudson.remoting.Base64; import static hudson.security.HudsonPrivateSecurityRealm.CLASSIC; import static hudson.security.HudsonPrivateSecurityRealm.PASSWORD_ENCODER; import hudson.security.pages.SignupPage; import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; + import jenkins.security.ApiTokenProperty; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.xml.HasXPath.hasXPath; import static org.junit.Assert.*; + +import jenkins.security.SecurityListener; +import org.apache.commons.lang.StringUtils; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.WithoutJenkins; import org.jvnet.hudson.test.recipes.LocalData; +import javax.annotation.Nonnull; + public class HudsonPrivateSecurityRealmTest { @Rule public JenkinsRule j = new JenkinsRule(); + private SpySecurityListenerImpl spySecurityListener; + + @Before + public void linkExtension() throws Exception { + spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + } + /** * Tests the data compatibility with Hudson before 1.283. * Starting 1.283, passwords are now stored hashed. @@ -67,6 +91,7 @@ public class HudsonPrivateSecurityRealmTest { // verify the sanity that the password is really used // this should fail j.createWebClient().login("bob", "bob"); + fail(); } catch (FailingHttpStatusCodeException e) { assertEquals(401,e.getStatusCode()); } @@ -259,4 +284,106 @@ public class HudsonPrivateSecurityRealmTest { assertNull(User.get("unknown2",false, Collections.emptyMap())); } + @Issue("JENKINS-48383") + @Test + public void selfRegistrationTriggerLoggedIn() throws Exception { + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null); + j.jenkins.setSecurityRealm(securityRealm); + j.jenkins.setCrumbIssuer(null); + + assertTrue(spySecurityListener.loggedInUsernames.isEmpty()); + + createFirstAccount("admin"); + assertTrue(spySecurityListener.loggedInUsernames.get(0).equals("admin")); + + createAccountByAdmin("alice"); + // no new event in such case + assertTrue(spySecurityListener.loggedInUsernames.isEmpty()); + + selfRegistration("bob"); + assertTrue(spySecurityListener.loggedInUsernames.get(0).equals("bob")); + } + + private void createFirstAccount(String login) throws Exception { + assertNull(User.getById(login, false)); + + JenkinsRule.WebClient wc = j.createWebClient(); + + HudsonPrivateSecurityRealm.SignupInfo info = new HudsonPrivateSecurityRealm.SignupInfo(); + info.username = login; + info.password1 = login; + info.password2 = login; + info.fullname = StringUtils.capitalize(login); + + WebRequest request = new WebRequest(new URL(wc.getContextPath() + "securityRealm/createFirstAccount"), HttpMethod.POST); + request.setRequestParameters(Arrays.asList( + new NameValuePair("username", login), + new NameValuePair("password1", login), + new NameValuePair("password2", login), + new NameValuePair("fullname", StringUtils.capitalize(login)), + new NameValuePair("email", login + "@" + login + ".com") + )); + + HtmlPage p = wc.getPage(request); + assertEquals(200, p.getWebResponse().getStatusCode()); + assertTrue(p.getDocumentElement().getElementsByAttribute("div", "class", "error").isEmpty()); + + assertNotNull(User.getById(login, false)); + } + + private void createAccountByAdmin(String login) throws Exception { + // user should not exist before + assertNull(User.getById(login, false)); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("admin"); + + spySecurityListener.loggedInUsernames.clear(); + + HtmlPage page = wc.goTo("securityRealm/addUser"); + HtmlForm form = page.getForms().stream() + .filter(htmlForm -> htmlForm.getActionAttribute().endsWith("/securityRealm/createAccountByAdmin")) + .findFirst() + .orElseThrow(() -> new AssertionError("Form must be present")); + + form.getInputByName("username").setValueAttribute(login); + form.getInputByName("password1").setValueAttribute(login); + form.getInputByName("password2").setValueAttribute(login); + form.getInputByName("fullname").setValueAttribute(StringUtils.capitalize(login)); + form.getInputByName("email").setValueAttribute(login + "@" + login + ".com"); + + HtmlPage p = j.submit(form); + assertEquals(200, p.getWebResponse().getStatusCode()); + assertTrue(p.getDocumentElement().getElementsByAttribute("div", "class", "error").isEmpty()); + + assertNotNull(User.getById(login, false)); + } + + private void selfRegistration(String login) throws Exception { + // user should not exist before + assertNull(User.getById(login, false)); + + JenkinsRule.WebClient wc = j.createWebClient(); + SignupPage signup = new SignupPage(wc.goTo("signup")); + signup.enterUsername(login); + signup.enterPassword(login); + signup.enterFullName(StringUtils.capitalize(login)); + signup.enterEmail(login + "@" + login + ".com"); + + HtmlPage p = signup.submit(j); + assertEquals(200, p.getWebResponse().getStatusCode()); + assertTrue(p.getDocumentElement().getElementsByAttribute("div", "class", "error").isEmpty()); + + assertNotNull(User.getById(login, false)); + } + + @TestExtension + public static class SpySecurityListenerImpl extends SecurityListener { + private List loggedInUsernames = new ArrayList<>(); + + @Override + protected void loggedIn(@Nonnull String username) { + loggedInUsernames.add(username); + } + } } diff --git a/test/src/test/java/hudson/security/LoginTest.java b/test/src/test/java/hudson/security/LoginTest.java index 86c1a0662c..91ef0e6e34 100644 --- a/test/src/test/java/hudson/security/LoginTest.java +++ b/test/src/test/java/hudson/security/LoginTest.java @@ -11,6 +11,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; +import hudson.model.User; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -56,7 +57,7 @@ public class LoginTest { WebClient wc = j.createWebClient(); wc.assertFails("loginError", SC_UNAUTHORIZED); // but not once the user logs in. - verifyNotError(wc.login("alice")); + verifyNotError(wc.withBasicApiToken(User.getById("alice", true))); } private HtmlForm prepareLoginFormWithRememberMeChecked(WebClient wc) throws IOException, org.xml.sax.SAXException { diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java new file mode 100644 index 0000000000..35bea92686 --- /dev/null +++ b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java @@ -0,0 +1,130 @@ +package hudson.security; + +import com.gargoylesoftware.htmlunit.util.Cookie; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import static java.util.logging.Level.FINEST; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; +import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices; +import org.acegisecurity.userdetails.User; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UsernameNotFoundException; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.LoggerRule; +import org.springframework.dao.DataAccessException; + +public class TokenBasedRememberMeServices2Test { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Rule + public LoggerRule logging = new LoggerRule(); + + private static boolean failureInduced; + + @Before + public void resetFailureInduced() { + failureInduced = false; + } + + @Test + public void rememberMeAutoLoginFailure() throws Exception { + j.jenkins.setSecurityRealm(new InvalidUserWhenLoggingBackInRealm()); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("alice", "alice", true); + + // we should see a remember me cookie + Cookie c = getRememberMeCookie(wc); + assertNotNull(c); + + // start a new session and attempt to access Jenkins, + // which should cause autoLogin failures + wc = j.createWebClient(); + wc.getCookieManager().addCookie(c); + + // even if SecurityRealm chokes, it shouldn't kill the page + logging.capture(1000).record(TokenBasedRememberMeServices.class, FINEST); + wc.goTo(""); + + // make sure that the server recorded this failure + assertTrue(failureInduced); + assertTrue(logging.getMessages().stream().anyMatch(m -> m.contains("contained username 'alice' but was not found"))); + // and the problematic cookie should have been removed + assertNull(getRememberMeCookie(wc)); + } + + private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) { + return wc.getCookieManager().getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY); + } + + private static class InvalidUserWhenLoggingBackInRealm extends AbstractPasswordBasedSecurityRealm { + @Override + protected UserDetails authenticate(String username, String password) throws AuthenticationException { + if (username.equals(password)) { + return new User(username, password, true, new GrantedAuthority[] {new GrantedAuthorityImpl("myteam")}); + } + throw new BadCredentialsException(username); + } + + @Override + public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { + throw new UnsupportedOperationException(); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + failureInduced = true; + throw new UsernameNotFoundException("intentionally not working"); + } + } + + @Test + public void basicFlow() throws Exception { + j.jenkins.setSecurityRealm(new StupidRealm()); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("bob", "bob", true); + + // we should see a remember me cookie + Cookie c = getRememberMeCookie(wc); + assertNotNull(c); + + // start a new session and attempt to access Jenkins, + wc = j.createWebClient(); + wc.getCookieManager().addCookie(c); + + // this will trigger remember me + wc.goTo(""); + + // make sure that our security realm failed to report the info correctly + assertTrue(failureInduced); + // but we should have logged in + wc.executeOnServer(() -> { + Authentication a = Jenkins.getAuthentication(); + assertEquals("bob", a.getName()); + assertEquals(ImmutableList.of("authenticated", "myteam"), Arrays.asList(a.getAuthorities()).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); + return null; + }); + } + + private static class StupidRealm extends InvalidUserWhenLoggingBackInRealm { + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + failureInduced = true; + throw new UserMayOrMayNotExistException("I cannot tell"); + } + } + +} diff --git a/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java b/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java index f3fddef1af..52dc455faf 100644 --- a/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java +++ b/test/src/test/java/hudson/security/csrf/DefaultCrumbIssuerTest.java @@ -8,6 +8,8 @@ package hudson.security.csrf; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import jenkins.model.Jenkins; +import junit.framework.Assert; import net.sf.json.JSONObject; import org.junit.Before; import org.junit.Rule; @@ -135,4 +137,19 @@ public class DefaultCrumbIssuerTest { wc.assertFails("crumbIssuer/api/json?jsonp=hack", HttpURLConnection.HTTP_FORBIDDEN); } + @Issue("JENKINS-34254") + @Test public void testRequirePostErrorPageCrumb() throws Exception { + Jenkins.getInstance().setCrumbIssuer(new DefaultCrumbIssuer(false)); + WebClient wc = r.createWebClient(); + try { + wc.goTo("quietDown"); + fail("expected failure"); + } catch (FailingHttpStatusCodeException ex) { + Assert.assertEquals("expect HTTP 405 method not allowed", 405, ex.getStatusCode()); + } + HtmlPage retry = (HtmlPage) wc.getCurrentWindow().getEnclosedPage(); + HtmlPage success = r.submit(retry.getFormByName("retry")); + Assert.assertTrue("quieting down", r.jenkins.isQuietingDown()); + } + } diff --git a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java index 0bff3490cf..c57acd55cd 100644 --- a/test/src/test/java/hudson/util/RobustReflectionConverterTest.java +++ b/test/src/test/java/hudson/util/RobustReflectionConverterTest.java @@ -27,16 +27,18 @@ package hudson.util; import hudson.cli.CLICommandInvoker; import hudson.diagnosis.OldDataMonitor; import hudson.model.AbstractDescribableImpl; -import hudson.model.Items; -import hudson.model.JobProperty; -import hudson.model.JobPropertyDescriptor; import hudson.model.Descriptor; import hudson.model.FreeStyleProject; +import hudson.model.Items; import hudson.model.Job; +import hudson.model.JobProperty; +import hudson.model.JobPropertyDescriptor; import hudson.model.Saveable; +import hudson.model.User; import hudson.security.ACL; import java.io.ByteArrayInputStream; +import java.net.URL; import java.util.Collections; import java.util.Map; @@ -81,7 +83,7 @@ public class RobustReflectionConverterTest { // GUI related implementations (@DataBoundConstructor and newInstance) aren't used actually // (no jelly files are provides and they don't work actually), // but written to clarify a use case. - public static class AcceptOnlySpecificKeyword extends AbstractDescribableImpl{ + public static class AcceptOnlySpecificKeyword extends AbstractDescribableImpl { public static final String ACCEPT_KEYWORD = "accept"; private final String keyword; @@ -185,7 +187,9 @@ public class RobustReflectionConverterTest { @Test public void testRestInterfaceFailure() throws Exception { Items.XSTREAM2.addCriticalField(KeywordProperty.class, "criticalField"); - + + User test = User.getById("test", true); + // without addCriticalField. This is accepted. { FreeStyleProject p = r.createFreeStyleProject(); @@ -198,11 +202,8 @@ public class RobustReflectionConverterTest { // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); - wc.login("test", "test"); - WebRequest req = new WebRequest( - wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), - HttpMethod.POST - ); + wc.withBasicApiToken(test); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + String.format("%s/config.xml", p.getUrl())), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, "badvalue", AcceptOnlySpecificKeyword.ACCEPT_KEYWORD)); wc.getPage(req); @@ -231,11 +232,8 @@ public class RobustReflectionConverterTest { // Configure a bad keyword via REST. r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); WebClient wc = r.createWebClient(); - wc.login("test", "test"); - WebRequest req = new WebRequest( - wc.createCrumbedUrl(String.format("%s/config.xml", p.getUrl())), - HttpMethod.POST - ); + wc.withBasicApiToken(test); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + String.format("%s/config.xml", p.getUrl())), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody(String.format(CONFIGURATION_TEMPLATE, AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, "badvalue")); diff --git a/test/src/test/java/hudson/widgets/HistoryWidgetTest.java b/test/src/test/java/hudson/widgets/HistoryWidgetTest.java new file mode 100644 index 0000000000..fd28e63f19 --- /dev/null +++ b/test/src/test/java/hudson/widgets/HistoryWidgetTest.java @@ -0,0 +1,27 @@ +package hudson.widgets; + +import hudson.model.FreeStyleProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class HistoryWidgetTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("JENKINS-15499") + public void moreLink() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + for (int x = 0; x < 3; x++) { + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.setJavaScriptEnabled(false); + wc.goTo("job/" + p.getName() + "/buildHistory/all"); + } + +} diff --git a/test/src/test/java/jenkins/bugs/Jenkins19124Test.java b/test/src/test/java/jenkins/bugs/Jenkins19124Test.java new file mode 100644 index 0000000000..4f976f85ba --- /dev/null +++ b/test/src/test/java/jenkins/bugs/Jenkins19124Test.java @@ -0,0 +1,94 @@ +package jenkins.bugs; + +import com.gargoylesoftware.htmlunit.WebClientUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlSelect; +import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.FreeStyleProject; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.io.IOException; +import javax.inject.Inject; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.QueryParameter; + +public class Jenkins19124Test { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Inject + public Foo.DescriptorImpl d; + + @Issue("JENKINS-19124") + @Test + public void interrelatedFormValidation() throws Exception { + j.jenkins.getInjector().injectMembers(this); + + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new Foo()); + + JenkinsRule.WebClient wc = j.createWebClient(); + HtmlPage c = wc.getPage(p, "configure"); + HtmlTextInput alpha = c.getElementByName("_.alpha"); + alpha.setValueAttribute("hello"); + WebClientUtil.waitForJSExec(wc); + assertEquals("hello", d.alpha); + assertEquals("2", d.bravo); + + HtmlSelect bravo = c.getElementByName("_.bravo"); + bravo.setSelectedAttribute("1", true); + WebClientUtil.waitForJSExec(wc); + assertEquals("hello", d.alpha); + assertEquals("1", d.bravo); + } + + public static class Foo extends Builder { + + public String getAlpha() { + return "alpha"; + } + public String getBravo() { + return "2"; + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + return true; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + + String alpha, bravo; + + public FormValidation doCheckAlpha(@QueryParameter String value, @QueryParameter String bravo) { + this.alpha = value; + this.bravo = bravo; + return FormValidation.ok(); + } + + public ListBoxModel doFillBravoItems() { + return new ListBoxModel().add("1").add("2").add("3"); + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + } + + } + +} diff --git a/test/src/test/groovy/jenkins/bugs/Jenkins41511Test.java b/test/src/test/java/jenkins/bugs/Jenkins41511Test.java similarity index 100% rename from test/src/test/groovy/jenkins/bugs/Jenkins41511Test.java rename to test/src/test/java/jenkins/bugs/Jenkins41511Test.java diff --git a/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java new file mode 100644 index 0000000000..856e5263ba --- /dev/null +++ b/test/src/test/java/jenkins/install/LoadDetachedPluginsTest.java @@ -0,0 +1,160 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.install; + +import hudson.ClassicPluginStrategy; +import hudson.ClassicPluginStrategy.DetachedPlugin; +import hudson.Plugin; +import hudson.PluginManager; +import hudson.PluginManagerUtil; +import hudson.PluginWrapper; +import hudson.util.VersionNumber; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RestartableJenkinsRule; +import org.jvnet.hudson.test.recipes.LocalData; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class LoadDetachedPluginsTest { + + @Rule public RestartableJenkinsRule rr = PluginManagerUtil.newRestartableJenkinsRule(); + + @Issue("JENKINS-48365") + @Test + @LocalData + public void upgradeFromJenkins1() throws IOException { + VersionNumber since = new VersionNumber("1.550"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(4)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + assertNoFailedPlugins(r); + }); + } + + @Issue("JENKINS-48365") + @Test + @LocalData + public void upgradeFromJenkins2() { + VersionNumber since = new VersionNumber("2.0"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + assertNoFailedPlugins(r); + }); + } + + @Issue("JENKINS-48604") + @Test + @LocalData + public void upgradeFromJenkins2WithNewerDependency() { + VersionNumber since = new VersionNumber("2.0"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + Plugin scriptSecurity = r.jenkins.getPlugin("script-security"); + assertThat("Script-security should be installed", scriptSecurity, notNullValue()); + assertThat("Dependencies of detached plugins should not be downgraded", + scriptSecurity.getWrapper().getVersionNumber(), equalTo(new VersionNumber("1.34"))); + assertNoFailedPlugins(r); + }); + } + + @Test + @LocalData + public void upgradeFromJenkins2WithOlderDependency() { + VersionNumber since = new VersionNumber("2.0"); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(since); + assertThat("Plugins have been detached since the pre-upgrade version", + detachedPlugins.size(), greaterThan(1)); + assertThat("Plugins detached between the pre-upgrade version and the current version should be installed", + getInstalledDetachedPlugins(r, detachedPlugins).size(), equalTo(detachedPlugins.size())); + Plugin scriptSecurity = r.jenkins.getPlugin("script-security"); + assertThat("Script-security should be installed", scriptSecurity, notNullValue()); + assertThat("Dependencies of detached plugins should be upgraded to the required version", + scriptSecurity.getWrapper().getVersionNumber(), equalTo(new VersionNumber("1.18.1"))); + assertNoFailedPlugins(r); + }); + } + + @Test + public void newInstallation() { + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(); + assertThat("Detached plugins should exist", detachedPlugins, not(empty())); + assertThat("Detached plugins should not be installed on a new instance", + getInstalledDetachedPlugins(r, detachedPlugins), empty()); + assertNoFailedPlugins(r); + }); + rr.then(r -> { + List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(); + assertThat("Detached plugins should exist", detachedPlugins, not(empty())); + assertThat("Detached plugins should not be installed after restarting", + getInstalledDetachedPlugins(r, detachedPlugins), empty()); + assertNoFailedPlugins(r); + }); + } + + private List getInstalledDetachedPlugins(JenkinsRule r, List detachedPlugins) { + PluginManager pluginManager = r.jenkins.getPluginManager(); + List installedPlugins = new ArrayList<>(); + for (DetachedPlugin plugin : detachedPlugins) { + PluginWrapper wrapper = pluginManager.getPlugin(plugin.getShortName()); + if (wrapper != null) { + installedPlugins.add(wrapper); + assertTrue("Detached plugins should be active if installed", wrapper.isActive()); + assertThat("Detached plugins should not have dependency errors", wrapper.getDependencyErrors(), empty()); + } + } + return installedPlugins; + } + + private void assertNoFailedPlugins(JenkinsRule r) { + assertThat("Detached plugins and their dependencies should not fail to install", + r.jenkins.getPluginManager().getFailedPlugins(), empty()); + } + +} diff --git a/test/src/test/java/jenkins/install/SetupWizardRestartTest.java b/test/src/test/java/jenkins/install/SetupWizardRestartTest.java new file mode 100644 index 0000000000..474c1772f9 --- /dev/null +++ b/test/src/test/java/jenkins/install/SetupWizardRestartTest.java @@ -0,0 +1,50 @@ +package jenkins.install; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import hudson.Main; +import jenkins.model.Jenkins; + +public class SetupWizardRestartTest { + @Rule + public RestartableJenkinsRule rr = new RestartableJenkinsRule(); + + @Issue("JENKINS-47439") + @Test + public void restartKeepsSetupWizardState() { + rr.addStep(new Statement() { + @Override + public void evaluate() throws IOException { + // Modify state so that we get into the same conditions as a real start + Main.isUnitTest = false; + FileUtils.write(InstallUtil.getLastExecVersionFile(), ""); + Jenkins j = rr.j.getInstance(); + // Re-evaluate current state based on the new context + InstallUtil.proceedToNextStateFrom(InstallState.UNKNOWN); + assertEquals("Unexpected install state", InstallState.NEW, j.getInstallState()); + assertTrue("Expecting setup wizard filter to be up", j.getSetupWizard().hasSetupWizardFilter()); + InstallUtil.saveLastExecVersion(); + } + }); + // Check that the state is retained after a restart + rr.addStep(new Statement() { + @Override + public void evaluate() { + Jenkins j = rr.j.getInstance(); + assertEquals("Unexpected install state", InstallState.NEW, j.getInstallState()); + assertTrue("Expecting setup wizard filter to be up after restart", j.getSetupWizard().hasSetupWizardFilter()); + } + }); + } + +} diff --git a/test/src/test/java/jenkins/model/JenkinsTest.java b/test/src/test/java/jenkins/model/JenkinsTest.java index 2e488bd80b..a68ee0a4d6 100644 --- a/test/src/test/java/jenkins/model/JenkinsTest.java +++ b/test/src/test/java/jenkins/model/JenkinsTest.java @@ -314,20 +314,23 @@ public class JenkinsTest { grant(Jenkins.READ).everywhere().to("bob"). grantWithoutImplication(Jenkins.ADMINISTER, Jenkins.READ).everywhere().to("charlie")); WebClient wc = j.createWebClient(); - wc.login("alice"); + + wc.withBasicApiToken(User.getById("alice", true)); wc.goTo("script"); wc.assertFails("script?script=System.setProperty('hack','me')", HttpURLConnection.HTTP_BAD_METHOD); assertNull(System.getProperty("hack")); WebRequest req = new WebRequest(new URL(wc.getContextPath() + "script?script=System.setProperty('hack','me')"), HttpMethod.POST); - wc.getPage(wc.addCrumb(req)); + wc.getPage(req); assertEquals("me", System.getProperty("hack")); wc.assertFails("scriptText?script=System.setProperty('hack','me')", HttpURLConnection.HTTP_BAD_METHOD); req = new WebRequest(new URL(wc.getContextPath() + "scriptText?script=System.setProperty('huck','you')"), HttpMethod.POST); - wc.getPage(wc.addCrumb(req)); + wc.getPage(req); assertEquals("you", System.getProperty("huck")); - wc.login("bob"); + + wc.withBasicApiToken(User.getById("bob", true)); wc.assertFails("script", HttpURLConnection.HTTP_FORBIDDEN); - wc.login("charlie"); + + wc.withBasicApiToken(User.getById("charlie", true)); wc.assertFails("script", HttpURLConnection.HTTP_FORBIDDEN); } @@ -338,18 +341,22 @@ public class JenkinsTest { grant(Jenkins.ADMINISTER).everywhere().to("alice"). grant(Jenkins.READ).everywhere().to("bob"). grantWithoutImplication(Jenkins.ADMINISTER, Jenkins.READ).everywhere().to("charlie")); + WebClient wc = j.createWebClient(); - wc.login("alice"); + + wc.withBasicApiToken(User.getById("alice", true)); wc.assertFails("eval", HttpURLConnection.HTTP_BAD_METHOD); assertEquals("3", eval(wc)); - wc.login("bob"); + + wc.withBasicApiToken(User.getById("bob", true)); try { eval(wc); fail("bob has only READ"); } catch (FailingHttpStatusCodeException e) { assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getStatusCode()); } - wc.login("charlie"); + + wc.withBasicApiToken(User.getById("charlie", true)); try { eval(wc); fail("charlie has ADMINISTER but not RUN_SCRIPTS"); @@ -358,7 +365,7 @@ public class JenkinsTest { } } private String eval(WebClient wc) throws Exception { - WebRequest req = new WebRequest(wc.createCrumbedUrl("eval"), HttpMethod.POST); + WebRequest req = new WebRequest(new URL(wc.getContextPath() + "eval"), HttpMethod.POST); req.setEncodingType(null); req.setRequestBody("${1+2}"); return wc.getPage(req).getWebResponse().getContentAsString(); diff --git a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java new file mode 100644 index 0000000000..c25e276b4e --- /dev/null +++ b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java @@ -0,0 +1,99 @@ +package jenkins.model; + +import hudson.Functions; +import hudson.Util; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Run; +import java.io.File; +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.FailureBuilder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class PeepholePermalinkTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Basic operation of the permalink generation. + */ + @Test + public void basics() throws Exception { + Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + File lsb = new File(p.getBuildDir(), "lastSuccessfulBuild"); + File lfb = new File(p.getBuildDir(), "lastFailedBuild"); + + assertLink(lsb, b1); + + // now another build that fails + p.getBuildersList().add(new FailureBuilder()); + FreeStyleBuild b2 = p.scheduleBuild2(0).get(); + + assertLink(lsb, b1); + assertLink(lfb, b2); + + // one more build and this time it succeeds + p.getBuildersList().clear(); + FreeStyleBuild b3 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + assertLink(lsb, b3); + assertLink(lfb, b2); + + // delete b3 and symlinks should update properly + b3.delete(); + assertLink(lsb, b1); + assertLink(lfb, b2); + + b1.delete(); + assertLink(lsb, null); + assertLink(lfb, b2); + + b2.delete(); + assertLink(lsb, null); + assertLink(lfb, null); + } + + private void assertLink(File symlink, Run build) throws Exception { + assertEquals(build == null ? "-1" : Integer.toString(build.getNumber()), Util.resolveSymlink(symlink)); + } + + /** + * job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we + * used to generate should still work + */ + @Test + public void legacyCompatibility() throws Exception { + Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); + + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + for (String n : new String[] {"lastStable", "lastSuccessful"}) { + // test if they both point to b1 + assertEquals(new File(p.getRootDir(), n + "/build.xml").length(), new File(b1.getRootDir(), "build.xml").length()); + } + } + + @Test + @Issue("JENKINS-19034") + public void rebuildBuildNumberPermalinks() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + File f = new File(p.getBuildDir(), "1"); + // assertTrue(Util.isSymlink(f)) + f.delete(); + PeepholePermalink link = (PeepholePermalink) p.getPermalinks().stream().filter(l -> l instanceof PeepholePermalink).findAny().get(); + link.updateCache(p, b); + assertTrue("build symlink hasn't been restored", f.exists()); + } + +} diff --git a/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java new file mode 100644 index 0000000000..eef3b21dfd --- /dev/null +++ b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java @@ -0,0 +1,146 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlFormUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import hudson.model.User; +import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.HttpResponses; +import hudson.util.Scrambler; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.HttpResponse; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class ApiCrumbExclusionTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + private WebClient wc; + + @Test + @Issue("JENKINS-22474") + public void callUsingApiTokenDoesNotRequireCSRFToken() throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setCrumbIssuer(null); + User foo = User.get("foo"); + + wc = j.createWebClient(); + + // API Token + wc.withBasicApiToken(foo); + makeRequestAndVerify("foo"); + + // Basic auth using password + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndVerify("foo"); + + wc = j.createWebClient(); + wc.login("foo"); + checkWeCanChangeMyDescription(200); + + wc = j.createWebClient(); + j.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); + + // even with crumbIssuer enabled, we are not required to send a CSRF token when using API token + wc.withBasicApiToken(foo); + makeRequestAndVerify("foo"); + + // Basic auth using password requires crumb + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndFail(403); + + wc = j.createWebClient(); + wc.login("foo"); + checkWeCanChangeMyDescription(200); + } + + private void makeRequestAndVerify(String expected) throws IOException, SAXException { + WebRequest req = new WebRequest(new URL(j.getURL(), "test-post")); + req.setHttpMethod(HttpMethod.POST); + req.setEncodingType(null); + Page p = wc.getPage(req); + assertEquals(expected, p.getWebResponse().getContentAsString()); + } + + private void makeRequestAndFail(int expectedCode) throws IOException, SAXException { + try { + makeRequestAndVerify("-"); + fail(); + } catch (FailingHttpStatusCodeException e) { + assertEquals(expectedCode, e.getStatusCode()); + } + } + + private void checkWeCanChangeMyDescription(int expectedCode) throws IOException, SAXException { + HtmlPage page = wc.goTo("me/configure"); + HtmlForm form = page.getFormByName("config"); + form.getTextAreaByName("_.description").setText("random description: " + Math.random()); + + Page result = HtmlFormUtil.submit(form); + assertEquals(expectedCode, result.getWebResponse().getStatusCode()); + } + + @TestExtension + public static class WhoAmI implements UnprotectedRootAction { + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "test-post"; + } + + public HttpResponse doIndex() { + User u = User.current(); + return HttpResponses.text(u != null ? u.getId() : "anonymous"); + } + } +} diff --git a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java index f5e746d161..8737d26974 100644 --- a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java +++ b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java @@ -121,7 +121,6 @@ public class ApiTokenPropertyTest { wc.getOptions().setThrowExceptionOnFailingStatusCode(true); WebRequest request = new WebRequest(new URL(j.getURL().toString() + foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken"), HttpMethod.POST); - wc.addCrumb(request); HtmlPage res = wc.getPage(request); // TODO This nicer alternative requires https://github.com/jenkinsci/jenkins/pull/2268 or similar to work diff --git a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java index 449293ae10..0b471dd2b3 100644 --- a/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java +++ b/test/src/test/java/jenkins/security/BasicHeaderProcessorTest.java @@ -1,15 +1,13 @@ package jenkins.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebRequest; +import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; import hudson.model.User; import hudson.util.HttpResponses; -import hudson.util.Scrambler; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -20,6 +18,10 @@ import org.xml.sax.SAXException; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.junit.Assert.*; /** * @author Kohsuke Kawaguchi @@ -30,78 +32,82 @@ public class BasicHeaderProcessorTest { private WebClient wc; + private SpySecurityListener spySecurityListener; + + @Before + public void prepareListeners(){ + //TODO simplify using #3021 into ExtensionList.lookupSingleton(SpySecurityListener.class) + this.spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + } + /** * Tests various ways to send the Basic auth. */ @Test public void testVariousWaysToCall() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - User foo = User.get("foo"); - User bar = User.get("bar"); + User foo = User.getById("foo", true); + User.getById("bar", true); wc = j.createWebClient(); // call without authentication - makeRequestWithAuthAndVerify(null, "anonymous"); + makeRequestAndVerify("anonymous"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // call with API token - ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); - final String token = t.getApiToken(); - makeRequestWithAuthAndVerify("foo:"+token, "foo"); + wc = j.createWebClient(); + wc.withBasicApiToken("foo"); + makeRequestAndVerify("foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with invalid API token - makeRequestAndFail("foo:abcd"+token); + wc = j.createWebClient(); + wc.withBasicCredentials("foo", "abcd" + foo.getProperty(ApiTokenProperty.class).getApiToken()); + makeRequestAndFail(); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); // call with password - makeRequestWithAuthAndVerify("foo:foo", "foo"); + wc = j.createWebClient(); + wc.withBasicCredentials("foo"); + makeRequestAndVerify("foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with incorrect password - makeRequestAndFail("foo:bar"); - + wc = j.createWebClient(); + wc.withBasicCredentials("foo", "bar"); + makeRequestAndFail(); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); + wc = j.createWebClient(); wc.login("bar"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("bar")); + spySecurityListener.loggedInCalls.assertLastEventIsAndThenRemoveIt("bar"); // if the session cookie is valid, then basic header won't be needed - makeRequestWithAuthAndVerify(null, "bar"); + makeRequestAndVerify("bar"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // if the session cookie is valid, and basic header is set anyway login should not fail either - makeRequestWithAuthAndVerify("bar:bar", "bar"); + wc.withBasicCredentials("bar"); + makeRequestAndVerify("bar"); + spySecurityListener.authenticatedCalls.assertNoNewEvents(); + spySecurityListener.failedToAuthenticateCalls.assertNoNewEvents(); // but if the password is incorrect, it should fail, instead of silently logging in as the user indicated by session - makeRequestAndFail("foo:bar"); + wc.withBasicCredentials("foo", "bar"); + makeRequestAndFail(); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); } - private void makeRequestAndFail(String userAndPass) throws IOException, SAXException { - makeRequestWithAuthCodeAndFail(encrypt("Basic", userAndPass)); - } - - private String encrypt(String prefix, String userAndPass) { - if (userAndPass==null) { - return null; - } - return prefix+" "+Scrambler.scramble(userAndPass); + private void makeRequestAndFail() throws IOException, SAXException { + makeRequestWithAuthCodeAndFail(null); } - private void makeRequestWithAuthCodeAndFail(String authCode) throws IOException, SAXException { - try { - makeRequestWithAuthCodeAndVerify(authCode, "-"); - fail(); - } catch (FailingHttpStatusCodeException e) { - assertEquals(401, e.getStatusCode()); - } - } - - private void makeRequestWithAuthAndVerify(String userAndPass, String username) throws IOException, SAXException { - makeRequestWithAuthCodeAndVerify(encrypt("Basic", userAndPass), username); - } - - private void makeRequestWithAuthCodeAndVerify(String authCode, String expected) throws IOException, SAXException { - WebRequest req = new WebRequest(new URL(j.getURL(),"test")); - req.setEncodingType(null); - if (authCode!=null) - req.setAdditionalHeader("Authorization", authCode); - Page p = wc.getPage(req); - assertEquals(expected, p.getWebResponse().getContentAsString().trim()); + private void makeRequestAndVerify(String expectedLogin) throws IOException, SAXException { + makeRequestWithAuthCodeAndVerify(null, expectedLogin); } @Test @@ -111,25 +117,54 @@ public class BasicHeaderProcessorTest { wc = j.createWebClient(); String[] basicCandidates = {"Basic", "BASIC", "basic", "bASIC"}; - + for (String prefix : basicCandidates) { // call with API token ApiTokenProperty t = foo.getProperty(ApiTokenProperty.class); final String token = t.getApiToken(); - String authCode1 = encrypt(prefix,"foo:"+token); + String authCode1 = encode(prefix,"foo:"+token); makeRequestWithAuthCodeAndVerify(authCode1, "foo"); - + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); + // call with invalid API token - String authCode2 = encrypt(prefix,"foo:abcd"+token); + String authCode2 = encode(prefix,"foo:abcd"+token); makeRequestWithAuthCodeAndFail(authCode2); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); // call with password - String authCode3 = encrypt(prefix,"foo:foo"); + String authCode3 = encode(prefix,"foo:foo"); makeRequestWithAuthCodeAndVerify(authCode3, "foo"); + spySecurityListener.authenticatedCalls.assertLastEventIsAndThenRemoveIt(u -> u.getUsername().equals("foo")); // call with incorrect password - String authCode4 = encrypt(prefix,"foo:bar"); + String authCode4 = encode(prefix,"foo:bar"); makeRequestWithAuthCodeAndFail(authCode4); + spySecurityListener.failedToAuthenticateCalls.assertLastEventIsAndThenRemoveIt("foo"); + } + } + + private String encode(String prefix, String userAndPass) { + if (userAndPass==null) { + return null; + } + return prefix + " " + Base64.getEncoder().encodeToString(userAndPass.getBytes(StandardCharsets.UTF_8)); + } + + private void makeRequestWithAuthCodeAndVerify(String authCode, String expectedLogin) throws IOException, SAXException { + WebRequest req = new WebRequest(new URL(j.getURL(),"test")); + req.setEncodingType(null); + if (authCode!=null) + req.setAdditionalHeader("Authorization", authCode); + Page p = wc.getPage(req); + assertEquals(expectedLogin, p.getWebResponse().getContentAsString()); + } + + private void makeRequestWithAuthCodeAndFail(String authCode) throws IOException, SAXException { + try { + makeRequestWithAuthCodeAndVerify(authCode, "-"); + fail(); + } catch (FailingHttpStatusCodeException e) { + assertEquals(401, e.getStatusCode()); } } @@ -152,7 +187,10 @@ public class BasicHeaderProcessorTest { public HttpResponse doIndex() { User u = User.current(); - return HttpResponses.plainText(u!=null ? u.getId() : "anonymous"); + return HttpResponses.text(u!=null ? u.getId() : "anonymous"); } } + + @TestExtension + public static class SpySecurityListenerImpl extends SpySecurityListener {} } diff --git a/test/src/test/java/jenkins/security/Security380Test.java b/test/src/test/java/jenkins/security/Security380Test.java index 5d3475df92..c7b3e90a6f 100644 --- a/test/src/test/java/jenkins/security/Security380Test.java +++ b/test/src/test/java/jenkins/security/Security380Test.java @@ -67,6 +67,7 @@ public class Security380Test { JenkinsRule.WebClient wc = j.createWebClient(); Page page = wc.goTo("listJobs", "text/plain"); + // return "0\r\n" Assert.assertEquals("expect 0 items", "0", page.getWebResponse().getContentAsString().trim()); } diff --git a/test/src/test/java/jenkins/security/SpySecurityListener.java b/test/src/test/java/jenkins/security/SpySecurityListener.java new file mode 100644 index 0000000000..0d9731cc90 --- /dev/null +++ b/test/src/test/java/jenkins/security/SpySecurityListener.java @@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import org.acegisecurity.userdetails.UserDetails; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +//TODO temporary solution, should be moved to Jenkins Test Harness project +/** + * Mean to be included in test classes to provide a way to spy on the SecurityListener events + * @see jenkins.security.BasicHeaderProcessorTest.SpySecurityListenerImpl + * @see hudson.security.CliAuthenticationTest.SpySecurityListenerImpl + */ +public abstract class SpySecurityListener extends SecurityListener { + public final EventQueue authenticatedCalls = new EventQueue<>(); + public final EventQueue failedToAuthenticateCalls = new EventQueue<>(); + public final EventQueue loggedInCalls = new EventQueue<>(); + public final EventQueue failedToLogInCalls = new EventQueue<>(); + public final EventQueue loggedOutCalls = new EventQueue<>(); + + public void clearPreviousCalls(){ + this.authenticatedCalls.clear(); + this.failedToAuthenticateCalls.clear(); + this.loggedInCalls.clear(); + this.failedToLogInCalls.clear(); + this.loggedOutCalls.clear(); + } + + @Override + protected void authenticated(@Nonnull UserDetails details) { + this.authenticatedCalls.add(details); + } + + @Override + protected void failedToAuthenticate(@Nonnull String username) { + this.failedToAuthenticateCalls.add(username); + } + + @Override + protected void loggedIn(@Nonnull String username) { + this.loggedInCalls.add(username); + } + + @Override + protected void failedToLogIn(@Nonnull String username) { + this.failedToLogInCalls.add(username); + } + + @Override + protected void loggedOut(@Nonnull String username) { + this.loggedOutCalls.add(username); + + } + + public static class EventQueue { + private final List eventList = new ArrayList<>(); + + private EventQueue add(T t){ + eventList.add(t); + return this; + } + + public void assertLastEventIsAndThenRemoveIt(T expected){ + assertLastEventIsAndThenRemoveIt(expected::equals); + } + + public void assertLastEventIsAndThenRemoveIt(Predicate predicate){ + if(eventList.isEmpty()){ + fail("event list is empty"); + } + + T t = eventList.remove(eventList.size() - 1); + assertTrue(predicate.test(t)); + eventList.clear(); + } + + public void assertNoNewEvents(){ + assertEquals("list of event should be empty", eventList.size(), 0); + } + + public void clear(){ + eventList.clear(); + } + } +} + diff --git a/test/src/test/java/lib/form/ApplyButtonTest.java b/test/src/test/java/lib/form/ApplyButtonTest.java new file mode 100644 index 0000000000..028e135da0 --- /dev/null +++ b/test/src/test/java/lib/form/ApplyButtonTest.java @@ -0,0 +1,42 @@ +package lib.form; + +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.markup.RawHtmlMarkupFormatter; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import org.apache.commons.io.IOUtils; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class ApplyButtonTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Editing code mirror should still gets reflected when you click apply. + */ + @Test + @Issue("JENKINS-18436") + public void editDescription() throws Exception { + j.jenkins.setMarkupFormatter(RawHtmlMarkupFormatter.INSTANCE); // need something using CodeMirror + FreeStyleProject p = j.createFreeStyleProject(); + FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + HtmlPage config = j.createWebClient().getPage(b, "configure"); + HtmlForm form = config.getFormByName("config"); + // HtmlUnit doesn't have JSON, so we need to emulate one + config.executeJavaScript(IOUtils.toString(ApplyButtonTest.class.getResource("JSON.js"))); + // it's hard to emulate the keytyping, so we just set the value into codemirror and test if this gets + // reflected back into TEXTAREA + config.executeJavaScript("document.getElementsByTagName('TEXTAREA')[0].codemirrorObject.setLine(0,'foobar')"); + j.getButtonByCaption(form, "Apply").click(); + + assertEquals("foobar", b.getDescription()); + } + +} diff --git a/test/src/test/java/lib/form/PasswordTest.java b/test/src/test/java/lib/form/PasswordTest.java index 0e3ed7c783..8eef7d40ff 100644 --- a/test/src/test/java/lib/form/PasswordTest.java +++ b/test/src/test/java/lib/form/PasswordTest.java @@ -27,16 +27,9 @@ import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.html.HtmlInput; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput; -import hudson.Extension; import hudson.cli.CopyJobCommand; import hudson.cli.GetJobCommand; -import hudson.model.Describable; -import hudson.model.Descriptor; -import hudson.model.FreeStyleProject; -import hudson.model.Item; -import hudson.model.JobProperty; -import hudson.model.JobPropertyDescriptor; -import hudson.model.User; +import hudson.model.*; import hudson.util.FormValidation; import hudson.util.Secret; import java.io.ByteArrayOutputStream; @@ -51,8 +44,10 @@ import org.acegisecurity.Authentication; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; -import org.jvnet.hudson.test.HudsonTestCase; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; @@ -62,25 +57,43 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; -public class PasswordTest extends HudsonTestCase implements Describable { - public Secret secret; +public class PasswordTest { - public void test1() throws Exception { - secret = Secret.fromString("secret"); - HtmlPage p = createWebClient().goTo("self/test1"); + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void secretNotPlainText() throws Exception { + SecretNotPlainText.secret = Secret.fromString("secret"); + HtmlPage p = j.createWebClient().goTo("secretNotPlainText"); String value = ((HtmlInput)p.getElementById("password")).getValueAttribute(); assertFalse("password shouldn't be plain text",value.equals("secret")); assertEquals("secret",Secret.fromString(value).getPlainText()); } - public DescriptorImpl getDescriptor() { - return jenkins.getDescriptorByType(DescriptorImpl.class); + @TestExtension("secretNotPlainText") + public static class SecretNotPlainText implements RootAction { + + public static Secret secret; + + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "secretNotPlainText"; + } } - @Extension - public static final class DescriptorImpl extends Descriptor {} - @Issue({"SECURITY-266", "SECURITY-304"}) + @Test public void testExposedCiphertext() throws Exception { boolean saveEnabled = Item.EXTENDED_READ.getEnabled(); Item.EXTENDED_READ.setEnabled(true); @@ -93,24 +106,28 @@ public class PasswordTest extends HudsonTestCase implements Describable { + + String text1, text2; + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + public FormValidation doCheckText1(@QueryParameter String value) { + this.text1 = value; + return FormValidation.ok(); + } + + public FormValidation doCheckText2(@QueryParameter String text1) { + this.text2 = "Received " + text1; + return FormValidation.ok(); + } + } + + } + + @Issue("JENKINS-27505") + @Test + public void text() throws Exception { + T1: { + String TEXT_TO_TEST = "some\nvalue\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + + // test for a textarea beginning with a empty line. + T2: { + String TEXT_TO_TEST = "\nbegin\n\nwith\nempty\nline\n\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + + // test for a textarea beginning with two empty lines. + T3: { + String TEXT_TO_TEST = "\n\nbegin\n\nwith\ntwo\nempty\nline\n\n"; + FreeStyleProject p = j.createFreeStyleProject(); + TextareaTestBuilder target = new TextareaTestBuilder(TEXT_TO_TEST); + p.getBuildersList().add(target); + j.configRoundtrip(p); + j.assertEqualDataBoundBeans(target, p.getBuildersList().get(TextareaTestBuilder.class)); + } + } + + public static class TextareaTestBuilder extends Builder { + + private String text; + + @DataBoundConstructor + public TextareaTestBuilder(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + } + + } + +} diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml new file mode 100644 index 0000000000..6fcea351c2 --- /dev/null +++ b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins1/config.xml @@ -0,0 +1,34 @@ + + + + 1.550 + 2 + NORMAL + true + + + false + + ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} + ${ITEM_ROOTDIR}/builds + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + \ No newline at end of file diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion new file mode 100644 index 0000000000..415b19fc36 --- /dev/null +++ b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2/jenkins.install.InstallUtil.lastExecVersion @@ -0,0 +1 @@ +2.0 \ No newline at end of file diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithNewerDependency.zip b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithNewerDependency.zip new file mode 100644 index 0000000000..557841273a Binary files /dev/null and b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithNewerDependency.zip differ diff --git a/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip new file mode 100644 index 0000000000..b85561cf0d Binary files /dev/null and b/test/src/test/resources/jenkins/install/LoadDetachedPluginsTest/upgradeFromJenkins2WithOlderDependency.zip differ diff --git a/test/src/test/resources/lib/form/PasswordTest/test1.jelly b/test/src/test/resources/lib/form/PasswordTest/SecretNotPlainText/index.jelly similarity index 91% rename from test/src/test/resources/lib/form/PasswordTest/test1.jelly rename to test/src/test/resources/lib/form/PasswordTest/SecretNotPlainText/index.jelly index 94fc58a272..c0ecc9595b 100644 --- a/test/src/test/resources/lib/form/PasswordTest/test1.jelly +++ b/test/src/test/resources/lib/form/PasswordTest/SecretNotPlainText/index.jelly @@ -26,7 +26,7 @@ THE SOFTWARE. Config page --> - + diff --git a/war/pom.xml b/war/pom.xml index 3d7ea1e54e..9147f0ecc3 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 2.94-SNAPSHOT + 2.97-SNAPSHOT jenkins-war @@ -130,7 +130,7 @@ THE SOFTWARE. org.jenkins-ci.modules sshd - 2.0 + 2.3 org.jenkins-ci.ui diff --git a/war/src/main/webapp/help/parameter/trim_zh_CN.html b/war/src/main/webapp/help/parameter/trim_zh_CN.html new file mode 100644 index 0000000000..a3c56e13b7 --- /dev/null +++ b/war/src/main/webapp/help/parameter/trim_zh_CN.html @@ -0,0 +1,3 @@ +
+ 清除字符串前后的清白字符。 +