package org.jenkinsci.plugins.github.webhook;
import com.cloudbees.jenkins.GitHubPushTrigger;
import com.cloudbees.jenkins.GitHubRepositoryName;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.plugins.git.GitSCM;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.WithoutJenkins;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHHook;
import org.kohsuke.github.GHRepository;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.Lists.asList;
import static com.google.common.collect.Lists.newArrayList;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn;
import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl;
import static org.junit.Assert.assertThat;
import static org.kohsuke.github.GHEvent.CREATE;
import static org.kohsuke.github.GHEvent.PULL_REQUEST;
import static org.kohsuke.github.GHEvent.PUSH;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anySetOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author lanwen (Merkushev Kirill)
*/
@RunWith(MockitoJUnitRunner.class)
public class WebhookManagerTest {
public static final GitSCM GIT_SCM = new GitSCM("ssh://git@github.com/dummy/dummy.git");
public static final URL HOOK_ENDPOINT = endpoint("http://hook.endpoint/");
public static final URL ANOTHER_HOOK_ENDPOINT = endpoint("http://another.url/");
@Rule
public JenkinsRule jenkins = new JenkinsRule();
@Spy
private WebhookManager manager = forHookUrl(HOOK_ENDPOINT);
@Spy
private GitHubRepositoryName nonactive = new GitHubRepositoryName("github.com", "dummy", "dummy");
@Spy
private GitHubRepositoryName active = new GitHubRepositoryName("github.com", "dummy", "active");
@Mock
private GHRepository repo;
@Test
public void shouldDoNothingOnNoAdminRights() throws Exception {
manager.unregisterFor(nonactive, newArrayList(active));
verify(manager, times(1)).withAdminAccess();
verify(manager, never()).fetchHooks();
}
@Test
public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception {
doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
when(repo.hasAdminAccess()).thenReturn(true);
manager.unregisterFor(nonactive, newArrayList(active));
verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT);
verify(manager, times(1)).webhookFor(HOOK_ENDPOINT);
verify(manager, times(1)).fetchHooks();
}
@Test
public void shouldSearchOnlyServiceHookOnActiveName() throws Exception {
doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class));
when(repo.hasAdminAccess()).thenReturn(true);
manager.unregisterFor(active, newArrayList(active));
verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT);
verify(manager, never()).webhookFor(HOOK_ENDPOINT);
verify(manager, times(1)).fetchHooks();
}
@Test
@WithoutJenkins
public void shouldMatchAdminAccessWhenTrue() throws Exception {
when(repo.hasAdminAccess()).thenReturn(true);
assertThat("has admin access", manager.withAdminAccess().apply(repo), is(true));
}
@Test
@WithoutJenkins
public void shouldMatchAdminAccessWhenFalse() throws Exception {
when(repo.hasAdminAccess()).thenReturn(false);
assertThat("has no admin access", manager.withAdminAccess().apply(repo), is(false));
}
@Test
@WithoutJenkins
public void shouldMatchWebHook() {
when(repo.hasAdminAccess()).thenReturn(false);
GHHook hook = hook(HOOK_ENDPOINT, PUSH);
assertThat("webhook has web name and url prop", manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(true));
}
@Test
@WithoutJenkins
public void shouldNotMatchOtherUrlWebHook() {
when(repo.hasAdminAccess()).thenReturn(false);
GHHook hook = hook(ANOTHER_HOOK_ENDPOINT, PUSH);
assertThat("webhook has web name and another url prop",
manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(false));
}
@Test
public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException {
doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
when(repo.hasAdminAccess()).thenReturn(true);
Predicate<GHHook> del = spy(Predicate.class);
when(manager.deleteWebhook()).thenReturn(del);
GHHook hook = hook(HOOK_ENDPOINT, CREATE);
GHHook prhook = hook(HOOK_ENDPOINT, PULL_REQUEST);
when(repo.getHooks()).thenReturn(newArrayList(hook, prhook));
manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive);
verify(del, times(2)).apply(any(GHHook.class));
verify(manager).createWebhook(HOOK_ENDPOINT, EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH)));
}
@Test
public void shouldNotReplaceAlreadyRegisteredHook() throws IOException {
doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
when(repo.hasAdminAccess()).thenReturn(true);
GHHook hook = hook(HOOK_ENDPOINT, PUSH);
when(repo.getHooks()).thenReturn(newArrayList(hook));
manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive);
verify(manager, never()).deleteWebhook();
verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class));
}
@Test
public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException {
FreeStyleProject project = jenkins.createFreeStyleProject();
project.setScm(GIT_SCM);
manager.registerFor((Item)project).run();
verify(manager, never()).createHookSubscribedTo(anyListOf(GHEvent.class));
}
@Test
public void shouldAddPushEventByDefault() throws IOException {
FreeStyleProject project = jenkins.createFreeStyleProject();
project.addTrigger(new GitHubPushTrigger());
project.setScm(GIT_SCM);
manager.registerFor((Item)project).run();
verify(manager).createHookSubscribedTo(newArrayList(PUSH));
}
@Test
public void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOException {
doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class));
when(repo.hasAdminAccess()).thenReturn(true);
assertThat("empty events list not allowed to be registered",
forHookUrl(HOOK_ENDPOINT)
.createHookSubscribedTo(Collections.<GHEvent>emptyList()).apply(active), nullValue());
}
@Test
public void shouldSelectOnlyHookManagedCreds() {
GitHubServerConfig conf = new GitHubServerConfig("");
conf.setManageHooks(false);
GitHubPlugin.configuration().getConfigs().add(conf);
assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH))
.apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue());
}
@Test
public void shouldNotSelectCredsWithCustomHost() {
GitHubServerConfig conf = new GitHubServerConfig("");
conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString());
conf.setManageHooks(false);
GitHubPlugin.configuration().getConfigs().add(conf);
assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH))
.apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue());
}
@Test
public void shouldSendSecretIfDefined() throws Exception {
String secretText = "secret_text";
storeSecretIn(GitHubPlugin.configuration(), secretText);
manager.createWebhook(HOOK_ENDPOINT, ImmutableSet.of(PUSH)).apply(repo);
verify(repo).createHook(
anyString(),
(Map<String, String>) argThat(hasEntry("secret", secretText)),
anySetOf(GHEvent.class),
anyBoolean()
);
}
private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) {
GHHook hook = mock(GHHook.class);
when(hook.getName()).thenReturn("web");
when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint.toExternalForm()));
when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events)));
return hook;
}
private static URL endpoint(String endpoint) {
try {
return new URL(endpoint);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}