/*
* The MIT License
*
* Copyright 2013 CloudBees.
*
* 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 com.cloudbees.hudson.plugins.folder;
import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider;
import com.cloudbees.plugins.credentials.domains.DomainCredentials;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput;
import hudson.model.Actionable;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.ListView;
import hudson.model.User;
import hudson.model.listeners.ItemListener;
import hudson.search.SearchItem;
import hudson.security.ACL;
import hudson.security.WhoAmI;
import hudson.security.Permission;
import hudson.security.ProjectMatrixAuthorizationStrategy;
import hudson.tasks.BuildTrigger;
import hudson.views.BuildButtonColumn;
import hudson.views.JobColumn;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import jenkins.model.Jenkins;
import jenkins.util.Timer;
import org.acegisecurity.AccessDeniedException;
import static org.hamcrest.Matchers.*;
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;
import org.jvnet.hudson.test.SleepBuilder;
import org.jvnet.hudson.test.TestExtension;
import org.jvnet.hudson.test.recipes.LocalData;
public class FolderTest {
@Rule public JenkinsRule r = new JenkinsRule();
/**
* Tests rename operation.
*/
@Test public void rename() throws Exception {
Folder f = createFolder();
f.setDescription("Some view");
String oldName = f.getName();
HtmlForm cfg = r.createWebClient().getPage(f, "configure").getFormByName("config");
cfg.getInputByName("_.name").setValueAttribute("newName");
for (HtmlForm form : r.submit(cfg).getForms()) {
if (form.getActionAttribute().equals("doRename")) {
r.submit(form);
break;
}
}
assertEquals("newName",f.getName());
assertEquals("Some view",f.getDescription());
assertNull(r.jenkins.getItem(oldName));
assertSame(r.jenkins.getItem("newName"),f);
}
@Test public void configRoundtrip() throws Exception {
Folder f = createFolder();
r.configRoundtrip(f);
}
/**
* Makes sure the child can be deleted.
*/
@Test public void deleteChild() throws Exception {
Folder f = createFolder();
FreeStyleProject child = f.createProject(FreeStyleProject.class, "foo");
assertEquals(1,f.getItems().size());
child.delete();
assertEquals(0,f.getItems().size());
}
/**
* Tests the path resolution of "foo" (relative) vs "/foo" (absolute)
*/
@Test public void copyJob() throws Exception {
/*
- foo
- folder
- foo
*/
FreeStyleProject top = r.createFreeStyleProject("foo");
top.setDescription("top");
Folder f = createFolder();
FreeStyleProject child = f.createProject(FreeStyleProject.class, "foo");
child.setDescription("child");
JenkinsRule.WebClient wc = r.createWebClient();
// "foo" should copy "child"
copyFromGUI(f, wc, "foo", "xyz");
assertEquals("child",((Job)f.getItem("xyz")).getDescription());
// "/foo" should copy "top"
copyFromGUI(f, wc, "/foo", "uvw");
assertEquals("top",((Job)f.getItem("uvw")).getDescription());
}
private void copyFromGUI(Folder f, JenkinsRule.WebClient wc, String fromName, String toName) throws Exception {
HtmlPage page = wc.getPage(f, "newJob");
((HtmlInput)page.getElementById("name")).type(toName);
HtmlInput fe = (HtmlInput) page.getElementById("from");
fe.focus();
fe.type(fromName);
r.submit(page.getFormByName("createItem"));
}
/**
* When copying a folder, its contents need to be recursively copied.
*/
@Test public void copy() throws Exception {
Folder f = createFolder();
FreeStyleProject c1 = f.createProject(FreeStyleProject.class, "child1");
Folder c2 = f.createProject(Folder.class, "nested");
FreeStyleProject c21 = c2.createProject(FreeStyleProject.class,"child2");
Folder f2 = r.jenkins.copy(f, "fcopy");
assertTrue(f2.getItem("child1") instanceof FreeStyleProject);
Folder n = (Folder)f2.getItem("nested");
assertTrue(n.getItem("child2") instanceof FreeStyleProject);
}
@Issue("JENKINS-34939")
@Test public void delete() throws Exception {
Folder d1 = r.jenkins.createProject(Folder.class, "d1");
d1.createProject(FreeStyleProject.class, "p1");
d1.createProject(FreeStyleProject.class, "p2");
d1.createProject(Folder.class, "d2").createProject(FreeStyleProject.class, "p4");
d1.delete();
assertEquals("AbstractFolder.items is sorted by name so we can predict deletion order",
"{d1=[d1], d1/d2=[d1, d1/d2, d1/p1, d1/p2], d1/d2/p4=[d1, d1/d2, d1/d2/p4, d1/p1, d1/p2], d1/p1=[d1, d1/p1, d1/p2], d1/p2=[d1, d1/p2]}",
DeleteListener.whatRemainedWhenDeleted.toString());
}
@TestExtension("delete") public static class DeleteListener extends ItemListener {
static Map<String,Set<String>> whatRemainedWhenDeleted = new TreeMap<String,Set<String>>();
@Override public void onDeleted(Item item) {
try {
// Access metadata from another thread.
whatRemainedWhenDeleted.put(item.getFullName(), Timer.get().submit(new Callable<Set<String>>() {
@Override public Set<String> call() throws Exception {
Set<String> remaining = new TreeSet<String>();
for (Item i : Jenkins.getActiveInstance().getAllItems()) {
remaining.add(i.getFullName());
if (i instanceof Actionable) {
((Actionable) i).getAllActions();
}
}
return remaining;
}
}).get());
} catch (Exception x) {
assert false : x;
}
}
}
/**
* This is more of a test of the core, but make sure the triggers resolve between ourselves.
*/
@Test public void trigger() throws Exception {
Folder f = createFolder();
FreeStyleProject a = f.createProject(FreeStyleProject.class, "a");
FreeStyleProject b = f.createProject(FreeStyleProject.class, "b");
a.getPublishersList().add(new BuildTrigger("b",false));
FreeStyleBuild a1 = r.assertBuildStatusSuccess(a.scheduleBuild2(0));
for (int i=0; i<10 && b.getLastBuild()==null; i++) {
Thread.sleep(100);
}
// make sue that a build of B happens
}
/**
* Makes sure that there's no JavaScript error in the new view page.
*/
@Test public void newViewPage() throws Exception {
Folder f = createFolder();
HtmlPage p = r.createWebClient().getPage(f, "newView");
HtmlForm fm = p.getFormByName("createItem");
fm.getInputByName("name").setValueAttribute("abcView");
for (HtmlRadioButtonInput r : fm.getRadioButtonsByName("mode")) {
if (r.getValueAttribute().equals(ListView.class.getName()))
r.click();
}
r.submit(fm);
assertSame(ListView.class, f.getView("abcView").getClass());
}
/**
* Make sure we can load the data before we supported views and the configuration of the view
* correctly comes back.
*/
@LocalData
@Test public void dataCompatibility() throws Exception {
Folder f = (Folder) r.jenkins.getItem("foo");
ListView pv = (ListView)f.getPrimaryView();
assertEquals(2,pv.getColumns().size());
assertEquals(JobColumn.class, pv.getColumns().get(0).getClass());
assertEquals(BuildButtonColumn.class, pv.getColumns().get(1).getClass());
// we only have 2 columns in the zip but we expect a lot more in the out-of-the-box ListView.
assertTrue(2<new ListView("test").getColumns().size());
}
@Test public void search() throws Exception {
FreeStyleProject topJob = r.jenkins.createProject(FreeStyleProject.class, "top job");
Folder f1 = r.jenkins.createProject(Folder.class, "f1");
FreeStyleProject middleJob = f1.createProject(FreeStyleProject.class, "middle job");
Folder f2 = f1.createProject(Folder.class, "f2");
FreeStyleProject bottomJob = f2.createProject(FreeStyleProject.class, "bottom job");
List<SearchItem> items = new ArrayList<SearchItem>();
f1.getSearchIndex().suggest("job", items);
assertEquals(new HashSet<SearchItem>(Arrays.asList(middleJob, bottomJob)), new HashSet<SearchItem>(items));
}
@Test public void reloadJenkinsAndFindBuildInProgress() throws Exception {
Folder f1 = r.jenkins.createProject(Folder.class, "f");
FreeStyleProject p1 = f1.createProject(FreeStyleProject.class, "test1");
FreeStyleBuild p1b1 = p1.scheduleBuild2(0).get(); // one completed build
p1.getBuildersList().add(new SleepBuilder(99999999));
p1.save();
FreeStyleBuild p1b2 = p1.scheduleBuild2(0).waitForStart(); // another build in progress
// trigger the full Jenkins reload
r.jenkins.reload();
Folder f2 = (Folder) r.jenkins.getItem("f");
assertNotSame(f1,f2);
FreeStyleProject p2 = (FreeStyleProject) f2.getItem("test1");
/* Fails now. Why was this here?
assertNotSame(p1,p2);
*/
FreeStyleBuild p2b1 = p2.getBuildByNumber(1);
FreeStyleBuild p2b2 = p2.getBuildByNumber(2);
assertTrue(p2b2.isBuilding());
assertSame(p2b2,p1b2);
assertNotSame(p1b1,p2b1);
p1b2.getExecutor().interrupt(); // kill the executor
}
@Test public void discoverPermission() throws Exception {
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
final Folder d = r.jenkins.createProject(Folder.class, "d");
final FreeStyleProject p1 = d.createProject(FreeStyleProject.class, "p1");
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
grant(Jenkins.READ).everywhere().toEveryone().
grant(Item.DISCOVER).everywhere().toAuthenticated().
grant(Item.READ).onItems(d).toEveryone().
grant(Item.READ).onItems(p1).to("alice"));
FreeStyleProject p2 = d.createProject(FreeStyleProject.class, "p2");
ACL.impersonate(Jenkins.ANONYMOUS, new Runnable() {
@Override public void run() {
assertEquals(Collections.emptyList(), d.getItems());
assertNull(d.getItem("p1"));
assertNull(d.getItem("p2"));
}
});
ACL.impersonate(User.get("alice").impersonate(), new Runnable() {
@Override public void run() {
assertEquals(Collections.singletonList(p1), d.getItems());
assertEquals(p1, d.getItem("p1"));
try {
d.getItem("p2");
fail("should have been told p2 exists");
} catch (AccessDeniedException x) {
// correct
}
}
});
}
@Test public void addAction() throws Exception {
Folder f = createFolder();
WhoAmI a = new WhoAmI();
f.addAction(a);
assertNotNull(f.getAction(WhoAmI.class));
}
@Issue("JENKINS-32487")
@Test public void shouldAssignPropertyOwnerOnCreationAndReload() throws Exception {
Folder folder = r.jenkins.createProject(Folder.class, "myFolder");
r.jenkins.setAuthorizationStrategy(new ProjectMatrixAuthorizationStrategy());
// We add a stub property to generate the persisted list
// Then we ensure owner is being assigned properly.
folder.addProperty(new FolderCredentialsProvider.FolderCredentialsProperty(new DomainCredentials[0]));
assertPropertyOwner("After property add", folder, FolderCredentialsProvider.FolderCredentialsProperty.class);
// Reload and ensure that the property owner is set
r.jenkins.reload();
Folder reloadedFolder = r.jenkins.getItemByFullName("myFolder", Folder.class);
assertPropertyOwner("After reload", reloadedFolder, FolderCredentialsProvider.FolderCredentialsProperty.class);
}
@Issue("JENKINS-32359")
@Test public void shouldProperlyPersistFolderPropertiesOnMultipleReloads() throws Exception {
Folder folder = r.jenkins.createProject(Folder.class, "myFolder");
// We add a stub property to generate the persisted list
// After that we save and reload the config in order to drop PersistedListOwner according to the JENKINS-32359 scenario
folder.addProperty(new FolderCredentialsProvider.FolderCredentialsProperty(new DomainCredentials[0]));
r.jenkins.reload();
// Add another property
Map<Permission,Set<String>> grantedPermissions = new HashMap<Permission, Set<String>>();
Set<String> sids = new HashSet<String>();
sids.add("admin");
grantedPermissions.put(Jenkins.ADMINISTER, sids);
folder = r.jenkins.getItemByFullName("myFolder", Folder.class);
r.jenkins.setAuthorizationStrategy(new ProjectMatrixAuthorizationStrategy());
folder.addProperty(new com.cloudbees.hudson.plugins.folder.properties.AuthorizationMatrixProperty(grantedPermissions));
// Reload folder from disk and check the state
r.jenkins.reload();
Folder reloadedFolder = r.jenkins.getItemByFullName("myFolder", Folder.class);
assertThat("Folder has not been found after the reloading", reloadedFolder, notNullValue());
assertThat("Property has not been reloaded, hence it has not been saved properly",
reloadedFolder.getProperties().get(com.cloudbees.hudson.plugins.folder.properties.AuthorizationMatrixProperty.class),
notNullValue());
// Also ensure that both property owners are configured correctly
assertPropertyOwner("After reload", reloadedFolder, FolderCredentialsProvider.FolderCredentialsProperty.class);
assertPropertyOwner("After reload", reloadedFolder, com.cloudbees.hudson.plugins.folder.properties.AuthorizationMatrixProperty.class);
}
/**
* Ensures that the specified property points to the folder.
* @param <T> Property type
* @param folder Folder
* @param propertyClass Property class
* @param step Failure message prefix
*/
private <T extends AbstractFolderProperty<AbstractFolder<?>>> void assertPropertyOwner
(String step, Folder folder, Class<T> propertyClass) {
AbstractFolder<?> propertyOwner = folder.getProperties().get(propertyClass).getOwner();
assertThat(step + ": The property owner should be instance of Folder",
propertyOwner, instanceOf(Folder.class));
assertThat(step + ": The owner field of the " + propertyClass +
" property should point to the owner folder " + folder,
(Folder)propertyOwner, equalTo(folder));
}
private Folder createFolder() throws IOException {
return r.jenkins.createProject(Folder.class, "folder" + r.jenkins.getItems().size());
}
}