/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2015 Christopher Simons
*
* 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.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.WebAssert;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.TextPage;
import hudson.Functions;
import hudson.util.TextFile;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.text.MessageFormat;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import jenkins.model.ProjectNamingStrategy;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.FailureBuilder;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.RunLoadCounter;
import org.jvnet.hudson.test.recipes.LocalData;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeFalse;
/**
* @author Kohsuke Kawaguchi
*/
public class JobTest {
@Rule public JenkinsRule j = new JenkinsRule();
@SuppressWarnings("unchecked")
@Test public void jobPropertySummaryIsShownInMainPage() throws Exception {
AbstractProject project = j.createFreeStyleProject();
project.addProperty(new JobPropertyImpl("NeedleInPage"));
HtmlPage page = j.createWebClient().getPage(project);
WebAssert.assertTextPresent(page, "NeedleInPage");
}
@Test public void buildNumberSynchronization() throws Exception {
AbstractProject project = j.createFreeStyleProject();
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch stopLatch = new CountDownLatch(2);
BuildNumberSyncTester test1 = new BuildNumberSyncTester(project, startLatch, stopLatch, true);
BuildNumberSyncTester test2 = new BuildNumberSyncTester(project, startLatch, stopLatch, false);
new Thread(test1).start();
new Thread(test2).start();
startLatch.countDown();
stopLatch.await();
assertTrue(test1.message, test2.passed);
assertTrue(test2.message, test2.passed);
}
public static class BuildNumberSyncTester implements Runnable {
private final AbstractProject p;
private final CountDownLatch start;
private final CountDownLatch stop;
private final boolean assign;
String message;
boolean passed;
BuildNumberSyncTester(AbstractProject p, CountDownLatch l1, CountDownLatch l2, boolean b) {
this.p = p;
this.start = l1;
this.stop = l2;
this.assign = b;
this.message = null;
this.passed = false;
}
public void run() {
try {
start.await();
for (int i = 0; i < 100; i++) {
int buildNumber = -1, savedBuildNumber = -1;
TextFile f;
synchronized (p) {
if (assign) {
buildNumber = p.assignBuildNumber();
f = p.getNextBuildNumberFile();
if (f == null) {
this.message = "Could not get build number file";
this.passed = false;
return;
}
savedBuildNumber = Integer.parseInt(f.readTrim());
if (buildNumber != (savedBuildNumber-1)) {
this.message = "Build numbers don't match (" + buildNumber + ", " + (savedBuildNumber-1) + ")";
this.passed = false;
return;
}
} else {
buildNumber = p.getNextBuildNumber() + 100;
p.updateNextBuildNumber(buildNumber);
f = p.getNextBuildNumberFile();
if (f == null) {
this.message = "Could not get build number file";
this.passed = false;
return;
}
savedBuildNumber = Integer.parseInt(f.readTrim());
if (buildNumber != savedBuildNumber) {
this.message = "Build numbers don't match (" + buildNumber + ", " + savedBuildNumber + ")";
this.passed = false;
return;
}
}
}
}
this.passed = true;
}
catch (InterruptedException e) {}
catch (IOException e) {
fail("Failed to assign build number");
}
finally {
stop.countDown();
}
}
}
@SuppressWarnings("unchecked")
public static class JobPropertyImpl extends JobProperty<Job<?,?>> {
public static DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private final String testString;
public JobPropertyImpl(String testString) {
this.testString = testString;
}
public String getTestString() {
return testString;
}
@Override
public JobPropertyDescriptor getDescriptor() {
return DESCRIPTOR;
}
private static final class DescriptorImpl extends JobPropertyDescriptor {
public String getDisplayName() {
return "";
}
}
}
@LocalData
@Test public void readPermission() throws Exception {
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
// Verify we can access both URLs:
wc.goTo("job/testJob/");
wc.goTo("jobCaseInsensitive/TESTJOB/");
}
@LocalData
@Test public void configDotXmlPermission() throws Exception {
j.jenkins.setCrumbIssuer(null);
JenkinsRule.WebClient wc = j.createWebClient();
boolean saveEnabled = Item.EXTENDED_READ.getEnabled();
Item.EXTENDED_READ.setEnabled(true);
try {
wc.assertFails("job/testJob/config.xml", HttpURLConnection.HTTP_FORBIDDEN);
wc.login("alice"); // 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)
tryConfigDotXml(wc, 500, "Config perm should imply EXTENDED_READ");
wc.login("charlie"); // Has only EXTENDED_READ permission
tryConfigDotXml(wc, 403, "No permission, should get 403");
} finally {
Item.EXTENDED_READ.setEnabled(saveEnabled);
}
}
private static void tryConfigDotXml(JenkinsRule.WebClient wc, int status, String msg) throws Exception {
// Verify we can GET the config.xml:
wc.goTo("job/testJob/config.xml", "application/xml");
// This page is a simple form to POST to /job/testJob/config.xml
// But it posts invalid data so we expect 500 if we have permission, 403 if not
HtmlPage page = wc.goTo("userContent/post.html");
try {
HtmlFormUtil.submit(page.getForms().get(0));
fail("Expected exception: " + msg);
} catch (FailingHttpStatusCodeException expected) {
assertEquals(msg, status, expected.getStatusCode());
}
wc.goTo("logout");
}
@LocalData @Issue("JENKINS-6371")
@Test public void getArtifactsUpTo() throws Exception {
// There was a bug where intermediate directories were counted,
// so too few artifacts were returned.
Run r = j.jenkins.getItemByFullName("testJob", Job.class).getLastCompletedBuild();
assertEquals(3, r.getArtifacts().size());
assertEquals(3, r.getArtifactsUpTo(3).size());
assertEquals(2, r.getArtifactsUpTo(2).size());
assertEquals(1, r.getArtifactsUpTo(1).size());
}
@Issue("JENKINS-10182")
@Test public void emptyDescriptionReturnsEmptyPage() throws Exception {
// A NPE was thrown if a job had a null (empty) description.
JenkinsRule.WebClient wc = j.createWebClient();
FreeStyleProject project = j.createFreeStyleProject("project");
project.setDescription("description");
assertEquals("description", ((TextPage) wc.goTo("job/project/description", "text/plain")).getContent());
project.setDescription(null);
assertEquals("", ((TextPage) wc.goTo("job/project/description", "text/plain")).getContent());
}
@Test public void projectNamingStrategy() throws Exception {
j.jenkins.setProjectNamingStrategy(new ProjectNamingStrategy.PatternProjectNamingStrategy("DUMMY.*", false));
final FreeStyleProject p = j.createFreeStyleProject("DUMMY_project");
assertNotNull("no project created", p);
try {
j.createFreeStyleProject("project");
fail("should not get here, the project name is not allowed, therefore the creation must fail!");
} catch (Failure e) {
// OK, expected
}finally{
// set it back to the default naming strategy, otherwise all other tests would fail to create jobs!
j.jenkins.setProjectNamingStrategy(ProjectNamingStrategy.DEFAULT_NAMING_STRATEGY);
}
j.createFreeStyleProject("project");
}
@Issue("JENKINS-16023")
@Test public void getLastFailedBuild() throws Exception {
final FreeStyleProject p = j.createFreeStyleProject();
RunLoadCounter.prepare(p);
p.getBuildersList().add(new FailureBuilder());
j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get());
j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get());
j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get());
p.getBuildersList().remove(FailureBuilder.class);
j.assertBuildStatusSuccess(p.scheduleBuild2(0));
j.assertBuildStatusSuccess(p.scheduleBuild2(0));
j.assertBuildStatusSuccess(p.scheduleBuild2(0));
assertEquals(6, p.getLastSuccessfulBuild().getNumber());
assertEquals(3, RunLoadCounter.assertMaxLoads(p, 1, new Callable<Integer>() {
@Override public Integer call() throws Exception {
return p.getLastFailedBuild().getNumber();
}
}).intValue());
}
@Issue("JENKINS-19764")
@Test public void testRenameWithCustomBuildsDirWithSubdir() throws Exception {
j.jenkins.setRawBuildsDir("${JENKINS_HOME}/builds/${ITEM_FULL_NAME}/builds");
final FreeStyleProject p = j.createFreeStyleProject();
p.scheduleBuild2(0).get();
p.renameTo("different-name");
}
@Issue("JENKINS-30502")
@Test
public void testRenameTrimsLeadingSpace() throws Exception {
tryRename("myJob1", " foo", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testRenameTrimsTrailingSpace() throws Exception {
tryRename("myJob2", "foo ", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testAllowTrimmingByUser() throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename("myJob3 ", "myJob3", "myJob3", false);
}
@Issue("JENKINS-30502")
@Test
public void testRenameWithLeadingSpaceTrimsLeadingSpace() throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename(" myJob4", " foo", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testRenameWithLeadingSpaceTrimsTrailingSpace()
throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename(" myJob5", "foo ", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testRenameWithTrailingSpaceTrimsTrailingSpace()
throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename("myJob6 ", "foo ", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testRenameWithTrailingSpaceTrimsLeadingSpace()
throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename("myJob7 ", " foo", "foo", false);
}
@Issue("JENKINS-30502")
@Test
public void testDoNotAutoTrimExistingUntrimmedNames() throws Exception {
assumeFalse("Unix-only test.", Functions.isWindows());
tryRename("myJob8 ", "myJob8 ", null, true);
}
private void tryRename(String initialName, String submittedName,
String correctResult, boolean shouldSkipConfirm) throws Exception {
j.jenkins.setCrumbIssuer(null);
FreeStyleProject job = j.createFreeStyleProject(initialName);
WebClient wc = j.createWebClient();
HtmlForm form = wc.getPage(job, "configure").getFormByName("config");
form.getInputByName("name").setValueAttribute(submittedName);
HtmlPage resultPage = j.submit(form);
String urlTemplate;
if (shouldSkipConfirm) {
urlTemplate = "/job/{0}/";
} else {
urlTemplate = "/job/{0}/rename?newName={1}";
}
String urlString = MessageFormat.format(
urlTemplate, initialName, correctResult).replace(" ", "%20");
assertThat(resultPage.getUrl().toString(), endsWith(urlString));
}
}