diff --git a/docs/USER_GUIDE.adoc b/docs/USER_GUIDE.adoc index a1c938ca2..c4cc37eae 100644 --- a/docs/USER_GUIDE.adoc +++ b/docs/USER_GUIDE.adoc @@ -328,12 +328,12 @@ System.setProperty("http.socket.timeout", "300") // 5 minutes In case Bitbucket has been configured to expire OAuth2 tokens before 5 minutes, you can configure via a JVM property the release time of the cache where all obtained OAuth2 tokens are stored. This setting is to avoid requests with expired tokens that will produce HTTP 401 responses. link:https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/[Bitbucket Cloud] access tokens expire in two hours. To change this amount of time (default is 300 seconds), add the system property `bitbucket.oauth2.cache.timeout=60` on Jenkins startup. -=== Disable Branch Indexing on Empty changes +=== Enable Branch Indexing on Empty changes -By default, the plugin triggers *a full branch indexing* when a push event contains *empty* changes. This may happen on various scenario, mainly in Bitbucket Data Center, such as: +By default, the plugin does not triggers *a full branch indexing* when a push event contains *empty* changes. This may happen on various scenario, mainly in Bitbucket Data Center, such as: * When manually merging remote **Open** pull requests. This particular scenario produces 2 events and cause duplicated builds. * For a fork, when link:https://confluence.atlassian.com/bitbucketserver/keeping-forks-synchronized-776639961.html[Auto-Sync] is on and a branch cannot be synchronised * A link:http://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html#Eventpayload-Mirrorsynchronized[mirror:repo_synchronized] event with too many refs -This behaviour can be disabled by adding the system property `bitbucket.hooks.processor.scanOnEmptyChanges=false` on Jenkins startup. +This behaviour can be enabled by adding the system property `bitbucket.hooks.processor.scanOnEmptyChanges=true` on Jenkins startup. diff --git a/pom.xml b/pom.xml index f4eae59a2..b80c764a8 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,10 @@ io.jenkins.plugins commons-lang3-api + + io.jenkins.plugins + commons-collections4-api + org.jenkins-ci.plugins jackson2-api diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java index 16e7291a0..cf4e6812a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java @@ -31,6 +31,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.PullRequestBranchType; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.extension.FallbackToOtherRepositoryGitSCMExtension; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; @@ -113,7 +114,7 @@ public BitbucketGitSCMBuilder(@NonNull BitbucketSCMSource scmSource, @NonNull SC String serverURL = scmSource.getServerUrl(); BitbucketEndpoint endpoint = BitbucketEndpointProvider.lookupEndpoint(serverURL) - .orElse(new BitbucketServerEndpoint(null, serverURL, false, null, false, null)); + .orElse(BitbucketApiUtils.isCloud(serverURL) ? new BitbucketCloudEndpoint() : new BitbucketServerEndpoint("tmp", serverURL)); String repositoryURL = endpoint.getRepositoryURL(scmSource.getRepoOwner(), scmSource.getRepository()); if (BitbucketApiUtils.isCloud(endpoint.getServerURL())) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java index 5be3d4a5d..20a89efd6 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -38,7 +38,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.MirrorListSupplier; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.trait.BranchDiscoveryTrait; import com.cloudbees.jenkins.plugins.bitbucket.trait.ForkPullRequestDiscoveryTrait; import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; @@ -412,10 +411,8 @@ public static FormValidation doCheckCredentialsId(@AncestorInPath SCMSourceOwner public static FormValidation doCheckMirrorId(@QueryParameter String value, @QueryParameter(fixEmpty = true, value = "serverUrl") String serverURL) { if (!value.isEmpty()) { - BitbucketServerWebhookImplementation webhookImplementation = - BitbucketServerEndpoint.findWebhookImplementation(serverURL); - if (webhookImplementation == BitbucketServerWebhookImplementation.PLUGIN) { - return FormValidation.error("Mirror can only be used with native webhooks"); + if (BitbucketServerEndpoint.supportsMirror(serverURL)) { + return FormValidation.error("Mirror is not supported by the choosen webhooks"); } } return FormValidation.ok(); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java index 16850eb6f..e13257f13 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -53,7 +53,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.util.DateUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.MirrorListSupplier; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.jenkins.plugins.bitbucket.trait.BranchDiscoveryTrait; @@ -1073,9 +1072,7 @@ public static FormValidation doCheckServerUrl(@AncestorInPath SCMSourceOwner con public static FormValidation doCheckMirrorId(@QueryParameter String value, @QueryParameter(fixEmpty = true, value = "serverUrl") String serverURL) { if (!value.isEmpty()) { - BitbucketServerWebhookImplementation webhookImplementation = - BitbucketServerEndpoint.findWebhookImplementation(serverURL); - if (webhookImplementation == BitbucketServerWebhookImplementation.PLUGIN) { + if (BitbucketServerEndpoint.supportsMirror(serverURL)) { return FormValidation.error("Mirror can only be used with native webhooks"); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketException.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketException.java index 90e856cdf..2d62f2aef 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketException.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketException.java @@ -24,6 +24,7 @@ package com.cloudbees.jenkins.plugins.bitbucket.api; public class BitbucketException extends RuntimeException { + private static final long serialVersionUID = 1L; public BitbucketException(String message, Throwable cause) { super(message, cause); @@ -33,6 +34,4 @@ public BitbucketException(String message) { super(message); } - private static final long serialVersionUID = 1L; - } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointDescriptor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointDescriptor.java index b235b7121..c1ba41e24 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointDescriptor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointDescriptor.java @@ -23,23 +23,7 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api.endpoint; -import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import hudson.Util; import hudson.model.Descriptor; -import hudson.security.ACL; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import java.net.MalformedURLException; -import java.net.URL; -import jenkins.model.Jenkins; -import org.jenkinsci.plugins.plaincredentials.StringCredentials; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; /** * {@link Descriptor} for {@link BitbucketEndpoint}s. @@ -47,62 +31,4 @@ * @since 936.4.0 */ public class BitbucketEndpointDescriptor extends Descriptor { - /** - * Stapler form completion. - * - * @param credentialsId selected credentials. - * @param serverURL the server URL. - * @return the available credentials. - */ - @RequirePOST - public ListBoxModel doFillCredentialsIdItems(@QueryParameter(fixEmpty = true) String credentialsId, - @QueryParameter(value = "serverUrl", fixEmpty = true) String serverURL) { - Jenkins jenkins = checkPermission(); - return BitbucketCredentialsUtils.listCredentials(jenkins, serverURL, credentialsId); - } - - private static Jenkins checkPermission() { - Jenkins jenkins = Jenkins.get(); - jenkins.checkPermission(Jenkins.MANAGE); - return jenkins; - } - - /** - * Stapler form completion. - * - * @param hookSignatureCredentialsId selected hook signature credentials. - * @param serverURL the server URL. - * @return the available credentials. - */ - @RequirePOST - public ListBoxModel doFillHookSignatureCredentialsIdItems(@QueryParameter(fixEmpty = true) String hookSignatureCredentialsId, - @QueryParameter(value = "serverUrl", fixEmpty = true) String serverURL) { - Jenkins jenkins = checkPermission(); - StandardListBoxModel result = new StandardListBoxModel(); - result.includeMatchingAs(ACL.SYSTEM2, - jenkins, - StringCredentials.class, - URIRequirementBuilder.fromUri(serverURL).build(), - CredentialsMatchers.always()); - if (hookSignatureCredentialsId != null) { - result.includeCurrentValue(hookSignatureCredentialsId); - } - return result; - } - - @Restricted(NoExternalUse.class) - @RequirePOST - public static FormValidation doCheckBitbucketJenkinsRootUrl(@QueryParameter String value) { - checkPermission(); - String url = Util.fixEmptyAndTrim(value); - if (url == null) { - return FormValidation.ok(); - } - try { - new URL(url); - } catch (MalformedURLException e) { - return FormValidation.error("Invalid URL: " + e.getMessage()); - } - return FormValidation.ok(); - } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointProvider.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointProvider.java index fb6027934..8095a5169 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointProvider.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/endpoint/BitbucketEndpointProvider.java @@ -159,7 +159,7 @@ public static BitbucketEndpoint registerEndpoint(@NonNull String name, @NonNull if (BitbucketApiUtils.isCloud(serverURL)) { endpoint = new BitbucketCloudEndpoint(); } else { - endpoint = new BitbucketServerEndpoint(name, serverURL, false, null, false, null); + endpoint = new BitbucketServerEndpoint(name, serverURL); } if (endpointCustomiser != null) { endpoint = endpointCustomiser.apply(endpoint); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhook.java similarity index 61% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhook.java index 45c5196ef..1e1d28358 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhook.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016-2018, Yieldlab AG + * Copyright (c) 2025, Falco Nikolas * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,23 +21,31 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.api.webhook; -import hudson.RestrictedSince; -import java.util.logging.Level; -import java.util.logging.Logger; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Describable; import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.accmod.restrictions.Beta; -@Restricted(NoExternalUse.class) -@RestrictedSince("933.3.0") -public class NativeServerPingHookProcessor extends HookProcessor { +@Restricted(Beta.class) +public interface BitbucketWebhook extends Describable { - private static final Logger LOGGER = Logger.getLogger(NativeServerPingHookProcessor.class.getName()); + /** + * Name to use to describe the hook implementation. + * + * @return the name to use for the implementation + */ + @CheckForNull + String getDisplayName(); - @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin) { - LOGGER.log(Level.INFO, "Received webhook ping event from {0}", origin); - } + /** + * The hook implementation identifier. + * + * @return the unique identifier for this implementation + */ + @NonNull + String getId(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookDescriptor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookDescriptor.java new file mode 100644 index 000000000..a45687820 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookDescriptor.java @@ -0,0 +1,35 @@ +/* + * 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 com.cloudbees.jenkins.plugins.bitbucket.api.webhook; + +import hudson.model.Descriptor; + +/** + * {@link Descriptor} for {@link BitbucketWebhook}s. + * + * @since 936.4.0 + */ +public abstract class BitbucketWebhookDescriptor extends Descriptor { + public abstract boolean isApplicable(String serverURL); +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessor.java new file mode 100644 index 000000000..4c187e258 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessor.java @@ -0,0 +1,151 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Falco Nikolas + * + * 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.jenkins.plugins.bitbucket.api.webhook; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionPoint; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import jenkins.scm.api.SCMEvent; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.util.SystemProperties; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * Implementations of this extension point must provide new behaviours to + * accommodate custom event payloads from webhooks sent from Bitbucket Cloud, + * Bitbucket Data Center, or installed plugins. + *

+ * There cannot be multiple processors processing the same incoming webhook for + * a specific event installed on the system, meaning the processor must fit to + * the incoming request as much as possible or the hook will be rejected. + */ +@Restricted(Beta.class) +public interface BitbucketWebhookProcessor extends ExtensionPoint { + static final String SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME = "bitbucket.hooks.processor.scanOnEmptyChanges"; + + /** + * Called by first for this processor that must respond if is able to handle + * this specific request + * + * @param headers request + * @param parameters request + * @return {@code true} if this processor is able to handle this hook + * request, {@code false} otherwise. + */ + boolean canHandle(@NonNull Map headers, @NonNull MultiValuedMap parameters); + + /** + * Extracts the server URL from where this request coming from, the URL must + * match one of the configured {@link BitbucketEndpoint}s. + * + * @param headers request + * @param parameters request + * @return the URL of the server from where this request has been sent. + */ + @NonNull + String getServerURL(@NonNull Map headers, @NonNull MultiValuedMap parameters); + + /** + * Extracts the event type that represent the payload in the request. + * + * @param headers request + * @param parameters request + * @return the event type key. + */ + @NonNull + String getEventType(Map headers, MultiValuedMap parameters); + + /** + * Returns a context for a given request used when process the payload. + * + * @param request hook + * @return a map of information extracted by the given request to be used in + * the {@link #process(String, String, Map, BitbucketEndpoint)} + * method. + */ + @NonNull + default Map buildHookContext(@NonNull HttpServletRequest request) { + return Map.of("origin", SCMEvent.originOf(request)); + } + + /** + * The implementation must verify if the incoming request is secured or not + * eventually gather some settings from the given {@link BitbucketEndpoint} + * configuration. + * + * @param headers request + * @param payload request + * @param endpoint configured for the given + * {@link #getServerURL(Map, MultiValuedMap)} + * @throws BitbucketWebhookProcessorException when signature verification fails + */ + void verifyPayload(@NonNull Map headers, + @NonNull String payload, + @NonNull BitbucketEndpoint endpoint) throws BitbucketWebhookProcessorException; + + /** + * Settings that will trigger a re-index of the multibranch + * project/organization folder when the request does not ship any source + * changes. + * + * @return if should perform a reindex of the project or not. + */ + default boolean reindexOnEmptyChanges() { + return SystemProperties.getBoolean(SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME, false); + } + + /** + * See Event + * Payloads for more information about the payload parameter format. + * + * @param eventType the type of hook event. + * @param payload the hook payload + * @param context build from incoming request + * @param endpoint configured in the Jenkins global page + */ + void process(@NonNull String eventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint); + + /** + * Implementations have to call this method when want propagate an + * {@link SCMHeadEvent} to the scm-api. + * + * @param event the to fire + * @param delaySeconds a delay in seconds to wait before propagate the + * event. If the given value is less than 0 than default will be + * used. + */ + default void notifyEvent(SCMHeadEvent event, int delaySeconds) { + if (delaySeconds == 0) { + SCMHeadEvent.fireNow(event); + } else { + SCMHeadEvent.fireLater(event, delaySeconds > 0 ? delaySeconds : BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerWebhookImplementation.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessorException.java similarity index 63% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerWebhookImplementation.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessorException.java index 445fc5cae..cd20d4125 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerWebhookImplementation.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookProcessorException.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016-2018, Yieldlab AG + * Copyright (c) 2025, Falco Nikolas * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,26 +21,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.server; +package com.cloudbees.jenkins.plugins.bitbucket.api.webhook; -import hudson.model.ModelObject; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException; -/** The different webhook implementations available for Bitbucket Server. */ -public enum BitbucketServerWebhookImplementation implements ModelObject { - /** Plugin-based webhooks. */ - PLUGIN("Plugin"), +public class BitbucketWebhookProcessorException extends BitbucketException { + private static final long serialVersionUID = 6682700868741672883L; + private final int httpCode; - /** Native webhooks, available since Bitbucket Server 5.4. */ - NATIVE("Native"); - - private final String displayName; - - BitbucketServerWebhookImplementation(String displayName) { - this.displayName = displayName; + public BitbucketWebhookProcessorException(int httpCode, String message) { + super(message); + this.httpCode = httpCode; } - @Override - public String getDisplayName() { - return displayName; + public int getHttpCode() { + return httpCode; } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java index 9a773dee4..18a8f4b3d 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -29,6 +29,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; @@ -86,6 +87,7 @@ public XmlFile getConfigFile() { XStream2 xs = new XStream2(XStream2.getDefaultDriver()); xs.alias("com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint", BitbucketCloudEndpoint.class); xs.alias("com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint", BitbucketServerEndpoint.class); + xs.aliasAttribute(BitbucketServerEndpoint.class, "serverURL", "serverUrl"); return new XmlFile(xs, cfgFile); } @@ -124,7 +126,7 @@ public String readResolveServerUrl(@CheckForNull String serverURL) { // exception case endpoint = new BitbucketCloudEndpoint(); } else { - endpoint = new BitbucketServerEndpoint(normalizedURL); + endpoint = new BitbucketServerEndpoint(null, normalizedURL); } addEndpoint(endpoint); } @@ -199,10 +201,10 @@ public void setEndpoints(@CheckForNull List endpoin continue; } else if (!(endpoint instanceof BitbucketCloudEndpoint) && BitbucketApiUtils.isCloud(serverURL)) { // fix type for the special case - BitbucketCloudEndpoint cloudEndpoint = new BitbucketCloudEndpoint(false, 0, 0, - endpoint.isManageHooks(), endpoint.getCredentialsId(), + CloudWebhook webhook = new CloudWebhook(endpoint.isManageHooks(), endpoint.getCredentialsId(), endpoint.isEnableHookSignature(), endpoint.getHookSignatureCredentialsId()); - cloudEndpoint.setBitbucketJenkinsRootUrl(endpoint.getEndpointJenkinsRootURL()); + webhook.setEndpointJenkinsRootURL(endpoint.getEndpointJenkinsRootURL()); + BitbucketCloudEndpoint cloudEndpoint = new BitbucketCloudEndpoint(false, 0, 0, webhook); iterator.set(cloudEndpoint); } serverURLs.add(serverURL); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java index d9b8a8805..cca38fd20 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java @@ -31,10 +31,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; -import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.DateUtils; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerVersion; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; @@ -293,13 +291,7 @@ public SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @Ch // Bitbucket server v7 doesn't have the `merge` ref for PRs // We don't return `ref` when working with v7 // so that pipeline falls back to heavyweight checkout properly - boolean ligthCheckout = BitbucketServerEndpoint.findServerVersion(serverURL) != BitbucketServerVersion.VERSION_7; - if (ligthCheckout) { - ref = "pull-requests/" + prHead.getId() + "/merge"; - } else { - // returning null to fall back to heavyweight checkout - return null; - } + return null; } } else if (head instanceof BitbucketTagSCMHead) { ref = "tags/" + head.getName(); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java index 68b012fd3..6951441ac 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java @@ -25,49 +25,45 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; -import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessorException; import hudson.Extension; +import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; import hudson.security.csrf.CrumbExclusion; -import hudson.util.HttpResponses; -import hudson.util.Secret; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.scm.api.SCMEvent; -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.HmacAlgorithms; -import org.apache.commons.codec.digest.HmacUtils; +import java.util.stream.Stream; +import org.apache.commons.collections4.EnumerationUtils; +import org.apache.commons.collections4.MultiMapUtils; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.apache.commons.lang3.StringUtils; import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.HttpResponses.HttpResponseException; +import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest2; -import static org.apache.commons.lang.StringUtils.trimToNull; - /** * Process Bitbucket push and pull requests creations/updates hooks. */ @Extension public class BitbucketSCMSourcePushHookReceiver extends CrumbExclusion implements UnprotectedRootAction { - private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSourcePushHookReceiver.class.getName()); - + private static final Logger logger = Logger.getLogger(BitbucketSCMSourcePushHookReceiver.class.getName()); private static final String PATH = "bitbucket-scmsource-hook"; - public static final String FULL_PATH = PATH + "/notify"; @Override @@ -94,120 +90,83 @@ public String getUrlName() { * @throws IOException if there is any issue reading the HTTP content payload. */ public HttpResponse doNotify(StaplerRequest2 req) throws IOException { - String origin = SCMEvent.originOf(req); - String body = IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8); - - String eventKey = req.getHeader("X-Event-Key"); - if (eventKey == null) { - return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "X-Event-Key HTTP header not found"); - } - HookEventType type = HookEventType.fromString(eventKey); - if (type == null) { - LOGGER.info(() -> "Received unknown Bitbucket hook: " + eventKey + ". Skipping."); - return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "X-Event-Key HTTP header invalid: " + eventKey); - } - - String bitbucketKey = req.getHeader("X-Bitbucket-Type"); // specific header from Plugin implementation - String serverURL = req.getParameter("server_url"); + try { + Map reqHeaders = getHeaders(req); + MultiValuedMap reqParameters = getParameters(req); + BitbucketWebhookProcessor hookProcessor = getHookProcessor(reqHeaders, reqParameters); + + String body = IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8); + if (StringUtils.isEmpty(body)) { + return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Payload is empty."); + } - BitbucketType instanceType = null; - if (bitbucketKey != null) { - instanceType = BitbucketType.fromString(bitbucketKey); - LOGGER.log(Level.FINE, "X-Bitbucket-Type header found {0}.", instanceType); - } - if (serverURL != null) { - if (instanceType == null) { - LOGGER.log(Level.FINE, "server_url request parameter found. Bitbucket Native Server webhook incoming."); - instanceType = BitbucketType.SERVER; - } else { - LOGGER.log(Level.FINE, "X-Bitbucket-Type header / server_url request parameter found. Bitbucket Plugin Server webhook incoming."); + String serverURL = hookProcessor.getServerURL(Collections.unmodifiableMap(reqHeaders), MultiMapUtils.unmodifiableMultiValuedMap(reqParameters)); + BitbucketEndpoint endpoint = BitbucketEndpointProvider + .lookupEndpoint(serverURL) + .orElse(null); + if (endpoint == null) { + logger.log(Level.SEVERE, "No configured bitbucket endpoint found for {0}.", serverURL); + return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "No bitbucket endpoint found for " + serverURL); } - } else { - LOGGER.log(Level.FINE, "X-Bitbucket-Type header / server_url request parameter not found. Bitbucket Cloud webhook incoming."); - instanceType = BitbucketType.CLOUD; - serverURL = BitbucketCloudEndpoint.SERVER_URL; - } - BitbucketEndpoint endpoint = BitbucketEndpointProvider - .lookupEndpoint(serverURL) - .orElse(null); - if (endpoint != null) { if (endpoint.isEnableHookSignature()) { - if (req.getHeader("X-Hub-Signature") != null) { - HttpResponseException error = checkSignature(req, body, endpoint); - if (error != null) { - return error; - } - } else { - return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "Payload has not be signed, configure the webHook secret in Bitbucket as documented at https://github.com/jenkinsci/bitbucket-branch-source-plugin/blob/master/docs/USER_GUIDE.adoc#webhooks-registering"); - } - } else if (req.getHeader("X-Hub-Signature") == null) { - LOGGER.log(Level.FINER, "Signature not configured for bitbucket endpoint {0}.", serverURL); + logger.log(Level.FINE, "Payload endpoint host {0}, request endpoint host {1}", new Object[] { endpoint, req.getRemoteAddr() }); + hookProcessor.verifyPayload(reqHeaders, body, endpoint); + } else { + logger.log(Level.FINER, "Signature not configured for bitbucket endpoint {0}.", serverURL); } - } else { - LOGGER.log(Level.INFO, "No bitbucket endpoint found for {0} to verify the signature of incoming webhook.", serverURL); - } - HookProcessor hookProcessor = getHookProcessor(type); - hookProcessor.process(type, body, instanceType, origin, serverURL); + Map context = hookProcessor.buildHookContext(req); + String eventType = hookProcessor.getEventType(Collections.unmodifiableMap(reqHeaders), MultiMapUtils.unmodifiableMultiValuedMap(reqParameters)); + + hookProcessor.process(eventType, body, context, endpoint); + } catch(BitbucketWebhookProcessorException e) { + return HttpResponses.error(e.getHttpCode(), e.getMessage()); + } return HttpResponses.ok(); } - @Nullable - private HttpResponseException checkSignature(@NonNull StaplerRequest2 req, @NonNull String body, @NonNull BitbucketEndpoint endpoint) { - LOGGER.log(Level.FINE, "Payload endpoint host {0}, request endpoint host {1}", new Object[] { endpoint, req.getRemoteAddr() }); - - StringCredentials signatureCredentials = endpoint.hookSignatureCredentials(); - if (signatureCredentials != null) { - String signatureHeader = req.getHeader("X-Hub-Signature"); - String bitbucketAlgorithm = trimToNull(StringUtils.substringBefore(signatureHeader, "=")); - String bitbucketSignature = trimToNull(StringUtils.substringAfter(signatureHeader, "=")); - HmacAlgorithms algorithm = getAlgorithm(bitbucketAlgorithm); - if (algorithm == null) { - return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "Signature " + bitbucketAlgorithm + " not supported"); - } - HmacUtils util; - try { - String key = Secret.toString(signatureCredentials.getSecret()); - util = new HmacUtils(algorithm, key.getBytes(StandardCharsets.UTF_8)); - byte[] digest = util.hmac(body); - if (!MessageDigest.isEqual(Hex.decodeHex(bitbucketSignature), digest)) { - return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "Signature verification failed"); - } - } catch (IllegalArgumentException e) { - return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Signature method not supported: " + algorithm); - } catch (DecoderException e) { - return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Hex signature can not be decoded: " + bitbucketSignature); - } + private BitbucketWebhookProcessor getHookProcessor(Map reqHeaders, + MultiValuedMap reqParameters) { + BitbucketWebhookProcessor hookProcessor; + + List matchingProcessors = getHookProcessors() + .filter(processor -> processor.canHandle(Collections.unmodifiableMap(reqHeaders), MultiMapUtils.unmodifiableMultiValuedMap(reqParameters))) + .toList(); + if (matchingProcessors.isEmpty()) { + logger.warning(() -> "No processor found for the incoming Bitbucket hook. Skipping."); + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_BAD_REQUEST, "No processor found that supports this event. Refer to the user documentation on how configure the webHook in Bitbucket at https://github.com/jenkinsci/bitbucket-branch-source-plugin/blob/master/docs/USER_GUIDE.adoc#webhooks-registering"); + } else if (matchingProcessors.size() > 1) { + String processors = StringUtils.joinWith("\n- ", matchingProcessors.stream() + .map(p -> p.getClass().getName()) + .toList()); + logger.severe(() -> "More processors found that handle the incoming Bitbucket hook:\n" + processors); + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_CONFLICT, "More processors found that handle the incoming Bitbucket hook."); } else { - String hookId = req.getHeader("X-Hook-UUID"); - String requestId = ObjectUtils.firstNonNull(req.getHeader("X-Request-UUID"), req.getHeader("X-Request-Id")); - String hookSignatureCredentialsId = endpoint.getHookSignatureCredentialsId(); - LOGGER.log(Level.WARNING, "No credentials {0} found to verify the signature of incoming webhook {1} request {2}", new Object[] { hookSignatureCredentialsId, hookId, requestId }); - return HttpResponses.error(HttpServletResponse.SC_FORBIDDEN, "No credentials " + hookSignatureCredentialsId + " found in Jenkins to verify the signature"); + hookProcessor = matchingProcessors.get(0); + logger.fine(() -> "Hook processor " + hookProcessor.getClass().getName() + " found."); } - return null; + return hookProcessor; } - @CheckForNull - private HmacAlgorithms getAlgorithm(String algorithm) { - switch (StringUtils.lowerCase(algorithm)) { - case "sha1": - return HmacAlgorithms.HMAC_SHA_1; - case "sha256": - return HmacAlgorithms.HMAC_SHA_256; - case "sha384": - return HmacAlgorithms.HMAC_SHA_384; - case "sha512": - return HmacAlgorithms.HMAC_SHA_512; - default: - return null; + /*test*/ Stream getHookProcessors() { + return ExtensionList.lookup(BitbucketWebhookProcessor.class).stream(); + } + + private MultiValuedMap getParameters(StaplerRequest2 req) { + MultiValuedMap reqParameters = new ArrayListValuedHashMap<>(); + for (Entry entry : req.getParameterMap().entrySet()) { + reqParameters.putAll(entry.getKey(), Arrays.asList(entry.getValue())); } + return reqParameters; } - /* For test purpose */ - HookProcessor getHookProcessor(HookEventType type) { - return type.getProcessor(); + private Map getHeaders(StaplerRequest2 req) { + Map reqHeaders = new HashMap<>(); + for (String headerName : EnumerationUtils.asIterable(req.getHeaderNames())) { + reqHeaders.put(headerName, req.getHeader(headerName)); + } + return reqHeaders; } @Override diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java index 926dfff42..2f0d5d4d5 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java @@ -23,7 +23,6 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; -import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -34,125 +33,118 @@ public enum HookEventType { /** * See EventPayloads-Push */ - PUSH("repo:push", PushHookProcessor.class), + PUSH("repo:push"), /** * See EventPayloads-Created */ - PULL_REQUEST_CREATED("pullrequest:created", PullRequestHookProcessor.class), + PULL_REQUEST_CREATED("pullrequest:created"), /** * See EventPayloads-Updated */ - PULL_REQUEST_UPDATED("pullrequest:updated", PullRequestHookProcessor.class), + PULL_REQUEST_UPDATED("pullrequest:updated"), /** * See EventPayloads-Merged */ - PULL_REQUEST_MERGED("pullrequest:fulfilled", PullRequestHookProcessor.class), + PULL_REQUEST_MERGED("pullrequest:fulfilled"), /** * See EventPayloads-Declined */ - PULL_REQUEST_DECLINED("pullrequest:rejected", PullRequestHookProcessor.class), + PULL_REQUEST_DECLINED("pullrequest:rejected"), /** * See EventPayloads-Approved */ - PULL_REQUEST_APPROVED("pullrequest:approved", PullRequestHookProcessor.class), + @Deprecated + PULL_REQUEST_APPROVED("pullrequest:approved"), /** * @see Eventpayload-Push * @since Bitbucket Server 5.4 */ - SERVER_REFS_CHANGED("repo:refs_changed", NativeServerPushHookProcessor.class), + SERVER_REFS_CHANGED("repo:refs_changed"), /** * @see Eventpayload-repo-mirr-syn * @since Bitbucket Server 6.5 */ - SERVER_MIRROR_REPO_SYNCHRONIZED("mirror:repo_synchronized", NativeServerPushHookProcessor.class), + SERVER_MIRROR_REPO_SYNCHRONIZED("mirror:repo_synchronized"), /** * @see Eventpayload-Opened * @since Bitbucket Server 5.4 */ - SERVER_PULL_REQUEST_OPENED("pr:opened", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_OPENED("pr:opened"), /** * @see Eventpayload-Merged * @since Bitbucket Server 5.4 */ - SERVER_PULL_REQUEST_MERGED("pr:merged", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_MERGED("pr:merged"), /** * See Eventpayload-Declined * @since Bitbucket Server 5.4 */ - SERVER_PULL_REQUEST_DECLINED("pr:declined", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_DECLINED("pr:declined"), /** * See Eventpayload-Deleted * * @since Bitbucket Server 5.4 */ - SERVER_PULL_REQUEST_DELETED("pr:deleted", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_DELETED("pr:deleted"), /** * See Eventpayload-Approved * * @since Bitbucket Server 5.4 */ - SERVER_PULL_REQUEST_APPROVED("pr:reviewer:approved", NativeServerPullRequestHookProcessor.class), + @Deprecated + SERVER_PULL_REQUEST_APPROVED("pr:reviewer:approved"), /** * @see Eventpayload: Pull Request - Modified * @since Bitbucket Server 5.10 */ - SERVER_PULL_REQUEST_MODIFIED("pr:modified", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_MODIFIED("pr:modified"), /** * @see Eventpayload: Pull Request - Reviewers Updated * @since Bitbucket Server 5.10 */ - SERVER_PULL_REQUEST_REVIEWER_UPDATED("pr:reviewer:updated", NativeServerPullRequestHookProcessor.class), + @Deprecated + SERVER_PULL_REQUEST_REVIEWER_UPDATED("pr:reviewer:updated"), /** * @see Eventpayload-Sourcebranchupdated * @since Bitbucket Server 7.0 */ - SERVER_PULL_REQUEST_FROM_REF_UPDATED("pr:from_ref_updated", NativeServerPullRequestHookProcessor.class), + SERVER_PULL_REQUEST_FROM_REF_UPDATED("pr:from_ref_updated"), /** * Sent when hitting the {@literal "Test connection"} button in Bitbucket Server. Apparently undocumented. */ - SERVER_PING("diagnostics:ping", NativeServerPingHookProcessor.class); + SERVER_PING("diagnostics:ping"); private final String key; - private final Class clazz; -

HookEventType(@NonNull String key, Class

clazz) { + HookEventType(@NonNull String key) { this.key = key; - this.clazz = clazz; } - @CheckForNull + @NonNull public static HookEventType fromString(String key) { for (HookEventType value : HookEventType.values()) { if (value.getKey().equals(key)) { return value; } } - return null; - } - - public HookProcessor getProcessor() { - try { - return (HookProcessor) clazz.getDeclaredConstructor().newInstance(); - } catch (ReflectiveOperationException e) { - throw new AssertionError("Can not instantiate hook payload processor: " + e.getMessage()); - } + throw new IllegalArgumentException("No enum constant " + HookEventType.class.getCanonicalName() + " have key " + key); } public String getKey() { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java deleted file mode 100644 index 0bcc1682e..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.hooks; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import hudson.security.ACL; -import hudson.security.ACLContext; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMSource; -import jenkins.scm.api.SCMSourceOwner; -import jenkins.scm.api.SCMSourceOwners; -import jenkins.util.SystemProperties; -import org.apache.commons.lang3.StringUtils; - -/** - * Abstract hook processor. - * - * Add new hook processors by extending this class and implement {@link #process(HookEventType, String, BitbucketType, String)}, - * extract details from the hook payload and then fire an {@link jenkins.scm.api.SCMEvent} to dispatch it to the SCM API. - */ -public abstract class HookProcessor { - - protected static final String SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME = "bitbucket.hooks.processor.scanOnEmptyChanges"; - protected static final boolean SCAN_ON_EMPTY_CHANGES = SystemProperties.getBoolean(SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME, true); - - private static final Logger LOGGER = Logger.getLogger(HookProcessor.class.getName()); - - /** - * See Event Payloads for more - * information about the payload parameter format. - * @param type the type of hook. - * @param payload the hook payload - * @param instanceType the Bitbucket type that called the hook - * @param origin the origin of the event. - */ - public abstract void process(HookEventType type, String payload, BitbucketType instanceType, String origin); - - /** - * See Event Payloads for more - * information about the payload parameter format. - * @param type the type of hook. - * @param payload the hook payload - * @param instanceType the Bitbucket type that called the hook - * @param origin the origin of the event. - * @param serverURL special value for native Bitbucket Server hooks which don't expose the server URL in the payload. - */ - public void process(HookEventType type, String payload, BitbucketType instanceType, String origin, String serverURL) { - process(type, payload, instanceType, origin); - } - - /** - * To be called by implementations once the owner and the repository have been extracted from the payload. - * - * @param owner the repository owner as configured in the SCMSource - * @param repository the repository name as configured in the SCMSource - * @param mirrorId the mirror id if applicable, may be null - */ - protected void scmSourceReIndex(final String owner, final String repository, final String mirrorId) { - try (ACLContext context = ACL.as2(ACL.SYSTEM2)) { - boolean reindexed = false; - for (SCMSourceOwner scmOwner : SCMSourceOwners.all()) { - List sources = scmOwner.getSCMSources(); - for (SCMSource source : sources) { - // Search for the correct SCM source - if (source instanceof BitbucketSCMSource scmSource - && StringUtils.equalsIgnoreCase(scmSource.getRepoOwner(), owner) - && scmSource.getRepository().equals(repository) - && (mirrorId == null || StringUtils.equalsIgnoreCase(mirrorId, scmSource.getMirrorId()))) { - LOGGER.log(Level.INFO, "Multibranch project found, reindexing " + scmOwner.getName()); - // TODO: SCMSourceOwner.onSCMSourceUpdated is deprecated. We may explore options with an - // SCMEventListener extension and firing SCMSourceEvents. - scmOwner.onSCMSourceUpdated(source); - reindexed = true; - } - } - } - if (!reindexed) { - LOGGER.log(Level.INFO, "No multibranch project matching for reindex on {0}/{1}", new Object[] {owner, repository}); - } - } - } - - /** - * Implementations have to call this method when want propagate an - * {@link SCMHeadEvent} to the scm-api. - * - * @param event the to fire - * @param delaySeconds a delay in seconds to wait before propagate the - * event. If the given value is less than 0 than default will be - * used. - */ - protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { - if (delaySeconds == 0) { - SCMHeadEvent.fireNow(event); - } else { - SCMHeadEvent.fireLater(event, delaySeconds > 0 ? delaySeconds : BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); - } - } -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java deleted file mode 100644 index 893738275..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.hooks; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; -import hudson.RestrictedSince; -import jenkins.scm.api.SCMEvent; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - -@Restricted(NoExternalUse.class) -@RestrictedSince("933.3.0") -public class PullRequestHookProcessor extends HookProcessor { - - @Override - public void process(final HookEventType hookEvent, String payload, final BitbucketType instanceType, String origin) { - if (payload != null) { - BitbucketPullRequestEvent pull; - if (instanceType == BitbucketType.SERVER) { - // plugin webhook case - pull = BitbucketServerWebhookPayload.pullRequestEventFromPayload(payload); - } else { - pull = BitbucketCloudWebhookPayload.pullRequestEventFromPayload(payload); - } - if (pull != null) { - SCMEvent.Type eventType; - switch (hookEvent) { - case PULL_REQUEST_CREATED: - eventType = SCMEvent.Type.CREATED; - break; - case PULL_REQUEST_DECLINED, - PULL_REQUEST_MERGED: - eventType = SCMEvent.Type.REMOVED; - break; - default: - eventType = SCMEvent.Type.UPDATED; - break; - } - // assume updated as a catch-all type - notifyEvent(new PREvent(eventType, pull, origin, hookEvent), BitbucketSCMSource.getEventDelaySeconds()); - } - } - } -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java deleted file mode 100644 index 6cb45ea5f..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.hooks; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; -import hudson.RestrictedSince; -import java.util.logging.Level; -import java.util.logging.Logger; -import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMHeadEvent; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - -@Restricted(NoExternalUse.class) -@RestrictedSince("933.3.0") -public class PushHookProcessor extends HookProcessor { - - private static final Logger LOGGER = Logger.getLogger(PushHookProcessor.class.getName()); - - @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin) { - if (payload != null) { - BitbucketPushEvent push; - if (instanceType == BitbucketType.SERVER) { - // plugin webhook case - push = BitbucketServerWebhookPayload.pushEventFromPayload(payload); - } else { - push = BitbucketCloudWebhookPayload.pushEventFromPayload(payload); - } - if (push != null) { - if (push.getChanges().isEmpty()) { - final String owner = push.getRepository().getOwnerName(); - final String repository = push.getRepository().getRepositoryName(); - if (instanceType == BitbucketType.CLOUD || SCAN_ON_EMPTY_CHANGES) { - LOGGER.log(Level.INFO, "Received push hook with empty changes from Bitbucket. Processing indexing on {0}/{1}. " + - "You may skip this scan by adding the system property -D{2}=false on startup.", - new Object[]{owner, repository, SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME}); - scmSourceReIndex(owner, repository, null); - } else { - LOGGER.log(Level.INFO, "Received push hook with empty changes from Bitbucket for {0}/{1}. Skipping.", - new Object[]{owner, repository}); - } - } else { - SCMHeadEvent.Type type = null; - for (BitbucketPushEvent.Change change : push.getChanges()) { - if ((type == null || type == SCMEvent.Type.CREATED) && change.isCreated()) { - type = SCMEvent.Type.CREATED; - } else if ((type == null || type == SCMEvent.Type.REMOVED) && change.isClosed()) { - type = SCMEvent.Type.REMOVED; - } else { - type = SCMEvent.Type.UPDATED; - } - } - notifyEvent(new PushEvent(type, push, origin), BitbucketSCMSource.getEventDelaySeconds()); - } - } - } - } - -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java index 8cde8d2ba..203acc539 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java @@ -28,7 +28,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudHook; -import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketPluginWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhook; @@ -64,38 +63,19 @@ public class WebhookConfiguration { )); /** - * The list of events available in Bitbucket Server v7.x. + * The list of events available in Bitbucket Data Center for the minimum supported version. */ - private static final List NATIVE_SERVER_EVENTS_v7 = Collections.unmodifiableList(Arrays.asList( + private static final List NATIVE_SERVER_EVENTS = Collections.unmodifiableList(Arrays.asList( HookEventType.SERVER_REFS_CHANGED.getKey(), HookEventType.SERVER_PULL_REQUEST_OPENED.getKey(), HookEventType.SERVER_PULL_REQUEST_MERGED.getKey(), HookEventType.SERVER_PULL_REQUEST_DECLINED.getKey(), HookEventType.SERVER_PULL_REQUEST_DELETED.getKey(), - // only on v5.10 and above HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey(), - HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey(), - // only on v6.5 and above HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey(), - // only on v7.x and above HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey() )); - /** - * The list of events available in Bitbucket Server v6.5+. - */ - private static final List NATIVE_SERVER_EVENTS_v6_5 = Collections.unmodifiableList(NATIVE_SERVER_EVENTS_v7.subList(0, 8)); - - /** - * The list of events available in Bitbucket Server v6.x. Applies to v5.10+. - */ - private static final List NATIVE_SERVER_EVENTS_v6 = Collections.unmodifiableList(NATIVE_SERVER_EVENTS_v7.subList(0, 7)); - - /** - * The list of events available in Bitbucket Server v5.9-. - */ - private static final List NATIVE_SERVER_EVENTS_v5 = Collections.unmodifiableList(NATIVE_SERVER_EVENTS_v7.subList(0, 5)); - /** * The title of the webhook. */ @@ -143,7 +123,8 @@ boolean updateHook(BitbucketWebHook hook, BitbucketSCMSource owner) { } } else if (hook instanceof BitbucketServerWebhook serverHook) { String serverURL = owner.getServerUrl(); - String url = getServerWebhookURL(serverURL, BitbucketEndpointProvider.lookupEndpointJenkinsRootURL(owner.getServerUrl())); + BitbucketEndpoint endpoint = BitbucketEndpointProvider.lookupEndpoint(serverURL).orElseThrow(); + String url = getServerWebhookURL(serverURL, endpoint.getEndpointJenkinsRootURL()); if (!url.equals(serverHook.getUrl())) { serverHook.setUrl(url); @@ -152,11 +133,11 @@ boolean updateHook(BitbucketWebHook hook, BitbucketSCMSource owner) { List events = serverHook.getEvents(); if (events == null) { - serverHook.setEvents(getNativeServerEvents(serverURL)); + serverHook.setEvents(getNativeServerEvents(endpoint)); updated = true; - } else if (!events.containsAll(getNativeServerEvents(serverURL))) { + } else if (!events.containsAll(getNativeServerEvents(endpoint))) { Set newEvents = new TreeSet<>(events); - newEvents.addAll(getNativeServerEvents(serverURL)); + newEvents.addAll(getNativeServerEvents(endpoint)); serverHook.setEvents(new ArrayList<>(newEvents)); updated = true; } @@ -171,27 +152,29 @@ boolean updateHook(BitbucketWebHook hook, BitbucketSCMSource owner) { } public BitbucketWebHook getHook(BitbucketSCMSource owner) { - final String serverUrl = owner.getServerUrl(); - final String rootUrl = BitbucketEndpointProvider.lookupEndpointJenkinsRootURL(owner.getServerUrl()); + final String serverURL = owner.getServerUrl(); + BitbucketEndpoint endpoint = BitbucketEndpointProvider.lookupEndpoint(serverURL).orElseThrow(); + final String rootURL = endpoint.getEndpointJenkinsRootURL(); final String signatureSecret = getSecret(owner.getServerUrl()); - if (BitbucketApiUtils.isCloud(serverUrl)) { + if (BitbucketApiUtils.isCloud(serverURL)) { BitbucketCloudHook hook = new BitbucketCloudHook(); hook.setEvents(CLOUD_EVENTS); hook.setActive(true); hook.setDescription(description); - hook.setUrl(rootUrl + BitbucketSCMSourcePushHookReceiver.FULL_PATH); + hook.setUrl(rootURL + BitbucketSCMSourcePushHookReceiver.FULL_PATH); hook.setSecret(signatureSecret); return hook; } - - switch (BitbucketServerEndpoint.findWebhookImplementation(serverUrl)) { + return null; +/* + switch (BitbucketServerEndpoint.findWebhookImplementation(serverURL)) { case NATIVE: { BitbucketServerWebhook hook = new BitbucketServerWebhook(); hook.setActive(true); hook.setDescription(description); - hook.setEvents(getNativeServerEvents(serverUrl)); - hook.setUrl(getServerWebhookURL(serverUrl, rootUrl)); + hook.setEvents(getNativeServerEvents(endpoint)); + hook.setUrl(getServerWebhookURL(serverURL, rootURL)); hook.setSecret(signatureSecret); return hook; } @@ -201,11 +184,12 @@ public BitbucketWebHook getHook(BitbucketSCMSource owner) { BitbucketPluginWebhook hook = new BitbucketPluginWebhook(); hook.setActive(true); hook.setDescription(description); - hook.setUrl(getServerWebhookURL(serverUrl, rootUrl)); + hook.setUrl(getServerWebhookURL(serverURL, rootURL)); hook.setCommittersToIgnore(committersToIgnore); return hook; } } +*/ } @Nullable @@ -224,44 +208,16 @@ private String getSecret(@NonNull String serverURL) { return null; } - private static List getNativeServerEvents(String serverUrl) { - BitbucketServerEndpoint endpoint = BitbucketEndpointProvider - .lookupEndpoint(serverUrl, BitbucketServerEndpoint.class) - .orElse(null); - if (endpoint != null) { - switch (endpoint.getServerVersion()) { - case VERSION_5: - return NATIVE_SERVER_EVENTS_v5; - case VERSION_5_10: - return NATIVE_SERVER_EVENTS_v6; - case VERSION_6: - // plugin version 2.9.1 introduced VERSION_6 setting for Bitbucket but it - // actually applies - // to Version 5.10+. In order to preserve backwards compatibility, rather than - // remove - // VERSION_6, it will use the same list as 5.10 until such time a need arises - // for it to have its - // own list - return NATIVE_SERVER_EVENTS_v6; - case VERSION_6_5: - return NATIVE_SERVER_EVENTS_v6_5; - case VERSION_7: - default: - return NATIVE_SERVER_EVENTS_v7; - } - } - - // Not specifically v6, use v7. - // Better to give an error than quietly not register some events. - return NATIVE_SERVER_EVENTS_v7; + private static List getNativeServerEvents(BitbucketEndpoint endpoint) { + return NATIVE_SERVER_EVENTS; } - private static String getServerWebhookURL(String serverUrl, String rootUrl) { - return UriTemplate.buildFromTemplate(rootUrl) + private static String getServerWebhookURL(String serverURL, String rootURL) { + return UriTemplate.buildFromTemplate(rootURL) .template(BitbucketSCMSourcePushHookReceiver.FULL_PATH) .query("server_url") .build() - .set("server_url", serverUrl) + .set("server_url", serverURL) .expand(); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint.java index d11f4136b..16c27b882 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint.java @@ -26,13 +26,19 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointDescriptor; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhook; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin.PluginWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Util; +import java.util.Objects; import jenkins.authentication.tokens.api.AuthenticationTokens; import jenkins.model.Jenkins; import org.apache.commons.lang3.StringUtils; @@ -49,50 +55,33 @@ */ public abstract class AbstractBitbucketEndpoint implements BitbucketEndpoint { - /** - * {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. - */ + // keept for backward XStream compatibility + @Deprecated private boolean manageHooks; - - /** - * The {@link StandardCredentials#getId()} of the credentials to use for auto-management of hooks. - */ - @CheckForNull + @Deprecated private String credentialsId; - - /** - * {@code true} if and only if Jenkins have to verify the signature of all incoming hooks. - */ + @Deprecated private boolean enableHookSignature; - - /** - * The {@link StringCredentials#getId()} of the credentials to use to verify the signature of hooks. - */ - @CheckForNull + @Deprecated private String hookSignatureCredentialsId; - - /** - * Jenkins Server Root URL to be used by that Bitbucket endpoint. - * The global setting from Jenkins.get().getRootUrl() - * will be used if this field is null or equals an empty string. - * This variable is bound to the UI, so an empty value is saved - * and returned by getter as such. - */ + @Deprecated private String bitbucketJenkinsRootUrl; + @Deprecated + private String webhookImplementation; - /** - * Constructor. - * - * @param manageHooks {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. - * @param credentialsId The {@link StandardCredentials#getId()} of the credentials to use for - * auto-management of hooks. - */ - AbstractBitbucketEndpoint(boolean manageHooks, @CheckForNull String credentialsId, - boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) { - this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId); - this.credentialsId = manageHooks ? fixEmptyAndTrim(credentialsId) : null; - this.enableHookSignature = enableHookSignature && StringUtils.isNotBlank(hookSignatureCredentialsId); - this.hookSignatureCredentialsId = enableHookSignature ? fixEmptyAndTrim(hookSignatureCredentialsId) : null; + @NonNull + private BitbucketWebhook webhook; + + AbstractBitbucketEndpoint(@NonNull BitbucketWebhook webhook) { + this.webhook = Objects.requireNonNull(webhook); + } + + public @NonNull BitbucketWebhook getWebhook() { + return webhook; + } + + protected void setWebhook(@NonNull BitbucketWebhook webhook) { + this.webhook = webhook; } @Override @@ -109,7 +98,9 @@ public void setManageHooks(boolean manageHooks, String credentialsId) { */ @Deprecated(since = "936.4.0", forRemoval = true) @NonNull - public abstract String getServerUrl(); + public String getServerUrl() { + return this.getServerURL(); + } /** * A Jenkins Server Root URL should end with a slash to use with webhooks. @@ -154,12 +145,6 @@ public String getEndpointJenkinsRootURL() { return normalizeJenkinsRootURL(endpointURL); } - @NonNull - @Override - public String getRepositoryURL(@NonNull String repoOwner, @NonNull String repoSlug) { - return this.getRepositoryUrl(repoOwner, repoSlug); - } - @DataBoundSetter public void setBitbucketJenkinsRootUrl(String bitbucketJenkinsRootUrl) { if (manageHooks) { @@ -193,11 +178,7 @@ public boolean isEnableHookSignature() { @Deprecated(since = "936.4.0", forRemoval = true) @NonNull public String getEndpointJenkinsRootUrl() { - if (StringUtils.isBlank(bitbucketJenkinsRootUrl)) { - return DisplayURLProvider.get().getRoot(); - } else { - return bitbucketJenkinsRootUrl; - } + return getEndpointJenkinsRootURL(); } /** @@ -207,8 +188,11 @@ public String getEndpointJenkinsRootUrl() { * @param repository the repository. * @return the user facing URL of the specified repository. */ + @Deprecated(since = "937.0.0", forRemoval = true) @NonNull - public abstract String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository); + public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + return this.getRepositoryURL(repoOwner, repository); + } /** * Returns {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. @@ -265,6 +249,23 @@ public BitbucketAuthenticator authenticator() { return AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(getServerURL()), credentials()); } + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Only non-null after we set them here!") + protected Object readResolve() { + if (webhook == null) { + if ("NATIVE".equals(webhookImplementation)) { + webhook = new ServerWebhook(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + ((ServerWebhook) webhook).setEndpointJenkinsRootURL(bitbucketJenkinsRootUrl); + } else if ("PLUGIN".equals(webhookImplementation)) { + webhook = new PluginWebhook(manageHooks, credentialsId); + ((PluginWebhook) webhook).setEndpointJenkinsRootURL(bitbucketJenkinsRootUrl); + } else { + webhook = new CloudWebhook(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + ((CloudWebhook) webhook).setEndpointJenkinsRootURL(bitbucketJenkinsRootUrl); + } + } + return this; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint.java index 78934ab1b..aa7dee26f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint.java @@ -25,16 +25,15 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointDescriptor; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhook; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; -import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; import com.damnhandy.uri.template.UriTemplate; -import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.util.FormValidation; import java.util.List; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.verb.POST; @@ -69,38 +68,12 @@ public class BitbucketCloudEndpoint extends AbstractBitbucketEndpoint { * Default constructor. */ public BitbucketCloudEndpoint() { - this(false, 0, 0, false, null, false, null); + this(false, 0, 0, new CloudWebhook(false, null, false, null)); } - @Deprecated(since = "936.3.1") - public BitbucketCloudEndpoint(boolean enableCache, int teamCacheDuration, int repositoriesCacheDuration, - boolean manageHooks, @CheckForNull String credentialsId) { - this(enableCache, teamCacheDuration, repositoriesCacheDuration, manageHooks, credentialsId, false, null); - } - - /** - * Constructor. - * - * @param enableCache {@code true} if caching should be used to reduce - * requests to Bitbucket. - * @param teamCacheDuration How long, in minutes, to cache the team - * response. - * @param repositoriesCacheDuration How long, in minutes, to cache the - * repositories response. - * @param manageHooks {@code true} if and only if Jenkins is supposed to - * auto-manage hooks for this end-point. - * @param credentialsId The {@link StandardCredentials#getId()} of the - * credentials to use for auto-management of hooks. - * @param enableHookSignature {@code true} hooks that comes Bitbucket Data - * Center are signed. - * @param hookSignatureCredentialsId The {@link StringCredentials#getId()} of the - * credentials to use for verify the signature of payload. - */ @DataBoundConstructor - public BitbucketCloudEndpoint(boolean enableCache, int teamCacheDuration, int repositoriesCacheDuration, - boolean manageHooks, @CheckForNull String credentialsId, - boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) { - super(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + public BitbucketCloudEndpoint(boolean enableCache, int teamCacheDuration, int repositoriesCacheDuration, @NonNull BitbucketWebhook webhook) { + super(webhook); this.enableCache = enableCache; this.teamCacheDuration = teamCacheDuration; this.repositoriesCacheDuration = repositoriesCacheDuration; @@ -126,19 +99,9 @@ public String getDisplayName() { return Messages.BitbucketCloudEndpoint_displayName(); } - /** - * {@inheritDoc} - */ - @Override - @NonNull - @Deprecated(since = "936.4.0", forRemoval = true) - public String getServerUrl() { - return SERVER_URL; - } - @Override public String getServerURL() { - return getServerUrl(); + return SERVER_URL; } /** @@ -146,7 +109,7 @@ public String getServerURL() { */ @NonNull @Override - public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + public String getRepositoryURL(@NonNull String repoOwner, @NonNull String repository) { UriTemplate template = UriTemplate .fromTemplate(SERVER_URL + "{/owner,repo}") .set("owner", repoOwner) @@ -191,11 +154,4 @@ public FormValidation doClear() { } } - private Object readResolve() { - if (getBitbucketJenkinsRootUrl() != null) { - setBitbucketJenkinsRootUrl(getBitbucketJenkinsRootUrl()); - } - return this; - } - } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint.java index 03998889a..c4856b8ee 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint.java @@ -26,32 +26,26 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointDescriptor; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhook; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerVersion; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; -import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.damnhandy.uri.template.UriTemplate; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.net.MalformedURLException; import java.net.URL; -import java.util.Objects; import jenkins.scm.api.SCMName; import org.apache.commons.lang3.StringUtils; -import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; - -import static java.util.Objects.requireNonNull; /** * Represents a Bitbucket Server instance. @@ -74,10 +68,8 @@ public class BitbucketServerEndpoint extends AbstractBitbucketEndpoint { }; @NonNull - public static BitbucketServerWebhookImplementation findWebhookImplementation(String serverURL) { - return BitbucketEndpointProvider.lookupEndpoint(serverURL, BitbucketServerEndpoint.class) - .map(BitbucketServerEndpoint::getWebhookImplementation) - .orElse(BitbucketServerWebhookImplementation.NATIVE); + public static boolean supportsMirror(String serverURL) { + return false; // depends on webhook type } @NonNull @@ -85,7 +77,7 @@ public static BitbucketServerVersion findServerVersion(String serverURL) { return BitbucketEndpointProvider .lookupEndpoint(serverURL, BitbucketServerEndpoint.class) .map(endpoint -> endpoint.getServerVersion()) - .orElse(BitbucketServerVersion.VERSION_7); + .orElse(BitbucketServerVersion.getMinSupportedVersion()); } /** @@ -98,15 +90,12 @@ public static BitbucketServerVersion findServerVersion(String serverURL) { * The URL of this Bitbucket Server. */ @NonNull - private final String serverUrl; - - @NonNull - private BitbucketServerWebhookImplementation webhookImplementation = BitbucketServerWebhookImplementation.PLUGIN; + private final String serverURL; /** * The server version for this endpoint. */ - private BitbucketServerVersion serverVersion = BitbucketServerVersion.VERSION_7; + private BitbucketServerVersion serverVersion = BitbucketServerVersion.getMinSupportedVersion(); /** * Whether to always call the can merge api when retrieving pull requests. @@ -122,40 +111,25 @@ public static BitbucketServerVersion findServerVersion(String serverURL) { * Default constructor. * @param serverURL */ - public BitbucketServerEndpoint(@NonNull String serverURL) { - this(null, serverURL, false, null, false, null); - } - - @Deprecated(since = "936.3.1") - public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String serverUrl, - boolean manageHooks, @CheckForNull String credentialsId) { - this(displayName, serverUrl, manageHooks, credentialsId, false, null); + public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String serverURL) { + this(displayName, serverURL, new ServerWebhook(false, null, false, null)); } /** * Constructor. * * @param displayName Optional name to use to describe the end-point. - * @param serverUrl The URL of this Bitbucket Server - * @param manageHooks {@code true} if and only if Jenkins is supposed to - * auto-manage hooks for this end-point. - * @param credentialsId The {@link StandardCredentials#getId()} of the - * credentials to use for auto-management of hooks. - * @param enableHookSignature {@code true} hooks that comes Bitbucket Data - * Center are signed. - * @param hookSignatureCredentialsId The {@link StringCredentials#getId()} of the - * credentials to use for verify the signature of payload. + * @param serverURL The URL of this Bitbucket Server + * @param webhook implementation to work for this end-point. */ @DataBoundConstructor - public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String serverUrl, - boolean manageHooks, @CheckForNull String credentialsId, - boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) { - super(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String serverURL, @NonNull BitbucketWebhook webhook) { + super(webhook); // use fixNull to silent nullability check - this.serverUrl = Util.fixNull(URLUtils.normalizeURL(serverUrl)); + this.serverURL = Util.fixNull(URLUtils.normalizeURL(serverURL)); this.displayName = StringUtils.isBlank(displayName) - ? SCMName.fromUrl(this.serverUrl, COMMON_PREFIX_HOSTNAMES) - : displayName.trim(); + ? SCMName.fromUrl(this.serverURL, COMMON_PREFIX_HOSTNAMES) + : displayName.trim(); } public boolean isCallCanMerge() { @@ -188,8 +162,12 @@ public BitbucketServerVersion getServerVersion() { } @DataBoundSetter - public void setServerVersion(@NonNull BitbucketServerVersion serverVersion) { - this.serverVersion = Objects.requireNonNull(serverVersion); + public void setServerVersion(@CheckForNull BitbucketServerVersion serverVersion) { + this.serverVersion = serverVersion; + if (serverVersion == null || BitbucketServerVersion.getMinSupportedVersion().compareTo(this.serverVersion) < 0) { + // force value to the minimum supported version + this.serverVersion = BitbucketServerVersion.getMinSupportedVersion(); + } } /** @@ -200,28 +178,9 @@ public String getDisplayName() { return displayName; } - /** - * {@inheritDoc} - */ - @Override - @NonNull - @Deprecated(since = "936.4.0", forRemoval = true) - public String getServerUrl() { - return serverUrl; - } - @Override public String getServerURL() { - return getServerUrl(); - } - - /** - * {@inheritDoc} - */ - @NonNull - @Override - public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { - return getRepositoryURL(repoOwner, repository); + return serverURL; } /** @@ -231,36 +190,20 @@ public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String reposi @Override public String getRepositoryURL(@NonNull String repoOwner, @NonNull String repository) { UriTemplate template = UriTemplate - .fromTemplate(serverUrl + "/{userOrProject}/{owner}/repos/{repo}") + .fromTemplate(serverURL + "/{userOrProject}/{owner}/repos/{repo}") .set("repo", repository); return repoOwner.startsWith("~") ? template.set("userOrProject", "users").set("owner", repoOwner.substring(1)).expand() : template.set("userOrProject", "projects").set("owner", repoOwner).expand(); } - @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Only non-null after we set them here!") - private Object readResolve() { - if (webhookImplementation == null) { - webhookImplementation = BitbucketServerWebhookImplementation.PLUGIN; - } - if (getBitbucketJenkinsRootUrl() != null) { - setBitbucketJenkinsRootUrl(getBitbucketJenkinsRootUrl()); - } + @Override +// @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Only non-null after we set them here!") + protected Object readResolve() { if (serverVersion == null) { - serverVersion = BitbucketServerVersion.VERSION_7; + serverVersion = BitbucketServerVersion.getMinSupportedVersion(); } - - return this; - } - - @NonNull - public BitbucketServerWebhookImplementation getWebhookImplementation() { - return webhookImplementation; - } - - @DataBoundSetter - public void setWebhookImplementation(@NonNull BitbucketServerWebhookImplementation webhookImplementation) { - this.webhookImplementation = requireNonNull(webhookImplementation); + return super.readResolve(); } /** @@ -269,16 +212,6 @@ public void setWebhookImplementation(@NonNull BitbucketServerWebhookImplementati @Extension public static class DescriptorImpl extends BitbucketEndpointDescriptor { - @Restricted(NoExternalUse.class) // stapler - @RequirePOST - public FormValidation doCheckEnableHookSignature(@QueryParameter BitbucketServerWebhookImplementation webhookImplementation, - @QueryParameter boolean enableHookSignature) { - if (enableHookSignature && webhookImplementation == BitbucketServerWebhookImplementation.PLUGIN) { - return FormValidation.error("Signature verification not supported for PLUGIN webhook"); - } - return FormValidation.ok(); - } - /** * {@inheritDoc} */ @@ -288,16 +221,6 @@ public String getDisplayName() { return Messages.BitbucketServerEndpoint_displayName(); } - @Restricted(NoExternalUse.class) - public ListBoxModel doFillWebhookImplementationItems() { - ListBoxModel items = new ListBoxModel(); - for (BitbucketServerWebhookImplementation webhookImplementation : BitbucketServerWebhookImplementation.values()) { - items.add(webhookImplementation, webhookImplementation.name()); - } - - return items; - } - @Restricted(NoExternalUse.class) public ListBoxModel doFillServerVersionItems() { ListBoxModel items = new ListBoxModel(); @@ -314,7 +237,7 @@ public ListBoxModel doFillServerVersionItems() { * @param value the URL to check. * @return the validation results. */ - public static FormValidation doCheckServerUrl(@QueryParameter String value) { + public static FormValidation doCheckServerURL(@QueryParameter String value) { try { new URL(value); } catch (MalformedURLException e) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook.java new file mode 100644 index 000000000..75e86cf46 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook.java @@ -0,0 +1,168 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Falco Nikolas + * + * 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.jenkins.plugins.bitbucket.impl.webhook; + +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookDescriptor; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import java.net.MalformedURLException; +import java.net.URL; +import jenkins.model.Jenkins; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import static hudson.Util.fixEmptyAndTrim; + +@Restricted(NoExternalUse.class) +public abstract class AbstractWebhook implements BitbucketWebhook { + + /** + * {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + */ + private boolean manageHooks; + + /** + * The {@link StandardCredentials#getId()} of the credentials to use for auto-management of hooks. + */ + @CheckForNull + private String credentialsId; + + /** + * {@code true} if and only if Jenkins have to verify the signature of all incoming hooks. + */ + private final boolean enableHookSignature; + + /** + * The {@link StringCredentials#getId()} of the credentials to use to verify the signature of hooks. + */ + @CheckForNull + private final String hookSignatureCredentialsId; + + /** + * Jenkins Server Root URL to be used by that Bitbucket endpoint. + * The global setting from Jenkins.get().getRootUrl() + * will be used if this field is null or equals an empty string. + * This variable is bound to the UI, so an empty value is saved + * and returned by getter as such. + */ + private String endpointJenkinsRootURL; + + protected AbstractWebhook(boolean manageHooks, @CheckForNull String credentialsId, + boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) { + this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId); + this.credentialsId = manageHooks ? fixEmptyAndTrim(credentialsId) : null; + this.enableHookSignature = enableHookSignature && StringUtils.isNotBlank(hookSignatureCredentialsId); + this.hookSignatureCredentialsId = enableHookSignature ? fixEmptyAndTrim(hookSignatureCredentialsId) : null; + } + + /** + * Returns {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * + * @return {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + */ + public final boolean isManageHooks() { + return manageHooks; + } + + /** + * Returns the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + * + * @return the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + */ + @CheckForNull + public final String getCredentialsId() { + return credentialsId; + } + + @CheckForNull + public String getHookSignatureCredentialsId() { + return hookSignatureCredentialsId; + } + + public boolean isEnableHookSignature() { + return enableHookSignature; + } + + public String getEndpointJenkinsRootURL() { + return endpointJenkinsRootURL; + } + + @DataBoundSetter + public void setEndpointJenkinsRootURL(@CheckForNull String endpointJenkinsRootURL) { + this.endpointJenkinsRootURL = fixEmptyAndTrim(endpointJenkinsRootURL); + } + + @Override + public String getDisplayName() { + return Messages.ServerWebhookImplementation_displayName(); + } + + @Override + public String getId() { + return "SERVER"; + } + + @SuppressWarnings("unchecked") + @Override + public Descriptor getDescriptor() { + return Jenkins.get().getDescriptorOrDie(getClass()); + } + + + public abstract static class AbstractBitbucketWebhookDescriptorImpl extends BitbucketWebhookDescriptor { + + @RequirePOST + public static FormValidation doCheckEndpointJenkinsRootURL(@QueryParameter String value) { + checkPermission(); + String url = Util.fixEmptyAndTrim(value); + if (url == null) { + return FormValidation.ok(); + } + try { + new URL(url); + } catch (MalformedURLException e) { + return FormValidation.error("Invalid URL: " + e.getMessage()); + } + return FormValidation.ok(); + } + + protected static Jenkins checkPermission() { + Jenkins jenkins = Jenkins.get(); + jenkins.checkPermission(Jenkins.MANAGE); + return jenkins; + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessor.java new file mode 100644 index 000000000..02886939d --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessor.java @@ -0,0 +1,187 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessorException; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketType; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.security.ACL; +import hudson.security.ACLContext; +import hudson.util.Secret; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.SCMSourceOwners; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import static org.apache.commons.lang.StringUtils.trimToNull; + +/** + * Abstract hook processor. + * + * Add new hook processors by extending this class and implement {@link #process(HookEventType, String, BitbucketType, String)}, + * extract details from the hook payload and then fire an {@link jenkins.scm.api.SCMEvent} to dispatch it to the SCM API. + */ +@Restricted(NoExternalUse.class) +public abstract class AbstractWebhookProcessor implements BitbucketWebhookProcessor { + + protected static final String REQUEST_ID_CLOUD_HEADER = "X-Request-UUID"; + protected static final String REQUEST_ID_SERVER_HEADER = "X-Request-Id"; + protected static final String SIGNATURE_HEADER = "X-Hub-Signature"; + protected static final String EVENT_TYPE_HEADER = "X-Event-Key"; + protected static final String SERVER_URL_PARAMETER = "server_url"; + + private static final Logger LOGGER = Logger.getLogger(AbstractWebhookProcessor.class.getName()); + + /** + * To be called by implementations once the owner and the repository have been extracted from the payload. + * + * @param owner the repository owner as configured in the SCMSource + * @param repository the repository name as configured in the SCMSource + * @param mirrorId the mirror id if applicable, may be null + */ + protected void scmSourceReIndex(final String owner, final String repository, final String mirrorId) { + try (ACLContext context = ACL.as2(ACL.SYSTEM2)) { + boolean reindexed = false; + for (SCMSourceOwner scmOwner : SCMSourceOwners.all()) { + List sources = scmOwner.getSCMSources(); + for (SCMSource source : sources) { + // Search for the correct SCM source + if (source instanceof BitbucketSCMSource scmSource + && StringUtils.equalsIgnoreCase(scmSource.getRepoOwner(), owner) + && scmSource.getRepository().equals(repository) + && (mirrorId == null || StringUtils.equalsIgnoreCase(mirrorId, scmSource.getMirrorId()))) { + LOGGER.log(Level.INFO, "Multibranch project found, reindexing " + scmOwner.getName()); + // TODO: SCMSourceOwner.onSCMSourceUpdated is deprecated. We may explore options with an + // SCMEventListener extension and firing SCMSourceEvents. + scmOwner.onSCMSourceUpdated(source); + reindexed = true; + } + } + } + if (!reindexed) { + LOGGER.log(Level.INFO, "No multibranch project matching for reindex on {0}/{1}", new Object[] {owner, repository}); + } + } + } + + @NonNull + @Override + public String getServerURL(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + String serverURL = parameters.get(SERVER_URL_PARAMETER).stream() + .findFirst() + .orElse(null); + if (StringUtils.isBlank(serverURL)) { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_BAD_REQUEST, SERVER_URL_PARAMETER + " query parameter not found or empty. Refer to the user documentation on how configure the webHook in Bitbucket at https://github.com/jenkinsci/bitbucket-branch-source-plugin/blob/master/docs/USER_GUIDE.adoc#webhooks-registering"); + } + return serverURL; + } + + @NonNull + @Override + public String getEventType(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + String eventType = headers.get(EVENT_TYPE_HEADER); + if (StringUtils.isEmpty(eventType)) { + throw new IllegalStateException(EVENT_TYPE_HEADER + " is missing or empty, this processor should not proceed after canHandle method. Please fill an issue at https://issues.jenkins.io reporting this stacktrace."); + } + return eventType; + } + + @Override + public void verifyPayload(@NonNull Map headers, @NonNull String body, BitbucketEndpoint endpoint) { + if (headers.containsKey(SIGNATURE_HEADER)) { + StringCredentials signatureCredentials = endpoint.hookSignatureCredentials(); + if (signatureCredentials != null) { + String signatureHeader = headers.get(SIGNATURE_HEADER); + String bitbucketAlgorithm = trimToNull(StringUtils.substringBefore(signatureHeader, "=")); + String bitbucketSignature = trimToNull(StringUtils.substringAfter(signatureHeader, "=")); + HmacAlgorithms algorithm = getAlgorithm(bitbucketAlgorithm); + if (algorithm == null) { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_FORBIDDEN, "Signature " + bitbucketAlgorithm + " not supported"); + } + HmacUtils util; + try { + String key = Secret.toString(signatureCredentials.getSecret()); + util = new HmacUtils(algorithm, key.getBytes(StandardCharsets.UTF_8)); + byte[] digest = util.hmac(body); + if (!MessageDigest.isEqual(Hex.decodeHex(bitbucketSignature), digest)) { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_FORBIDDEN, "Signature verification failed"); + } + } catch (IllegalArgumentException e) { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_BAD_REQUEST, "Signature method not supported: " + algorithm); + } catch (DecoderException e) { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_BAD_REQUEST, "Hex signature can not be decoded: " + bitbucketSignature); + } + } else { + String hookId = headers.get("X-Hook-UUID"); + String requestId = ObjectUtils.firstNonNull(headers.get("X-Request-UUID"), headers.get("X-Request-Id")); + String hookSignatureCredentialsId = endpoint.getHookSignatureCredentialsId(); + LOGGER.log(Level.WARNING, "No credentials {0} found to verify the signature of incoming webhook {1} request {2}", new Object[] { hookSignatureCredentialsId, hookId, requestId }); + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_FORBIDDEN, "No credentials " + hookSignatureCredentialsId + " found in Jenkins to verify the signature"); + } + } else { + throw new BitbucketWebhookProcessorException(HttpServletResponse.SC_FORBIDDEN, "Payload has not be signed, configure the webHook secret in Bitbucket as documented at https://github.com/jenkinsci/bitbucket-branch-source-plugin/blob/master/docs/USER_GUIDE.adoc#webhooks-registering"); + } + } + + protected String getOrigin(Map context) { + return StringUtils.firstNonBlank((String) context.get("origin"), "unknow"); + } + + @CheckForNull + private HmacAlgorithms getAlgorithm(String algorithm) { + switch (StringUtils.lowerCase(algorithm)) { + case "sha1": + return HmacAlgorithms.HMAC_SHA_1; + case "sha256": + return HmacAlgorithms.HMAC_SHA_256; + case "sha384": + return HmacAlgorithms.HMAC_SHA_384; + case "sha512": + return HmacAlgorithms.HMAC_SHA_512; + default: + return null; + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/AbstractSCMHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/AbstractSCMHeadEvent.java new file mode 100644 index 000000000..fa6cbbe8d --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/AbstractSCMHeadEvent.java @@ -0,0 +1,115 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPushEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.scm.SCM; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMNavigator; +import org.apache.commons.lang3.StringUtils; + +public abstract class AbstractSCMHeadEvent

extends SCMHeadEvent

{ + + AbstractSCMHeadEvent(Type type, P payload, String origin) { + super(type, payload, origin); + } + + @Override + public boolean isMatch(@NonNull SCMNavigator navigator) { + if (!(navigator instanceof BitbucketSCMNavigator)) { + return false; + } + BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; + if (!isProjectKeyMatch(bbNav.getProjectKey())) { + return false; + } + + if (!isServerURLMatch(bbNav.getServerUrl())) { + return false; + } + return StringUtils.equalsIgnoreCase(bbNav.getRepoOwner(), getRepository().getOwnerName()); + } + + protected abstract BitbucketRepository getRepository(); + + private boolean isProjectKeyMatch(String projectKey) { + if (StringUtils.isBlank(projectKey)) { + return true; + } + BitbucketRepository repository = getRepository(); + if (repository.getProject() != null) { + return projectKey.equals(repository.getProject().getKey()); + } + return true; + } + + protected boolean isServerURLMatch(String serverURL) { + if (serverURL == null || BitbucketApiUtils.isCloud(serverURL)) { + // this is a Bitbucket cloud navigator + if (getPayload() instanceof BitbucketServerPullRequestEvent || getPayload() instanceof BitbucketServerPushEvent) { + return false; + } + } else { + // this is a Bitbucket server navigator + if (getPayload() instanceof BitbucketCloudPullRequestEvent || getPayload() instanceof BitbucketCloudPushEvent) { + return false; + } + Map> links = getRepository().getLinks(); + if (links != null && links.containsKey("self")) { + boolean matches = false; + for (BitbucketHref link: links.get("self")) { + try { + URI navUri = new URI(serverURL); + URI evtUri = new URI(link.getHref()); + if (navUri.getHost().equalsIgnoreCase(evtUri.getHost())) { + matches = true; + break; + } + } catch (URISyntaxException e) { + // ignore + } + } + return matches; + } + } + return true; + } + + @Override + public boolean isMatch(@NonNull SCM scm) { + return false; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPREvent.java similarity index 94% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPREvent.java index 4a1906e58..96b520346 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPREvent.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; @@ -30,6 +30,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Collections; @@ -48,10 +50,10 @@ import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; -final class PREvent extends AbstractSCMHeadEvent implements HasPullRequests { +final class CloudPREvent extends AbstractSCMHeadEvent implements HasPullRequests { private final HookEventType hookEvent; - PREvent(Type type, BitbucketPullRequestEvent payload, + CloudPREvent(Type type, BitbucketPullRequestEvent payload, String origin, HookEventType hookEvent) { super(type, payload, origin); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessor.java new file mode 100644 index 000000000..781742fe7 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessor.java @@ -0,0 +1,90 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.RestrictedSince; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class CloudPullRequestWebhookProcessor extends AbstractWebhookProcessor { + + private static final List supportedEvents = List.of( + HookEventType.PULL_REQUEST_CREATED.getKey(), // needed to create job + HookEventType.PULL_REQUEST_DECLINED.getKey(), // needed to remove job + HookEventType.PULL_REQUEST_MERGED.getKey(), // needed to remove job + HookEventType.PULL_REQUEST_UPDATED.getKey()); // needed to update git content and trigger build job + + @Override + public boolean canHandle(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey(REQUEST_ID_CLOUD_HEADER) + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && !parameters.containsKey(SERVER_URL_PARAMETER); + } + + @NonNull + @Override + public String getServerURL(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + return BitbucketCloudEndpoint.SERVER_URL; + } + + @Override + public void process(@NonNull String hookEventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { + HookEventType hookEvent = HookEventType.fromString(hookEventType); + BitbucketPullRequestEvent pull = BitbucketCloudWebhookPayload.pullRequestEventFromPayload(payload); + if (pull != null) { + SCMEvent.Type eventType; + switch (hookEvent) { + case PULL_REQUEST_CREATED: + eventType = SCMEvent.Type.CREATED; + break; + case PULL_REQUEST_DECLINED, + PULL_REQUEST_MERGED: + eventType = SCMEvent.Type.REMOVED; + break; + default: + eventType = SCMEvent.Type.UPDATED; + break; + } + // assume updated as a catch-all type + notifyEvent(new CloudPREvent(eventType, pull, getOrigin(context), hookEvent), BitbucketSCMSource.getEventDelaySeconds()); + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushEvent.java similarity index 95% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushEvent.java index cacdd6389..d1ef7f136 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushEvent.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; @@ -41,9 +41,9 @@ import jenkins.scm.api.SCMSource; import org.apache.commons.lang3.StringUtils; -final class PushEvent extends AbstractSCMHeadEvent { +final class CloudPushEvent extends AbstractSCMHeadEvent { - PushEvent(Type type, BitbucketPushEvent payload, String origin) { + CloudPushEvent(Type type, BitbucketPushEvent payload, String origin) { super(type, payload, origin); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessor.java new file mode 100644 index 000000000..78669ccb9 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessor.java @@ -0,0 +1,89 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.RestrictedSince; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class CloudPushWebhookProcessor extends AbstractWebhookProcessor { + + private static final List supportedEvents = List.of( + HookEventType.PUSH.getKey()); + + @Override + public boolean canHandle(Map headers, MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey(REQUEST_ID_CLOUD_HEADER) + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && !parameters.containsKey(SERVER_URL_PARAMETER); + } + + @NonNull + @Override + public String getServerURL(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + return BitbucketCloudEndpoint.SERVER_URL; + } + + @Override + public void process(@NonNull String hookEventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { + BitbucketPushEvent push = BitbucketCloudWebhookPayload.pushEventFromPayload(payload); + if (push != null) { + if (push.getChanges().isEmpty()) { + final String owner = push.getRepository().getOwnerName(); + final String repository = push.getRepository().getRepositoryName(); + scmSourceReIndex(owner, repository, null); + } else { + SCMEvent.Type type = null; + for (BitbucketPushEvent.Change change : push.getChanges()) { + if ((type == null || type == SCMEvent.Type.CREATED) && change.isCreated()) { + type = SCMEvent.Type.CREATED; + } else if ((type == null || type == SCMEvent.Type.REMOVED) && change.isClosed()) { + type = SCMEvent.Type.REMOVED; + } else { + type = SCMEvent.Type.UPDATED; + } + } + notifyEvent(new CloudPushEvent(type, push, getOrigin(context)), BitbucketSCMSource.getEventDelaySeconds()); + } + } + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhook.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhook.java new file mode 100644 index 000000000..7e30543c1 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhook.java @@ -0,0 +1,114 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Falco Nikolas + * + * 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.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.Messages; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import hudson.Extension; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +public class CloudWebhook extends AbstractWebhook { + + public CloudWebhook(boolean manageHooks, String credentialsId) { + this(manageHooks, credentialsId, false, null); + } + + @DataBoundConstructor + public CloudWebhook(boolean manageHooks, String credentialsId, boolean enableHookSignature, String hookSignatureCredentialsId) { + super(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + } + + @Override + public String getDisplayName() { + return Messages.CloudWebhookImplementation_displayName(); + } + + @Override + public String getId() { + return "CLOUD"; + } + + @Symbol("cloudWebhook") + @Extension + public static class DescriptorImpl extends AbstractBitbucketWebhookDescriptorImpl { + + @Override + public String getDisplayName() { + return "Native Cloud"; + } + + @Override + public boolean isApplicable(String serverURL) { + return BitbucketApiUtils.isCloud(serverURL); + } + + /** + * Stapler form completion. + * + * @param credentialsId selected credentials. + * @param serverURL the server URL. + * @return the available credentials. + */ + @RequirePOST + public ListBoxModel doFillCredentialsIdItems(@QueryParameter(fixEmpty = true) String credentialsId) { + Jenkins jenkins = checkPermission(); + return BitbucketCredentialsUtils.listCredentials(jenkins, BitbucketCloudEndpoint.SERVER_URL, credentialsId); + } + + /** + * Stapler form completion. + * + * @param hookSignatureCredentialsId selected hook signature credentials. + * @param serverURL the server URL. + * @return the available credentials. + */ + @RequirePOST + public ListBoxModel doFillHookSignatureCredentialsIdItems(@QueryParameter(fixEmpty = true) String hookSignatureCredentialsId) { + Jenkins jenkins = checkPermission(); + StandardListBoxModel result = new StandardListBoxModel(); + result.includeMatchingAs(ACL.SYSTEM2, + jenkins, + StringCredentials.class, + URIRequirementBuilder.fromUri(BitbucketCloudEndpoint.SERVER_URL).build(), + CredentialsMatchers.always()); + if (hookSignatureCredentialsId != null) { + result.includeCurrentValue(hookSignatureCredentialsId); + } + return result; + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/AbstractSCMHeadEvent.java similarity index 90% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/AbstractSCMHeadEvent.java index 47777cd75..3bf0f5c44 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/AbstractSCMHeadEvent.java @@ -21,14 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPushEvent; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPushEvent; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.scm.SCM; import java.net.URI; @@ -39,9 +41,10 @@ import jenkins.scm.api.SCMNavigator; import org.apache.commons.lang3.StringUtils; +@Deprecated(since = "937.0.0") abstract class AbstractSCMHeadEvent

extends SCMHeadEvent

{ - protected AbstractSCMHeadEvent(Type type, P payload, String origin) { + AbstractSCMHeadEvent(Type type, P payload, String origin) { super(type, payload, origin); } @@ -77,12 +80,12 @@ private boolean isProjectKeyMatch(String projectKey) { protected boolean isServerURLMatch(String serverURL) { if (serverURL == null || BitbucketApiUtils.isCloud(serverURL)) { // this is a Bitbucket cloud navigator - if (getPayload() instanceof BitbucketServerPullRequestEvent) { + if (getPayload() instanceof BitbucketServerPullRequestEvent || getPayload() instanceof BitbucketServerPushEvent) { return false; } } else { // this is a Bitbucket server navigator - if (getPayload() instanceof BitbucketCloudPullRequestEvent) { + if (getPayload() instanceof BitbucketCloudPullRequestEvent || getPayload() instanceof BitbucketCloudPushEvent) { return false; } Map> links = getRepository().getLinks(); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPREvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPREvent.java new file mode 100644 index 000000000..16d60702a --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPREvent.java @@ -0,0 +1,148 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; + +import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; +import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; + +@Deprecated(since = "937.0.0") +final class PluginPREvent extends AbstractSCMHeadEvent implements HasPullRequests { + private final HookEventType hookEvent; + + PluginPREvent(Type type, BitbucketPullRequestEvent payload, + String origin, + HookEventType hookEvent) { + super(type, payload, origin); + this.hookEvent = hookEvent; + } + + @Override + protected BitbucketRepository getRepository() { + return getPayload().getRepository(); + } + + @NonNull + @Override + public String getSourceName() { + return getRepository().getRepositoryName(); + } + + @NonNull + @Override + @SuppressFBWarnings(value = "SBSC_USE_STRINGBUFFER_CONCATENATION", justification = "false positive, the scope of branchName variable is inside the for cycle, no string contatenation happens into a loop") + public Map heads(@NonNull SCMSource source) { + if (!(source instanceof BitbucketSCMSource)) { + return Collections.emptyMap(); + } + BitbucketSCMSource src = (BitbucketSCMSource) source; + if (!isServerURLMatch(src.getServerUrl())) { + return Collections.emptyMap(); + } + BitbucketRepository repository = getRepository(); + if (!src.getRepoOwner().equalsIgnoreCase(repository.getOwnerName())) { + return Collections.emptyMap(); + } + if (!src.getRepository().equalsIgnoreCase(repository.getRepositoryName())) { + return Collections.emptyMap(); + } + + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(src.getTraits()); + if (!ctx.wantPRs()) { + // doesn't want PRs, let the push event handle origin branches + return Collections.emptyMap(); + } + BitbucketPullRequest pull = getPayload().getPullRequest(); + String pullRepoOwner = pull.getSource().getRepository().getOwnerName(); + String pullRepository = pull.getSource().getRepository().getRepositoryName(); + SCMHeadOrigin headOrigin = src.originOf(pullRepoOwner, pullRepository); + Set strategies = + headOrigin == SCMHeadOrigin.DEFAULT + ? ctx.originPRStrategies() + : ctx.forkPRStrategies(); + Map result = new HashMap<>(strategies.size()); + for (ChangeRequestCheckoutStrategy strategy : strategies) { + String branchName = "PR-" + pull.getId(); + if (strategies.size() > 1) { + branchName = branchName + "-" + strategy.name().toLowerCase(Locale.ENGLISH); + } + String originalBranchName = pull.getSource().getBranch().getName(); + PullRequestSCMHead head = new PullRequestSCMHead( + branchName, + pullRepoOwner, + pullRepository, + originalBranchName, + pull, + headOrigin, + strategy + ); + if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { + // special case for repo being deleted + result.put(head, null); + } else { + String targetHash = pull.getDestination().getCommit().getHash(); + String pullHash = pull.getSource().getCommit().getHash(); + + SCMRevision revision = new PullRequestSCMRevision(head, + new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), + new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash) + ); + result.put(head, revision); + } + } + return result; + } + + @Override + public Iterable getPullRequests(BitbucketSCMSource src) throws InterruptedException { + if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { + return Collections.emptyList(); + } + return Collections.singleton(getPayload().getPullRequest()); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessor.java new file mode 100644 index 000000000..2d8038a5c --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessor.java @@ -0,0 +1,84 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.RestrictedSince; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Deprecated(since = "937.0.0") +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class PluginPullRequestWebhookProcessor extends AbstractWebhookProcessor { + + private static final List supportedEvents = List.of( + HookEventType.PULL_REQUEST_CREATED.getKey(), // needed to create job + HookEventType.PULL_REQUEST_DECLINED.getKey(), // needed to remove job + HookEventType.PULL_REQUEST_MERGED.getKey(), // needed to remove job + HookEventType.PULL_REQUEST_UPDATED.getKey()); // needed to update git content and trigger build job + + @Override + public boolean canHandle(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey("X-Bitbucket-Type") + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && parameters.containsKey(SERVER_URL_PARAMETER); + } + + @Override + public void process(@NonNull String hookEventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { + HookEventType hookEvent = HookEventType.fromString(hookEventType); + BitbucketPullRequestEvent pull = BitbucketServerWebhookPayload.pullRequestEventFromPayload(payload); + if (pull != null) { + SCMEvent.Type eventType; + switch (hookEvent) { + case PULL_REQUEST_CREATED: + eventType = SCMEvent.Type.CREATED; + break; + case PULL_REQUEST_DECLINED, + PULL_REQUEST_MERGED: + eventType = SCMEvent.Type.REMOVED; + break; + default: + eventType = SCMEvent.Type.UPDATED; + break; + } + // assume updated as a catch-all type + notifyEvent(new PluginPREvent(eventType, pull, getOrigin(context), hookEvent), BitbucketSCMSource.getEventDelaySeconds()); + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushEvent.java new file mode 100644 index 000000000..7d66195a7 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushEvent.java @@ -0,0 +1,106 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Reference; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Target; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import org.apache.commons.lang3.StringUtils; + +@Deprecated(since = "937.0.0") +final class PluginPushEvent extends AbstractSCMHeadEvent { + + PluginPushEvent(Type type, BitbucketPushEvent payload, String origin) { + super(type, payload, origin); + } + + @NonNull + @Override + public String getSourceName() { + return getRepository().getRepositoryName(); + } + + @NonNull + @Override + public Map heads(@NonNull SCMSource source) { + if (!(source instanceof BitbucketSCMSource)) { + return Collections.emptyMap(); + } + BitbucketSCMSource src = (BitbucketSCMSource) source; + if (!isServerURLMatch(src.getServerUrl())) { + return Collections.emptyMap(); + } + if (!StringUtils.equalsIgnoreCase(src.getRepoOwner(), getPayload().getRepository().getOwnerName())) { + return Collections.emptyMap(); + } + if (!src.getRepository().equalsIgnoreCase(getPayload().getRepository().getRepositoryName())) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + for (BitbucketPushEvent.Change change: getPayload().getChanges()) { + if (change.isClosed()) { + result.put(new BranchSCMHead(change.getOld().getName()), null); + } else { + // created is true + Reference newChange = change.getNew(); + Target target = newChange.getTarget(); + + SCMHead head = null; + String eventType = newChange.getType(); + if ("tag".equals(eventType)) { + // for BB Cloud date is valued only in case of annotated tag + Date tagDate = newChange.getDate() != null ? newChange.getDate() : target.getDate(); + if (tagDate == null) { + // fall back to the jenkins time when the request is processed + tagDate = new Date(); + } + head = new BitbucketTagSCMHead(newChange.getName(), tagDate.getTime()); + } else { + head = new BranchSCMHead(newChange.getName()); + } + result.put(head, new AbstractGitSCMSource.SCMRevisionImpl(head, target.getHash())); + } + } + return result; + } + + @Override + protected BitbucketRepository getRepository() { + return getPayload().getRepository(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessor.java new file mode 100644 index 000000000..d99a79928 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessor.java @@ -0,0 +1,94 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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 com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.RestrictedSince; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Deprecated(since = "937.0.0") +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class PluginPushWebhookProcessor extends AbstractWebhookProcessor { + + private static final Logger logger = Logger.getLogger(PluginPushWebhookProcessor.class.getName()); + private static final List supportedEvents = List.of( + HookEventType.PUSH.getKey(), + HookEventType.PULL_REQUEST_UPDATED.getKey()); + + @Override + public boolean canHandle(Map headers, MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey("X-Bitbucket-Type") + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && parameters.containsKey(SERVER_URL_PARAMETER); + } + + @Override + public void process(@NonNull String eventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { + BitbucketPushEvent push = BitbucketServerWebhookPayload.pushEventFromPayload(payload); + if (push != null) { + if (push.getChanges().isEmpty()) { + final String owner = push.getRepository().getOwnerName(); + final String repository = push.getRepository().getRepositoryName(); + if (!reindexOnEmptyChanges()) { + logger.log(Level.INFO, "Received push hook with empty changes from Bitbucket. Processing indexing on {0}/{1}. " + + "You may skip this scan by adding the system property -D{2}=false on startup.", new Object[]{owner, repository, SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME}); + scmSourceReIndex(owner, repository, null); + } else { + logger.log(Level.INFO, "Received push hook with empty changes from Bitbucket for {0}/{1}. Skipping.", + new Object[]{owner, repository}); + } + } else { + SCMEvent.Type type = null; + for (BitbucketPushEvent.Change change : push.getChanges()) { + if ((type == null || type == SCMEvent.Type.CREATED) && change.isCreated()) { + type = SCMEvent.Type.CREATED; + } else if ((type == null || type == SCMEvent.Type.REMOVED) && change.isClosed()) { + type = SCMEvent.Type.REMOVED; + } else { + type = SCMEvent.Type.UPDATED; + } + } + notifyEvent(new PluginPushEvent(type, push, getOrigin(context)), BitbucketSCMSource.getEventDelaySeconds()); + } + } + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook.java new file mode 100644 index 000000000..7c6fe54bb --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook.java @@ -0,0 +1,184 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Falco Nikolas + * + * 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.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookDescriptor; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.Messages; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import java.net.MalformedURLException; +import java.net.URL; +import jenkins.model.Jenkins; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import static hudson.Util.fixEmptyAndTrim; + +@Deprecated(since = "937.0.0") +// https://help.moveworkforward.com/BPW/atlassian-bitbucket-post-webhook-api +// https://help.moveworkforward.com/BPW/how-to-get-configurations-using-post-webhooks-for- +public class PluginWebhook implements BitbucketWebhook { + + /** + * {@code true} if and only if Jenkins is supposed to auto-manage hooks for + * this end-point. + */ + private boolean manageHooks; + + /** + * The {@link StandardCredentials#getId()} of the credentials to use for + * auto-management of hooks. + */ + @CheckForNull + private String credentialsId; + + /** + * Jenkins Server Root URL to be used by that Bitbucket endpoint. + * The global setting from Jenkins.get().getRootUrl() + * will be used if this field is null or equals an empty string. + * This variable is bound to the UI, so an empty value is saved + * and returned by getter as such. + */ + private String endpointJenkinsRootURL; + + @DataBoundConstructor + public PluginWebhook(boolean manageHooks, @CheckForNull String credentialsId) { + this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId); + this.credentialsId = manageHooks ? fixEmptyAndTrim(credentialsId) : null; + } + + /** + * Returns {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * + * @return {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + */ + public final boolean isManageHooks() { + return manageHooks; + } + + /** + * Returns the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + * + * @return the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + */ + @CheckForNull + public final String getCredentialsId() { + return credentialsId; + } + + public String getEndpointJenkinsRootURL() { + return endpointJenkinsRootURL; + } + + @DataBoundSetter + public void setEndpointJenkinsRootURL(@CheckForNull String endpointJenkinsRootURL) { + this.endpointJenkinsRootURL = fixEmptyAndTrim(endpointJenkinsRootURL); + } + + @Override + public String getDisplayName() { + return Messages.ServerWebhookImplementation_displayName(); + } + + @Override + public String getId() { + return "PLUGIN"; + } + + @SuppressWarnings("unchecked") + @Override + public Descriptor getDescriptor() { + return Jenkins.get().getDescriptorOrDie(getClass()); + } + + @Symbol("pluginWebhook") + @Extension + public static class DescriptorImpl extends BitbucketWebhookDescriptor { + + @Override + public String getDisplayName() { + return "Post Webhooks for Bitbucket"; + } + + @Override + public boolean isApplicable(String serverURL) { + return !BitbucketApiUtils.isCloud(serverURL); + } + + /** + * Stapler form completion. + * + * @param credentialsId selected credentials. + * @param serverURL the server URL. + * @return the available credentials. + */ + @RequirePOST + public ListBoxModel doFillCredentialsIdItems(@QueryParameter(fixEmpty = true) String credentialsId, + @QueryParameter(value = "serverURL", fixEmpty = true) String serverURL) { + Jenkins jenkins = checkPermission(); + return BitbucketCredentialsUtils.listCredentials(jenkins, serverURL, credentialsId); + } + + @Restricted(NoExternalUse.class) + @RequirePOST + public static FormValidation doCheckEndpointJenkinsRootURL(@QueryParameter String value) { + checkPermission(); + String url = Util.fixEmptyAndTrim(value); + if (url == null) { + return FormValidation.ok(); + } + try { + new URL(url); + } catch (MalformedURLException e) { + return FormValidation.error("Invalid URL: " + e.getMessage()); + } + return FormValidation.ok(); + } + + private static Jenkins checkPermission() { + Jenkins jenkins = Jenkins.get(); + jenkins.checkPermission(Jenkins.MANAGE); + return jenkins; + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/AbstractNativeServerSCMHeadEvent.java similarity index 98% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/AbstractNativeServerSCMHeadEvent.java index b3340566d..c27f5a6fb 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/AbstractNativeServerSCMHeadEvent.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerHeadEvent.java similarity index 97% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerHeadEvent.java index c3535b906..75f5b5140 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerHeadEvent.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; @@ -29,6 +29,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerPullRequestEvent; import com.google.common.base.Ascii; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessor.java new file mode 100644 index 000000000..7808e0760 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessor.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright (c) 2016-2018, Yieldlab AG + * + * 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.jenkins.plugins.bitbucket.impl.webhook.server; + +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.RestrictedSince; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.collections4.MultiValuedMap; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class ServerPingWebhookProcessor extends AbstractWebhookProcessor { + + private static final Logger logger = Logger.getLogger(ServerPingWebhookProcessor.class.getName()); + private static final List supportedEvents = List.of(HookEventType.SERVER_PING.getKey()); + + @Override + public boolean canHandle(Map headers, MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && parameters.containsKey(SERVER_URL_PARAMETER); + } + + @Override + public void verifyPayload(Map headers, String body, BitbucketEndpoint endpoint) { + // ping hook is not signed + } + + @Override + public void process(@NonNull String eventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { + logger.log(Level.INFO, "Received webhook ping event from {0}", getOrigin(context)); + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessor.java similarity index 59% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessor.java index 4f59394c3..5ee1c2b1d 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessor.java @@ -21,33 +21,52 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerPullRequestEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; import hudson.RestrictedSince; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +@Extension @Restricted(NoExternalUse.class) @RestrictedSince("933.3.0") -public class NativeServerPullRequestHookProcessor extends HookProcessor { +public class ServerPullRequestWebhookProcessor extends AbstractWebhookProcessor { - private static final Logger LOGGER = Logger.getLogger(NativeServerPullRequestHookProcessor.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ServerPullRequestWebhookProcessor.class.getName()); + + private static final List supportedEvents = List.of( + HookEventType.SERVER_PULL_REQUEST_OPENED.getKey(), + HookEventType.SERVER_PULL_REQUEST_MERGED.getKey(), + HookEventType.SERVER_PULL_REQUEST_DECLINED.getKey(), + HookEventType.SERVER_PULL_REQUEST_DELETED.getKey(), + HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey(), + HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey()); @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin) { - // without a server URL, the event wouldn't match anything + public boolean canHandle(@NonNull Map headers, @NonNull MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey(REQUEST_ID_SERVER_HEADER) + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && parameters.containsKey(SERVER_URL_PARAMETER); } @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin, String serverUrl) { - + public void process(@NonNull String hookEventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { final NativeServerPullRequestEvent pullRequestEvent; try { pullRequestEvent = JsonParser.toJava(payload, NativeServerPullRequestEvent.class); @@ -56,6 +75,7 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta return; } + HookEventType hookEvent = HookEventType.fromString(hookEventType); final SCMEvent.Type eventType; switch (hookEvent) { case SERVER_PULL_REQUEST_OPENED: @@ -67,7 +87,6 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta eventType = SCMEvent.Type.REMOVED; break; case SERVER_PULL_REQUEST_MODIFIED, - SERVER_PULL_REQUEST_REVIEWER_UPDATED, SERVER_PULL_REQUEST_FROM_REF_UPDATED: eventType = SCMEvent.Type.UPDATED; break; @@ -76,6 +95,6 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta return; } - notifyEvent(new ServerHeadEvent(serverUrl, eventType, pullRequestEvent, origin), BitbucketSCMSource.getEventDelaySeconds()); + notifyEvent(new ServerHeadEvent(endpoint.getServerURL(), eventType, pullRequestEvent, getOrigin(context)), BitbucketSCMSource.getEventDelaySeconds()); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushEvent.java similarity index 98% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushEvent.java index c0e897f7f..824121cf5 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushEvent.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; @@ -34,6 +34,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; @@ -101,7 +102,7 @@ public boolean equals(Object obj) { } // event logs with the name of the processor - private static final Logger LOGGER = Logger.getLogger(NativeServerPushHookProcessor.class.getName()); + private static final Logger LOGGER = Logger.getLogger(ServerPushWebhookProcessor.class.getName()); private final BitbucketServerRepository repository; private final BitbucketServerCommit refCommit; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessor.java similarity index 75% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessor.java index d3bd48158..72e908b0e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessor.java @@ -21,10 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookProcessor; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerChange; @@ -32,37 +35,45 @@ import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerRefsChangedEvent; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; import hudson.RestrictedSince; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.scm.api.SCMEvent; +import org.apache.commons.collections4.MultiValuedMap; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +@Extension @Restricted(NoExternalUse.class) @RestrictedSince("933.3.0") -public class NativeServerPushHookProcessor extends HookProcessor { +public class ServerPushWebhookProcessor extends AbstractWebhookProcessor { - private static final Logger LOGGER = Logger.getLogger(NativeServerPushHookProcessor.class.getName()); + private static final Logger logger = Logger.getLogger(ServerPushWebhookProcessor.class.getName()); + private static final List supportedEvents = List.of( + HookEventType.SERVER_REFS_CHANGED.getKey(), + HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey()); @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin) { - return; // without a server URL, the event wouldn't match anything + public boolean canHandle(Map headers, MultiValuedMap parameters) { + return headers.containsKey(EVENT_TYPE_HEADER) + && headers.containsKey(REQUEST_ID_SERVER_HEADER) + && supportedEvents.contains(headers.get(EVENT_TYPE_HEADER)) + && parameters.containsKey(SERVER_URL_PARAMETER); } @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin, String serverUrl) { - if (payload == null) { - return; - } - + public void process(@NonNull String hookEventType, @NonNull String payload, @NonNull Map context, @NonNull BitbucketEndpoint endpoint) { final BitbucketServerRepository repository; final BitbucketServerCommit refCommit; final List changes; final String mirrorId; try { + HookEventType hookEvent = HookEventType.fromString(hookEventType); if (hookEvent == HookEventType.SERVER_REFS_CHANGED) { final NativeServerRefsChangedEvent event = JsonParser.toJava(payload, NativeServerRefsChangedEvent.class); repository = event.getRepository(); @@ -80,31 +91,31 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta if (event.getRefLimitExceeded()) { final String owner = repository.getOwnerName(); final String repositoryName = repository.getRepositoryName(); - LOGGER.log(Level.INFO, "Received mirror synchronized event with refLimitExceeded from Bitbucket. Processing with indexing on {0}/{1}. " + + logger.log(Level.INFO, "Received mirror synchronized event with refLimitExceeded from Bitbucket. Processing with indexing on {0}/{1}. " + "You may skip this scan by adding the system property -D{2}=false on startup.", new Object[]{owner, repositoryName, SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME}); scmSourceReIndex(owner, repositoryName, mirrorId); return; } } else { - throw new UnsupportedOperationException("Hook event of type " + hookEvent + " is not supported.\n" + throw new UnsupportedOperationException("Hook event of type " + hookEventType + " is not supported.\n" + "Please fill an issue at https://issues.jenkins.io to the bitbucket-branch-source-plugin component."); } } catch (final IOException e) { - LOGGER.log(Level.SEVERE, "Can not read hook payload", e); + logger.log(Level.SEVERE, "Can not read hook payload", e); return; } if (changes.isEmpty()) { final String owner = repository.getOwnerName(); final String repositoryName = repository.getRepositoryName(); - if (SCAN_ON_EMPTY_CHANGES) { - LOGGER.log(Level.INFO, "Received push hook with empty changes from Bitbucket. Processing indexing on {0}/{1}. " + + if (reindexOnEmptyChanges()) { + logger.log(Level.INFO, "Received push hook with empty changes from Bitbucket. Processing indexing on {0}/{1}. " + "You may skip this scan by adding the system property -D{2}=false on startup.", new Object[]{owner, repositoryName, SCAN_ON_EMPTY_CHANGES_PROPERTY_NAME}); scmSourceReIndex(owner, repositoryName, mirrorId); } else { - LOGGER.log(Level.INFO, "Received push hook with empty changes from Bitbucket for {0}/{1}. Skipping.", + logger.log(Level.INFO, "Received push hook with empty changes from Bitbucket for {0}/{1}. Skipping.", new Object[]{owner, repositoryName}); } } else { @@ -118,15 +129,15 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta } else if ("ADD".equals(type)) { events.put(SCMEvent.Type.CREATED, change); } else { - LOGGER.log(Level.INFO, "Unknown change event type of {0} received from Bitbucket Server", type); + logger.log(Level.INFO, "Unknown change event type of {0} received from Bitbucket Server", type); } } for (final SCMEvent.Type type : events.keySet()) { - ServerPushEvent headEvent = new ServerPushEvent(serverUrl, type, events.get(type), origin, repository, refCommit, mirrorId); + ServerPushEvent headEvent = new ServerPushEvent(endpoint.getServerURL(), type, events.get(type), getOrigin(context), repository, refCommit, mirrorId); notifyEvent(headEvent, BitbucketSCMSource.getEventDelaySeconds()); } } - } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhook.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhook.java new file mode 100644 index 000000000..97f187296 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhook.java @@ -0,0 +1,117 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Falco Nikolas + * + * 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.jenkins.plugins.bitbucket.impl.webhook.server; + +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.Messages; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +public class ServerWebhook extends AbstractWebhook { + + public ServerWebhook(boolean manageHooks, @CheckForNull String credentialsId) { + super(manageHooks, credentialsId, false, null); + } + + @DataBoundConstructor + public ServerWebhook(boolean manageHooks, @CheckForNull String credentialsId, + boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) { + super(manageHooks, credentialsId, enableHookSignature, hookSignatureCredentialsId); + } + + @Override + public String getDisplayName() { + return Messages.ServerWebhookImplementation_displayName(); + } + + @Override + public String getId() { + return "NATIVE"; + } + + @Symbol("serverWebhook") + @Extension + public static class DescriptorImpl extends AbstractBitbucketWebhookDescriptorImpl { + + @Override + public String getDisplayName() { + return "Native Data Center"; + } + + @Override + public boolean isApplicable(String serverURL) { + return !BitbucketApiUtils.isCloud(serverURL); + } + + /** + * Stapler form completion. + * + * @param credentialsId selected credentials. + * @param serverURL the server URL. + * @return the available credentials. + */ + @RequirePOST + public ListBoxModel doFillCredentialsIdItems(@QueryParameter(fixEmpty = true) String credentialsId, + @QueryParameter(value = "serverURL", fixEmpty = true) String serverURL) { + Jenkins jenkins = checkPermission(); + return BitbucketCredentialsUtils.listCredentials(jenkins, serverURL, credentialsId); + } + + /** + * Stapler form completion. + * + * @param hookSignatureCredentialsId selected hook signature credentials. + * @param serverURL the server URL. + * @return the available credentials. + */ + @RequirePOST + public ListBoxModel doFillHookSignatureCredentialsIdItems(@QueryParameter(fixEmpty = true) String hookSignatureCredentialsId, + @QueryParameter(value = "serverURL", fixEmpty = true) String serverURL) { + Jenkins jenkins = checkPermission(); + StandardListBoxModel result = new StandardListBoxModel(); + result.includeMatchingAs(ACL.SYSTEM2, + jenkins, + StringCredentials.class, + URIRequirementBuilder.fromUri(serverURL).build(), + CredentialsMatchers.always()); + if (hookSignatureCredentialsId != null) { + result.includeCurrentValue(hookSignatureCredentialsId); + } + return result; + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java index 98190276c..a304c2002 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java @@ -26,11 +26,12 @@ import hudson.model.ModelObject; public enum BitbucketServerVersion implements ModelObject { - VERSION_7("Bitbucket Data Center v8.x (and later)"), - VERSION_6_5("Bitbucket Server v6.5 to v6.10 - EOL reached, any support DISMISSED"), - VERSION_6("Bitbucket Server v6.0 to v6.4 - EOL reached, any support DISMISSED"), - VERSION_5_10("Bitbucket Server v5.10 to v5.16 - EOL reached, any support DISMISSED"), - VERSION_5("Bitbucket Server v5.9 (and earlier) - EOL reached, any support DISMISSED"); + VERSION_8("Bitbucket Data Center v8.x (and later)"), + VERSION_7("Bitbucket Server v7 - EOS reached, any support DISMISSED"), + VERSION_6_5("Bitbucket Server v6.5 to v6.10 - EOS reached, any support DISMISSED"), + VERSION_6("Bitbucket Server v6.0 to v6.4 - EOS reached, any support DISMISSED"), + VERSION_5_10("Bitbucket Server v5.10 to v5.16 - EOS reached, any support DISMISSED"), + VERSION_5("Bitbucket Server v5.9 (and earlier) - EOS reached, any support DISMISSED"); private final String displayName; @@ -43,4 +44,12 @@ public String getDisplayName() { return displayName; } + /** + * The minimal supported version. + *

+ * If configured less than this it will be changed to the minimal. + */ + public static BitbucketServerVersion getMinSupportedVersion() { + return BitbucketServerVersion.VERSION_8; + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index ff0e93971..7d05f8fb2 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -46,18 +46,14 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerVersion; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranch; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranches; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequestCanMerge; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketPluginWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerProject; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhook; import com.damnhandy.uri.template.UriTemplate; import com.damnhandy.uri.template.impl.Operator; import com.fasterxml.jackson.core.JacksonException; @@ -76,7 +72,6 @@ import java.lang.reflect.ParameterizedType; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -97,7 +92,6 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicNameValuePair; -import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.abbreviate; import static org.apache.commons.lang3.StringUtils.substring; @@ -159,17 +153,10 @@ public class BitbucketServerAPIClient extends AbstractBitbucketApi implements Bi */ private final boolean userCentric; private final String baseURL; - private final BitbucketServerWebhookImplementation webhookImplementation; private final CloseableHttpClient client; public BitbucketServerAPIClient(@NonNull String baseURL, @NonNull String owner, @CheckForNull String repositoryName, @CheckForNull BitbucketAuthenticator authenticator, boolean userCentric) { - this(baseURL, owner, repositoryName, authenticator, userCentric, BitbucketServerEndpoint.findWebhookImplementation(baseURL)); - } - - public BitbucketServerAPIClient(@NonNull String baseURL, @NonNull String owner, @CheckForNull String repositoryName, - @CheckForNull BitbucketAuthenticator authenticator, boolean userCentric, - @NonNull BitbucketServerWebhookImplementation webhookImplementation) { super(authenticator); this.userCentric = userCentric; this.owner = Util.fixEmptyAndTrim(owner); @@ -178,7 +165,6 @@ public BitbucketServerAPIClient(@NonNull String baseURL, @NonNull String owner, } this.repositoryName = repositoryName; this.baseURL = Util.removeTrailingSlash(baseURL); - this.webhookImplementation = requireNonNull(webhookImplementation); this.client = setupClientBuilder().build(); } @@ -272,12 +258,10 @@ private List getPullRequests(UriTemplate template) t setupPullRequest(pullRequest, endpoint); } - if (endpoint != null) { - // Get PRs again as revisions could be changed by other events during setupPullRequest - if (endpoint.isCallChanges() && BitbucketServerVersion.VERSION_7.equals(endpoint.getServerVersion())) { - pullRequests = getPagedRequest(template, BitbucketServerPullRequest.class); - pullRequests.removeIf(this::shouldIgnore); - } + // Get PRs again as revisions could be changed by other events during setupPullRequest + if (endpoint != null && endpoint.isCallChanges()) { + pullRequests = getPagedRequest(template, BitbucketServerPullRequest.class); + pullRequests.removeIf(this::shouldIgnore); } return pullRequests; @@ -303,7 +287,7 @@ private void setupPullRequest(@NonNull BitbucketServerPullRequest pullRequest, @ } } } - if (endpoint.isCallChanges() && BitbucketServerVersion.VERSION_7.equals(endpoint.getServerVersion())) { + if (endpoint.isCallChanges()) { callPullRequestChangesById(pullRequest.getId()); } } @@ -643,6 +627,7 @@ public BitbucketCommit resolveCommit(@NonNull BitbucketPullRequest pull) throws @Override public void registerCommitWebHook(BitbucketWebHook hook) throws IOException { + /* switch (webhookImplementation) { case PLUGIN: // API documentation at https://help.moveworkforward.com/BPW/how-to-manage-configurations-using-post-webhooks-f#HowtomanageconfigurationsusingPostWebhooksforBitbucketAPIs?-Createpostwebhook @@ -671,10 +656,12 @@ public void registerCommitWebHook(BitbucketWebHook hook) throws IOException { logger.log(Level.WARNING, "Cannot register {0} webhook.", webhookImplementation); break; } + */ } @Override public void updateCommitWebHook(BitbucketWebHook hook) throws IOException { + /* switch (webhookImplementation) { case PLUGIN: // API documentation at https://help.moveworkforward.com/BPW/how-to-manage-configurations-using-post-webhooks-f#HowtomanageconfigurationsusingPostWebhooksforBitbucketAPIs?-UpdateapostwebhookbyID @@ -703,10 +690,12 @@ public void updateCommitWebHook(BitbucketWebHook hook) throws IOException { logger.log(Level.WARNING, "Cannot update {0} webhook.", webhookImplementation); break; } + */ } @Override public void removeCommitWebHook(BitbucketWebHook hook) throws IOException { + /* switch (webhookImplementation) { case PLUGIN: deleteRequest( @@ -734,11 +723,13 @@ public void removeCommitWebHook(BitbucketWebHook hook) throws IOException { logger.log(Level.WARNING, "Cannot remove {0} webhook.", webhookImplementation); break; } + */ } @NonNull @Override public List getWebHooks() throws IOException { + /* switch (webhookImplementation) { case PLUGIN: String url = UriTemplate @@ -754,7 +745,7 @@ public List getWebHooks() throws IOException { .set("repo", repositoryName); return getPagedRequest(uriTemplate, BitbucketServerWebhook.class); } - +*/ return Collections.emptyList(); } diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/config.jelly index 4b869b907..b16e91d46 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/config.jelly @@ -24,18 +24,6 @@ THE SOFTWARE. - - - - - - - - - - - - - - + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint/config-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint/config-detail.jelly index ab1a32045..8ff5f86b2 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint/config-detail.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpoint/config-detail.jelly @@ -22,19 +22,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/config-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/config-detail.jelly index ff192cc92..cdb430942 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/config-detail.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/config-detail.jelly @@ -22,21 +22,20 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-serverUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-serverURL.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-serverUrl.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-serverURL.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-webhookImplementation.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-webhookImplementation.html deleted file mode 100644 index 5386867c9..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/help-webhookImplementation.html +++ /dev/null @@ -1,11 +0,0 @@ -

- Determines which Webhook implementation is to be used: -
-
Plugin
-
The third party Webhook implementation provided by the Post Webhooks for Bitbucket plugin.
-
Please note cloning from mirror is not supported with this implementation.
- -
Native
-
The native Webhook implementation available since Bitbucket Server 5.4.
-
-
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/manage-hooks-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/manage-hooks-detail.jelly deleted file mode 100644 index 109342fce..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpoint/manage-hooks-detail.jelly +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/config.jelly new file mode 100644 index 000000000..592f4af80 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/config.jelly @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-credentialsId.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-credentialsId.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-credentialsId.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-enableHookSignature.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-enableHookSignature.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-enableHookSignature.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-enableHookSignature.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-bitbucketJenkinsRootUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-endpointJenkinsRootURL.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-bitbucketJenkinsRootUrl.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-endpointJenkinsRootURL.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-hookSignatureCredentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-hookSignatureCredentialsId.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-hookSignatureCredentialsId.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-hookSignatureCredentialsId.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-manageHooks.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-manageHooks.html similarity index 100% rename from src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/AbstractBitbucketEndpoint/help-manageHooks.html rename to src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhook/help-manageHooks.html diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/Messages.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/Messages.properties new file mode 100644 index 000000000..b1b25770f --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/Messages.properties @@ -0,0 +1,24 @@ +# +# The MIT License +# +# Copyright (c) 2025, Falco Nikolas +# +# 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. +CloudWebhookImplementation.displayName=Native Cloud Implementation +ServerWebhookImplementation.displayName=Native Server Implementation diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/config.jelly new file mode 100644 index 000000000..6779be8fe --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/config.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-credentialsId.html new file mode 100644 index 000000000..b7061f123 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-credentialsId.html @@ -0,0 +1,7 @@ +
+ Select the credentials to use for managing hooks. Both GLOBAL and SYSTEM scoped credentials are eligible as the + management of hooks is run in the context of Jenkins itself and not in the context of the individual items. +

+ For security reasons most credentials are only available when HTTPS is used. +

+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-endpointJenkinsRootURL.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-endpointJenkinsRootURL.html new file mode 100644 index 000000000..ff9bbb027 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-endpointJenkinsRootURL.html @@ -0,0 +1,6 @@ +
+ You can customize the Jenkins Server Root URL to be used by this Bitbucket endpoint, + instead of the one set in Jenkins global configuration. This may be useful in local + networks with different DNS views for different clients (such as a Bitbucket server) + or to receive webhooks through some tunneling or relaying service hosted elsewhere. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-manageHooks.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-manageHooks.html new file mode 100644 index 000000000..391b54782 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhook/help-manageHooks.html @@ -0,0 +1,6 @@ +
+ Selecting this option will enable the automatic management of web hooks for all items that use this + endpoint, except those items that have explicitly opted out of hook management. + When this option is not selected, individual items can still opt in to hook management provided the credentials + those items have been configured with have permission to manage the required hooks. +
diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java index 685ef34a6..22e2a9582 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java @@ -64,7 +64,7 @@ public class BranchScanningIntegrationTest { @Test public void indexingTest() throws Exception { BitbucketEndpointConfiguration.get() - .addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test", false, null, false, null)); + .addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test")); BitbucketMockApiFactory.add("http://bitbucket.test", BitbucketClientMockUtils.getAPIClientMock(false, false)); MockMultiBranchProjectImpl p = j.jenkins.createProject(MockMultiBranchProjectImpl.class, "test"); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java index 3f3944cf1..b82d9cadd 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java @@ -28,6 +28,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.hooks.WebhookAutoRegisterListener; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; import com.cloudbees.jenkins.plugins.bitbucket.trait.WebhookRegistrationTrait; import hudson.model.listeners.ItemListener; import hudson.util.RingBufferLogHandler; @@ -82,7 +83,7 @@ void registerHookTest() throws Exception { @Test void registerHookTest2() throws Exception { - BitbucketEndpointConfiguration.get().setEndpoints(List.of(new BitbucketCloudEndpoint(false, 0, 0, true, "dummy", false, null))); + BitbucketEndpointConfiguration.get().setEndpoints(List.of(new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(true, "dummy")))); BitbucketApi mock = Mockito.mock(BitbucketApi.class); BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, mock); RingBufferLogHandler log = createJULTestHandler(); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java index 9b1f08ea3..03f257a31 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java @@ -26,7 +26,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; import java.io.FileNotFoundException; import java.io.IOException; @@ -143,7 +142,7 @@ private static class BitbucketServerIntegrationClient extends BitbucketServerAPI private final IRequestAudit audit; private BitbucketServerIntegrationClient(String payloadRootPath, String baseURL, String owner, String repositoryName) { - super(baseURL, owner, repositoryName, mock(BitbucketAuthenticator.class), false, BitbucketServerWebhookImplementation.NATIVE); + super(baseURL, owner, repositoryName, mock(BitbucketAuthenticator.class), false); if (payloadRootPath == null) { this.payloadRootPath = PAYLOAD_RESOURCE_ROOTPATH; diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java index c3a500ae4..a2385bc5c 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java @@ -28,8 +28,9 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.Messages; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerVersion; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; @@ -103,7 +104,7 @@ void given__newInstance__when__configuredWithEmpty__then__cloudPresent() { void given__newInstance__when__configuredWithCloud__then__cloudPresent() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); assumeFalse("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - instance.setEndpoints(List.of(buildEndpoint(true, "dummy"))); + instance.setEndpoints(List.of(buildCloudEndpoint(true, "dummy"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("dummy"); @@ -114,7 +115,7 @@ void given__newInstance__when__configuredWithCloud__then__cloudPresent() { void given__newInstance__when__configuredWithMultipleCloud__then__onlyFirstCloudPresent() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); assumeFalse("first".equals(instance.getEndpoints().get(0).getCredentialsId())); - instance.setEndpoints(List.of(buildEndpoint(true, "first"), buildEndpoint(true, "second"), buildEndpoint(true, "third"))); + instance.setEndpoints(List.of(buildCloudEndpoint(true, "first"), buildCloudEndpoint(true, "second"), buildCloudEndpoint(true, "third"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("first"); @@ -127,7 +128,7 @@ void given__newInstance__when__configuredAsAnon__then__permissionError() { r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); try (ACLContext context = ACL.as2(Jenkins.ANONYMOUS2)) { assertThatThrownBy(() -> - instance.setEndpoints(List.of(buildEndpoint(true, "first"), buildEndpoint(true, "second"), buildEndpoint(true, "third"))) + instance.setEndpoints(List.of(buildCloudEndpoint(true, "first"), buildCloudEndpoint(true, "second"), buildCloudEndpoint(true, "third"))) ) .hasMessage(hudson.security.Messages.AccessDeniedException2_MissingPermission("anonymous", "Overall/Administer")); } finally { @@ -143,7 +144,7 @@ void given__newInstance__when__configuredAsManage__then__OK() { mockStrategy.grant(Jenkins.MANAGE).onRoot().to("admin"); r.jenkins.setAuthorizationStrategy(mockStrategy); try (ACLContext context = ACL.as(User.get("admin"))) { - instance.setEndpoints(List.of(buildEndpoint(true, "first"), buildEndpoint(true, "second"), buildEndpoint(true, "third"))); + instance.setEndpoints(List.of(buildCloudEndpoint(true, "first"), buildCloudEndpoint(true, "second"), buildCloudEndpoint(true, "third"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("first"); @@ -158,9 +159,9 @@ void given__newInstance__when__configuredWithServerUsingCloudUrl__then__converte BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); assumeFalse("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); instance.setEndpoints(List.of( - new BitbucketServerEndpoint("I am silly", BitbucketCloudEndpoint.SERVER_URL, true, "dummy"), - buildEndpoint(true, "second"), - buildEndpoint(true, "third"))); + new BitbucketServerEndpoint("I am silly", BitbucketCloudEndpoint.SERVER_URL, new ServerWebhook(true, "dummy")), + buildCloudEndpoint(true, "second"), + buildCloudEndpoint(true, "third"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("dummy"); @@ -171,7 +172,7 @@ void given__newInstance__when__configuredWithServerUsingCloudUrl__then__converte void given__newInstance__when__configuredWithServer__then__serverPresent() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); assumeFalse("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { @@ -187,8 +188,8 @@ void given__newInstance__when__configuredWithTwoServers__then__serversPresent() BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); assumeFalse("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); instance.setEndpoints(List.of( - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", false, null))); + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { @@ -208,7 +209,7 @@ void given__newInstance__when__configuredWithTwoServers__then__serversPresent() @Test void given__instanceWithCloud__when__addingAnotherCloud__then__onlyFirstCloudRetained() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(buildEndpoint(true, "dummy"))); + instance.setEndpoints(List.of(buildCloudEndpoint(true, "dummy"))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); assertThat(instance.addEndpoint(new BitbucketCloudEndpoint())).isFalse(); @@ -221,10 +222,10 @@ void given__instanceWithCloud__when__addingAnotherCloud__then__onlyFirstCloudRet @Test void given__instanceWithServer__when__addingCloud__then__cloudAdded() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - assertThat(instance.addEndpoint(buildEndpoint(true, "added"))).isTrue(); + assertThat(instance.addEndpoint(buildCloudEndpoint(true, "added"))).isTrue(); assertThat(instance.getEndpoints()).hasExactlyElementsOfTypes(BitbucketServerEndpoint.class, BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("dummy"); @@ -241,10 +242,10 @@ void given__instanceWithServer__when__addingCloud__then__cloudAdded() { @Test void given__instanceWithServer__when__addingDifferentServer__then__serverAdded() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - assertThat(instance.addEndpoint(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "added"))).isTrue(); + assertThat(instance.addEndpoint(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "added")))).isTrue(); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("dummy"); @@ -257,10 +258,10 @@ void given__instanceWithServer__when__addingDifferentServer__then__serverAdded() @Test void given__instanceWithServer__when__addingSameServer__then__onlyFirstServerRetained() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - assertThat(instance.addEndpoint(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", false, null))).isFalse(); + assertThat(instance.addEndpoint(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/"))).isFalse(); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { assertThat(endpoint.getCredentialsId()).isEqualTo("dummy"); @@ -270,7 +271,7 @@ void given__instanceWithServer__when__addingSameServer__then__onlyFirstServerRet @Test void given__instanceWithCloud__when__updatingCloud__then__cloudUpdated() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(buildEndpoint(true, "dummy"))); + instance.setEndpoints(List.of(buildCloudEndpoint(true, "dummy"))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); instance.updateEndpoint(new BitbucketCloudEndpoint()); @@ -284,9 +285,9 @@ void given__instanceWithCloud__when__updatingCloud__then__cloudUpdated() { @Test void given__instanceWithServer__when__updatingCloud__then__cloudAdded() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - instance.updateEndpoint(buildEndpoint(true, "added")); + instance.updateEndpoint(buildCloudEndpoint(true, "added")); assertThat(instance.getEndpoints()).hasExactlyElementsOfTypes(BitbucketServerEndpoint.class, BitbucketCloudEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { @@ -300,10 +301,10 @@ void given__instanceWithServer__when__updatingCloud__then__cloudAdded() { @Test void given__instanceWithServer__when__updatingDifferentServer__then__serverAdded() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy", false, null))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); assumeTrue("dummy".equals(instance.getEndpoints().get(0).getCredentialsId())); - instance.updateEndpoint(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "added", false, null)); + instance.updateEndpoint(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "added"))); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { @@ -317,9 +318,9 @@ void given__instanceWithServer__when__updatingDifferentServer__then__serverAdded @Test void given__instanceWithServer__when__updatingSameServer__then__serverUpdated() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")))); - instance.updateEndpoint(new BitbucketServerEndpoint("Example, Inc.", "https://bitbucket.example.com/", false, null)); + instance.updateEndpoint(new BitbucketServerEndpoint("Example, Inc.", "https://bitbucket.example.com/")); assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); assertThat(instance.getEndpoints()).element(0).satisfies(endpoint -> { @@ -338,7 +339,7 @@ void given__newInstance__when__removingCloud__then__defaultRestored() { .element(0) .satisfies(endpoint -> assertThat(endpoint.getCredentialsId()).isNull()); // remove default does not really remove it - assertThat(instance.removeEndpoint(buildEndpoint(true, "dummy"))).isFalse(); + assertThat(instance.removeEndpoint(buildCloudEndpoint(true, "dummy"))).isFalse(); // default always exists assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketCloudEndpoint.class); } @@ -348,15 +349,15 @@ void given__instanceWithCloudAndServers__when__removingServer__then__matchingSer BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assumeTrue("first".equals(instance.getEndpoints().get(0).getCredentialsId())); assumeTrue("second".equals(instance.getEndpoints().get(1).getCredentialsId())); assumeTrue("third".equals(instance.getEndpoints().get(2).getCredentialsId())); - assertThat(instance.removeEndpoint(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", false, null))).isTrue(); + assertThat(instance.removeEndpoint(new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/"))).isTrue(); assertThat(instance.getEndpoints()).hasExactlyElementsOfTypes(BitbucketCloudEndpoint.class, BitbucketServerEndpoint.class); assertThat(instance.getEndpoints().get(0).getCredentialsId()).isEqualTo("first"); assertThat(instance.getEndpoints().get(1).getCredentialsId()).isEqualTo("third"); @@ -367,9 +368,9 @@ void given__instanceWithCloudAndServers__when__removingCloud__then__cloudRemoved BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assumeTrue("first".equals(instance.getEndpoints().get(0).getCredentialsId())); assumeTrue("second".equals(instance.getEndpoints().get(1).getCredentialsId())); @@ -386,15 +387,15 @@ void given__instanceWithCloudAndServers__when__removingNonExisting__then__noChan BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assumeTrue("first".equals(instance.getEndpoints().get(0).getCredentialsId())); assumeTrue("second".equals(instance.getEndpoints().get(1).getCredentialsId())); assumeTrue("third".equals(instance.getEndpoints().get(2).getCredentialsId())); - assertThat(instance.removeEndpoint(new BitbucketServerEndpoint("Test", "http://bitbucket.test", true, "fourth"))).isFalse(); + assertThat(instance.removeEndpoint(new BitbucketServerEndpoint("Test", "http://bitbucket.test", new ServerWebhook(true, "third")))).isFalse(); assertThat(instance.getEndpoints()).hasExactlyElementsOfTypes(BitbucketCloudEndpoint.class, BitbucketServerEndpoint.class, BitbucketServerEndpoint.class); assertThat(instance.getEndpoints().get(0).getCredentialsId()).isEqualTo("first"); assertThat(instance.getEndpoints().get(1).getCredentialsId()).isEqualTo("second"); @@ -404,7 +405,7 @@ void given__instanceWithCloudAndServers__when__removingNonExisting__then__noChan @Test void given__instance__when__onlyOneEndpoint__then__endpointsNotSelectable() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "dummy", false, null))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "dummy")))); assertThat(instance.isEndpointSelectable()).isFalse(); } @@ -413,9 +414,9 @@ void given__instance__when__multipleEndpoints__then__endpointsSelectable() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assertThat(instance.isEndpointSelectable()).isTrue(); } @@ -424,9 +425,9 @@ void given__instance__when__multipleEndpoints__then__endpointsSelectable() { void given__instanceWithCloudAndServers__when__findingExistingEndpoint__then__endpointFound() { BitbucketEndpointConfiguration.get().setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assertThat(BitbucketEndpointProvider.lookupEndpoint(BitbucketCloudEndpoint.SERVER_URL)).isPresent() .hasValueSatisfying(endpoint -> { @@ -453,8 +454,8 @@ void given__instanceWithCloudAndServers__when__findingExistingEndpoint__then__en void given__instanceWithServers__when__findingNonExistingEndpoint__then__endpointNotFound() { BitbucketEndpointConfiguration.get().setEndpoints( List.of( - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "dummy") + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "dummy")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "dummy")) )); assertThat(BitbucketEndpointProvider.lookupEndpoint(BitbucketCloudEndpoint.SERVER_URL)).isEmpty(); assertThat(BitbucketEndpointProvider.lookupEndpoint("http://bitbucket.example.com/")).isEmpty(); @@ -470,9 +471,9 @@ void given__instanceWithServers__when__findingNonExistingEndpoint__then__endpoin void given__instanceWithCloudAndServers__when__findingInvalid__then__endpointNotFound() { BitbucketEndpointConfiguration.get().setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assertThat(BitbucketEndpointProvider.lookupEndpoint("0schemes-start-with+digits:no leading slash")).isEmpty(); assertThat(BitbucketEndpointProvider.lookupEndpoint("http://host name with spaces:443")).isEmpty(); @@ -484,9 +485,9 @@ void given__instanceWithCloudAndServers__when__populatingDropBox__then__endpoint BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); ListBoxModel items = instance.getEndpointItems(); assertThat(items).hasSize(3); @@ -503,9 +504,9 @@ void given__instanceWithCloudAndServers__when__resolvingExistingEndpoint__then__ BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", new ServerWebhook(true, "third")) )); assertThat(instance.getEndpointItems()).hasSize(3); assertThat(instance.readResolveServerUrl(null)).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); @@ -530,7 +531,7 @@ void given__instanceWithCloudAndServers__when__resolvingNewEndpointAsSystem__the r.jenkins.setAuthorizationStrategy(mockStrategy); try (ACLContext context = ACL.as2(ACL.SYSTEM2)) { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("existing", "https://bitbucket.test", false, null, false, null))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("existing", "https://bitbucket.test"))); assertThat(instance.getEndpointItems()).hasSize(1); assertThat(instance.readResolveServerUrl(null)).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); assertThat(instance.getEndpointItems()).hasSize(2); @@ -570,7 +571,7 @@ void given__instanceWithCloudAndServers__when__resolvingNewEndpointAsSystem__the @Test void given__instanceWithCloudAndServers__when__resolvingNewEndpointAsAnon__then__normalizedReturnedNotAdded() { BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); - instance.setEndpoints(List.of(new BitbucketServerEndpoint("existing", "https://bitbucket.test", false, null, false, null))); + instance.setEndpoints(List.of(new BitbucketServerEndpoint("existing", "https://bitbucket.test"))); try (ACLContext context = ACL.as2(Jenkins.ANONYMOUS2)) { assertThat(instance.getEndpointItems()).hasSize(1); assertThat(instance.readResolveServerUrl(null)).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); @@ -599,9 +600,9 @@ void given__instanceWithConfig__when__configRoundtrip__then__configRetained() th BitbucketEndpointConfiguration instance = BitbucketEndpointConfiguration.get(); instance.setEndpoints( List.of( - buildEndpoint(true, "first"), - new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second", false, null), - new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", false, null, false, null) + buildCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", new ServerWebhook(true, "second")), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/") )); SystemCredentialsProvider.getInstance().setDomainCredentialsMap( Collections.singletonMap(Domain.global(), Arrays.asList( @@ -637,7 +638,7 @@ void given__instanceWithConfig__when__configRoundtrip__then__configRetained() th } @Test - void given__serverConfig__without__webhookImplementation__then__usePlugin() throws Exception { + void given__serverConfig__without__webhookImplementation__then__useNative() throws Exception { final URL configWithoutWebhookImpl = Resources.getResource(getClass(), "config-without-webhook-impl.xml"); final File configFile = new File(Jenkins.get().getRootDir(), BitbucketEndpointConfiguration.class.getName() + ".xml"); FileUtils.copyURLToFile(configWithoutWebhookImpl, configFile); @@ -646,7 +647,6 @@ void given__serverConfig__without__webhookImplementation__then__usePlugin() thro assertThat(instance.getEndpoints()).hasOnlyElementsOfType(BitbucketServerEndpoint.class); final BitbucketServerEndpoint endpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(0); - assertThat(endpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); } @Test @@ -675,7 +675,6 @@ void load_serverConfig__with_old_signatures() throws Exception { assertThat(endpoint.getCredentialsId()).isEqualTo("admin.basic.credentials"); assertThat(endpoint.getEndpointJenkinsRootURL()).isEqualTo("http://host.docker.internal:8090/jenkins/"); assertThat(endpoint.getDisplayName()).isEqualTo("server"); - assertThat(endpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.NATIVE); assertThat(endpoint.isCallCanMerge()).isTrue(); assertThat(endpoint.isCallChanges()).isTrue(); }); @@ -713,8 +712,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isEqualTo("second"); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); assertThat(instance.getEndpoints()).element(2).isInstanceOf(BitbucketServerEndpoint.class); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(2); @@ -724,8 +722,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isTrue(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(3); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -734,8 +731,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isEqualTo("third"); assertThat(serverEndpoint.isCallCanMerge()).isTrue(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.NATIVE); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(4); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -744,8 +740,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isEqualTo("fourth"); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isFalse(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(5); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -754,8 +749,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isEqualTo("fifth"); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(6); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -764,8 +758,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isFalse(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(7); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -774,8 +767,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isFalse(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_7); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(8); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -784,8 +776,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_6_5); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(9); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -794,8 +785,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_6); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(10); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -804,8 +794,7 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_5_10); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(11); assertThat(serverEndpoint.getDisplayName()).isEqualTo("Example Inc"); @@ -814,12 +803,11 @@ void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.getCredentialsId()).isNull(); assertThat(serverEndpoint.isCallCanMerge()).isFalse(); assertThat(serverEndpoint.isCallChanges()).isTrue(); - assertThat(serverEndpoint.getWebhookImplementation()).isEqualTo(BitbucketServerWebhookImplementation.PLUGIN); - assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.VERSION_5); + assertThat(serverEndpoint.getServerVersion()).isEqualTo(BitbucketServerVersion.getMinSupportedVersion()); } - private BitbucketCloudEndpoint buildEndpoint(boolean manageHook, String credentials) { - return new BitbucketCloudEndpoint(false, 0, 0, manageHook, credentials, false, null); + private BitbucketCloudEndpoint buildCloudEndpoint(boolean manageHook, String credentials) { + return new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(manageHook, credentials, false, null)); } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java index ceb0c0c10..88c31a2e2 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java @@ -23,28 +23,35 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessor; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; -import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; -import com.cloudbees.jenkins.plugins.bitbucket.test.util.BitbucketTestUtil; -import jakarta.servlet.ReadListener; -import jakarta.servlet.ServletInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.UUID; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.MockRequest; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.commons.collections4.MultiValuedMap; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest2; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,357 +65,111 @@ static void init(JenkinsRule rule) { j = rule; } - private BitbucketSCMSourcePushHookReceiver sut; private StaplerRequest2 req; - private HookProcessor hookProcessor; - private String credentialsId; + private Map headers; + private BitbucketSCMSourcePushHookReceiver sut; @BeforeEach void setup() throws Exception { req = mock(StaplerRequest2.class); - - hookProcessor = mock(HookProcessor.class); - sut = new BitbucketSCMSourcePushHookReceiver() { + headers = new HashMap<>(); + when(req.getHeader(anyString())).thenAnswer(new Answer() { @Override - HookProcessor getHookProcessor(HookEventType type) { - return hookProcessor; + public String answer(InvocationOnMock invocation) throws Throwable { + return headers.get(invocation.getArgument(0)); } - }; - - credentialsId = BitbucketTestUtil.registerHookCredentials("Gkvl$k$wyNpQAF42", j).getId(); + }); + when(req.getHeaderNames()).thenAnswer(new Answer>() { + @Override + public Enumeration answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(headers.keySet()); + } + }); + when(req.getInputStream()).thenReturn(new MockRequest("{}")); } - private void mockCloudRequest() { + private void mockRequest() { when(req.getRemoteHost()).thenReturn("https://bitbucket.org"); when(req.getRemoteAddr()).thenReturn("185.166.143.48"); when(req.getScheme()).thenReturn("https"); when(req.getServerName()).thenReturn("jenkins.example.com"); when(req.getLocalPort()).thenReturn(80); when(req.getRequestURI()).thenReturn("/bitbucket-scmsource-hook/notify"); - when(req.getHeader("User-Agent")).thenReturn("Bitbucket-Webhooks/2.0"); - when(req.getHeader("X-Attempt-Number")).thenReturn("1"); - when(req.getHeader("Content-Type")).thenReturn("application/json"); - when(req.getHeader("X-Hook-UUID")).thenReturn(UUID.randomUUID().toString()); - when(req.getHeader("X-Request-UUID")).thenReturn(UUID.randomUUID().toString()); - when(req.getHeader("traceparent")).thenReturn(UUID.randomUUID().toString()); - when(req.getHeader("User-Agent")).thenReturn("Bitbucket-Webhooks/2.0"); - } - - private void mockServerRequest(String serverURL) { - when(req.getRemoteHost()).thenReturn("http://localhost:7990"); - when(req.getParameter("server_url")).thenReturn(serverURL); - when(req.getRemoteAddr()).thenReturn("127.0.0.1"); - when(req.getScheme()).thenReturn("https"); - when(req.getServerName()).thenReturn("jenkins.example.com"); - when(req.getLocalPort()).thenReturn(80); - when(req.getRequestURI()).thenReturn("/bitbucket-scmsource-hook/notify"); - when(req.getHeader("Content-Type")).thenReturn("application/json; charset=utf-8"); - when(req.getHeader("X-Request-Id")).thenReturn(UUID.randomUUID().toString()); - when(req.getHeader("User-Agent")).thenReturn("Atlassian HttpClient 4.2.0 / Bitbucket-9.5.2 (9005002) / Default"); - } - - private void mockPluginRequest(String serverURL) { - when(req.getRemoteHost()).thenReturn("http://localhost:7990"); - when(req.getParameter("server_url")).thenReturn(serverURL); - when(req.getRemoteAddr()).thenReturn("127.0.0.1"); - when(req.getScheme()).thenReturn("https"); - when(req.getServerName()).thenReturn("jenkins.example.com"); - when(req.getLocalPort()).thenReturn(80); - when(req.getRequestURI()).thenReturn("/bitbucket-scmsource-hook/notify"); - when(req.getHeader("Content-Type")).thenReturn("application/json; charset=utf-8"); - when(req.getHeader("X-Bitbucket-Type")).thenReturn("server"); - when(req.getHeader("User-Agent")).thenReturn("Bitbucket version: 8.18.0, Post webhook plugin version: 7.13.41-SNAPSHOT"); - } - - @Test - void test_cloud_signature_is_missing() throws Exception { - BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId); - endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); - - try { - when(req.getHeader("X-Event-Key")).thenReturn("repo:push"); - when(req.getInputStream()).thenReturn(loadResource("cloud/signed_payload.json")); - - /*HttpResponse response = */sut.doNotify(req); - // really hard to verify if response contains a status 400 - verify(hookProcessor, never()).process(any(), anyString(), any(), anyString(), anyString()); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } } @Test - void test_cloud_signature() throws Exception { - mockCloudRequest(); - BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId); - endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); - - try { - when(req.getHeader("X-Event-Key")).thenReturn("repo:push"); - when(req.getHeader("X-Hub-Signature")).thenReturn("sha256=f205c729821c6954aff2afe72b965c34015b4baf96ea8ddc2cc44999c014a035"); - when(req.getInputStream()).thenReturn(loadResource("cloud/signed_payload.json")); - - sut.doNotify(req); - verify(hookProcessor).process( - eq(HookEventType.PUSH), - anyString(), - eq(BitbucketType.CLOUD), - eq("https://bitbucket.org/185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq("https://bitbucket.org")); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_cloud_bad_signature() throws Exception { - BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, false, null , true, credentialsId); - endpoint.setBitbucketJenkinsRootUrl("http://jenkins.acme.com:8080/jenkins"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); - - try { - when(req.getHeader("X-Event-Key")).thenReturn("repo:push"); - when(req.getHeader("X-Hub-Signature")).thenReturn("sha256=f205c729821c6954aff2afe72b965c34015b4baf96ea8ddc2cc44999c014a036"); - when(req.getInputStream()).thenReturn(loadResource("cloud/signed_payload.json")); - - /*HttpResponse response = */sut.doNotify(req); - // really hard to verify if response contains a status 400 - verify(hookProcessor, never()).process(any(), anyString(), any(), anyString(), anyString()); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_native_signature() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, true, credentialsId); - endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); - - try { - mockServerRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("repo:refs_changed"); - when(req.getHeader("X-Hub-Signature")).thenReturn("sha256=4ffba9e7b58ea3d7e1a230446e8c92baea0aeec89b73f598932387254f0de13e"); - when(req.getInputStream()).thenReturn(loadResource("native/signed_payload.json")); - - sut.doNotify(req); - // really hard to verify if response contains a status 400 - verify(hookProcessor).process( - eq(HookEventType.SERVER_REFS_CHANGED), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); - - // verify bad signature - reset(hookProcessor); - when(req.getHeader("X-Hub-Signature")).thenReturn("sha256=4ffba9e7b58ea3d7e1a230446e8c92baea0aeec89b73f598932387254f0de13f"); - when(req.getInputStream()).thenReturn(loadResource("native/signed_payload.json")); - /*HttpResponse response = */ sut.doNotify(req); - // really hard to verify if response contains a status 400 - verify(hookProcessor, never()).process(any(), anyString(), any(), anyString(), anyString()); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_native_ping() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null); + void test_roundtrip() throws Exception { + BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(false, null, true, "hmac")); endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); try { - mockServerRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("diagnostics:ping"); - when(req.getInputStream()).thenReturn(loadResource("native/ping_payload.json")); - - sut.doNotify(req); - verify(hookProcessor).process( - eq(HookEventType.SERVER_PING), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_plugin_pullrequest_created() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null); - endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); - - try { - mockPluginRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:created"); - when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_created.json")); + BitbucketWebhookProcessor hookProcessor = mock(BitbucketWebhookProcessor.class); + sut = new BitbucketSCMSourcePushHookReceiver() { + @Override + Stream getHookProcessors() { + return Stream.of(hookProcessor); + } + }; + mockRequest(); + headers.putAll(HookProcessorTestUtil.getCloudHeaders()); + when(hookProcessor.canHandle(any(), any())).thenReturn(true); + when(hookProcessor.getServerURL(any(), any())).thenReturn(endpoint.getServerURL()); + when(hookProcessor.getEventType(any(), any())).thenReturn("event:X"); sut.doNotify(req); - verify(hookProcessor).process( - eq(HookEventType.PULL_REQUEST_CREATED), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_plugin_pullrequest_updated() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null); - endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); + ArgumentCaptor> headersCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor> parametersCaptor = ArgumentCaptor.forClass(MultiValuedMap.class); - try { - mockPluginRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:updated"); - when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_updated.json")); - - sut.doNotify(req); - verify(hookProcessor).process( - eq(HookEventType.PULL_REQUEST_UPDATED), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } + verify(hookProcessor).canHandle(headersCaptor.capture(), parametersCaptor.capture()); + assertThat(headersCaptor.getValue()).containsAllEntriesOf(headers); + assertThat(parametersCaptor.getValue().entries()).isEmpty(); - @Test - void test_plugin_pullrequest_merged() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null); - endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); + verify(hookProcessor).getServerURL(headersCaptor.capture(), parametersCaptor.capture()); + assertThat(headersCaptor.getValue()).containsAllEntriesOf(headers); + assertThat(parametersCaptor.getValue().entries()).isEmpty(); - try { - mockPluginRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:fulfilled"); - when(req.getInputStream()).thenReturn(loadResource("plugin/pullrequest_merged.json")); + verify(hookProcessor).getEventType(headersCaptor.capture(), parametersCaptor.capture()); + assertThat(headersCaptor.getValue()).containsAllEntriesOf(headers); + assertThat(parametersCaptor.getValue().entries()).isEmpty(); - sut.doNotify(req); + verify(hookProcessor).verifyPayload(headersCaptor.capture(), eq("{}"), eq(endpoint)); verify(hookProcessor).process( - eq(HookEventType.PULL_REQUEST_MERGED), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); + eq("event:X"), + eq("{}"), + eq(Map.of("origin", "https://bitbucket.org/185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify")), + eq(endpoint)); } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); + BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerURL()); } } @Test - void test_plugin_create_branch() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, false, null); + void stop_process_when_multiple_processors_canHandle_incoming_webhook() throws Exception { + BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(false, null, true, "hmac")); endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); try { - mockPluginRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("repo:push"); - when(req.getInputStream()).thenReturn(loadResource("plugin/branch_created.json")); - // when(req.getInputStream()).thenReturn(loadResource("plugin/branch_deleted.json")); - // when(req.getInputStream()).thenReturn(loadResource("plugin/commit_update.json")); - // when(req.getInputStream()).thenReturn(loadResource("plugin/commit_update2.json")); - - sut.doNotify(req); - verify(hookProcessor).process( - eq(HookEventType.PUSH), - anyString(), - eq(BitbucketType.SERVER), - eq("http://localhost:7990/127.0.0.1 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq(endpoint.getServerUrl())); - } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); - } - } - - @Test - void test_native_bad_signature() throws Exception { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("datacenter", "http://localhost:7990/bitbucket", false, null, true, credentialsId); - endpoint.setBitbucketJenkinsRootUrl("https://jenkins.example.com"); - BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); + BitbucketWebhookProcessor hookProcessor = mock(BitbucketWebhookProcessor.class); + when(hookProcessor.canHandle(any(), any())).thenReturn(true); + sut = new BitbucketSCMSourcePushHookReceiver() { + @Override + Stream getHookProcessors() { + BitbucketWebhookProcessor otherHookProcessor = mock(BitbucketWebhookProcessor.class); + when(otherHookProcessor.canHandle(any(), any())).thenReturn(true); + return Stream.of(hookProcessor, otherHookProcessor); + } + }; + mockRequest(); + headers.putAll(HookProcessorTestUtil.getCloudHeaders()); - try { - mockServerRequest(endpoint.getServerUrl()); - when(req.getHeader("X-Event-Key")).thenReturn("repo:refs_changed"); - when(req.getHeader("X-Hub-Signature")).thenReturn("sha256=4ffba9e7b58ea3d7e1a230446e8c92baea0aeec89b73f598932387254f0de13f"); - when(req.getInputStream()).thenReturn(loadResource("native/signed_payload.json")); - /*HttpResponse response = */ sut.doNotify(req); - // really hard to verify if response contains a status 400 - verify(hookProcessor, never()).process(any(), anyString(), any(), anyString(), anyString()); + HttpResponse response = sut.doNotify(req); + assertThat(response).asString().contains("More processors found that handle the incoming Bitbucket hook."); + verify(hookProcessor, never()).getServerURL(any(), any()); } finally { - BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerUrl()); + BitbucketEndpointConfiguration.get().removeEndpoint(endpoint.getServerURL()); } } - @Test - void test_cloud_pullrequest_created() throws Exception { - mockCloudRequest(); - when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:created"); - when(req.getInputStream()).thenReturn(loadResource("cloud/pullrequest_created.json")); - - sut.doNotify(req); - - verify(hookProcessor).process( - eq(HookEventType.PULL_REQUEST_CREATED), - anyString(), - eq(BitbucketType.CLOUD), - eq("https://bitbucket.org/185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq("https://bitbucket.org")); - } - - @Test - void test_cloud_pullrequest_declined() throws Exception { - mockCloudRequest(); - when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:rejected"); - when(req.getInputStream()).thenReturn(loadResource("cloud/pullrequest_rejected.json")); - - sut.doNotify(req); - - verify(hookProcessor).process( - eq(HookEventType.PULL_REQUEST_DECLINED), - anyString(), - eq(BitbucketType.CLOUD), - eq("https://bitbucket.org/185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), - eq("https://bitbucket.org")); - } - - private ServletInputStream loadResource(String resource) { - final InputStream delegate = this.getClass().getResourceAsStream(resource); - return new ServletInputStream() { - - @Override - public int read() throws IOException { - return delegate.read(); - } - - @Override - public void setReadListener(ReadListener readListener) { - } - - @Override - public boolean isReady() { - return !isFinished(); - } - - @Override - public boolean isFinished() { - try { - return delegate.available() == 0; - } catch (IOException e) { - return false; - } - } - }; - } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java deleted file mode 100644 index d1fbfabd2..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2025, Nikolas Falco - * - * 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.jenkins.plugins.bitbucket.hooks; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; -import hudson.scm.SCM; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.List; -import jenkins.scm.api.SCMEvent.Type; -import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMNavigator; -import jenkins.scm.api.SCMSource; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.junit.jupiter.WithJenkins; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class PullRequestHookProcessorTest { - - private PullRequestHookProcessor sut; - protected SCMHeadEvent scmEvent; - - @BeforeEach - void setup() { - sut = new PullRequestHookProcessor() { - @Override - protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { - PullRequestHookProcessorTest.this.scmEvent = event; - } - }; - } - - @Test - void test_pullrequest_created() throws Exception { - sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); - - PREvent event = (PREvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(Type.CREATED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - } - - @Test - void test_pullrequest_rejected() throws Exception { - sut.process(HookEventType.PULL_REQUEST_DECLINED, loadResource("pullrequest_rejected.json"), BitbucketType.CLOUD, "origin"); - - PREvent event = (PREvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(Type.REMOVED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - } - - @Test - void test_pullrequest_created_when_event_match_SCMNavigator() throws Exception { - sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); - - PREvent event = (PREvent) scmEvent; - // discard any scm navigator than bitbucket - assertThat(event.isMatch(mock(SCMNavigator.class))).isFalse(); - - BitbucketSCMNavigator scmNavigator = new BitbucketSCMNavigator("amuniz"); - // cloud could not filter by ProjectKey - assertThat(event.isMatch(scmNavigator)).isTrue(); - // if set must match the project of repository from which the hook is generated - scmNavigator.setProjectKey("PRJKEY"); - assertThat(event.isMatch(scmNavigator)).isTrue(); - // project key is case sensitive - scmNavigator.setProjectKey("prjkey"); - assertThat(event.isMatch(scmNavigator)).isFalse(); - - // workspace/owner is case insensitive - scmNavigator = new BitbucketSCMNavigator("AMUNIZ"); - assertThat(event.isMatch(scmNavigator)).isTrue(); - } - - @WithJenkins - @Test - void test_pullrequest_created_when_event_match_SCMSource(JenkinsRule r) throws Exception { - sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); - - PREvent event = (PREvent) scmEvent; - // discard any scm navigator than bitbucket - assertThat(event.isMatch(mock(SCMSource.class))).isFalse(); - - BitbucketSCMSource scmSource = new BitbucketSCMSource("amuniz", "test-repos"); - // skip scm source that has not been configured to discover PRs - assertThat(event.isMatch(scmSource)).isFalse(); - - scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); - assertThat(event.isMatch(scmSource)).isTrue(); - - // workspace/owner is case insensitive - scmSource = new BitbucketSCMSource("AMUNIZ", "TEST-REPOS"); - scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(1))); - assertThat(event.isMatch(scmSource)).isTrue(); - - assertThat(event.getPullRequests(scmSource)) - .isNotEmpty() - .hasSize(1); - } - - @WithJenkins - @Test - void test_pullrequest_rejected_returns_empty_pullrequests_when_event_match_SCMSource(JenkinsRule r) throws Exception { - sut.process(HookEventType.PULL_REQUEST_DECLINED, loadResource("pullrequest_rejected.json"), BitbucketType.CLOUD, "origin"); - - PREvent event = (PREvent) scmEvent; - - BitbucketSCMSource scmSource = new BitbucketSCMSource("aMUNIZ", "test-repos"); - scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); - assertThat(event.isMatch(scmSource)).isTrue(); - assertThat(event.getPullRequests(scmSource)).isEmpty(); - } - - private String loadResource(String resource) throws IOException { - try (InputStream stream = this.getClass().getResourceAsStream("cloud/" + resource)) { - return IOUtils.toString(stream, StandardCharsets.UTF_8); - } - } -} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessorTest.java deleted file mode 100644 index ef1d66166..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessorTest.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2025, Nikolas Falco - * - * 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.jenkins.plugins.bitbucket.hooks; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; -import hudson.scm.SCM; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; -import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMEvent.Type; -import jenkins.scm.api.SCMHead; -import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMRevision; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.jvnet.hudson.test.Issue; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class PushHookProcessorTest { - - private PushHookProcessor sut; - private SCMHeadEvent scmEvent; - - @BeforeEach - void setup() { - sut = new PushHookProcessor() { - @Override - protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { - PushHookProcessorTest.this.scmEvent = event; - } - }; - } - - @Test - void test_tag_created() throws Exception { - sut.process(HookEventType.PUSH, loadResource("cloud/tag_created.json"), BitbucketType.CLOUD, "origin"); - - PushEvent event = (PushEvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(Type.CREATED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - - BitbucketSCMSource scmSource = new BitbucketSCMSource("AMUNIZ", "test-repos"); - Map heads = event.heads(scmSource); - assertThat(heads.keySet()) - .first() - .usingRecursiveComparison() - .isEqualTo(new BitbucketTagSCMHead("simple-tag", 1738608795000L)); // verify is using last commit date - } - - @Test - void test_annotated_tag_created() throws Exception { - sut.process(HookEventType.PUSH, loadResource("cloud/annotated_tag_created.json"), BitbucketType.CLOUD, "origin"); - - PushEvent event = (PushEvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(Type.CREATED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - - BitbucketSCMSource scmSource = new BitbucketSCMSource("AMUNIz", "test-repos"); - Map heads = event.heads(scmSource); - assertThat(heads.keySet()) - .first() - .usingRecursiveComparison() - .isEqualTo(new BitbucketTagSCMHead("test-tag", 1738608816000L)); - } - - @Test - void test_commmit_created() throws Exception { - sut.process(HookEventType.PUSH, loadResource("cloud/commit_created.json"), BitbucketType.CLOUD, "origin"); - - PushEvent event = (PushEvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(Type.UPDATED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - - BitbucketSCMSource scmSource = new BitbucketSCMSource("aMUNIZ", "test-repos"); - Map heads = event.heads(scmSource); - assertThat(heads.keySet()) - .first() - .usingRecursiveComparison() - .isEqualTo(new BranchSCMHead("feature/issue-819")); - assertThat(heads.values()) - .first() - .usingRecursiveComparison() - .isEqualTo(new SCMRevisionImpl(new BranchSCMHead("feature/issue-819"), "5ecffa3874e96920f24a2b3c0d0038e47d5cd1a4")); - } - - @Test - void test_push_server() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("server/pushPayload.json"), BitbucketType.SERVER, "origin"); - - PushEvent event = (PushEvent) scmEvent; - assertThat(event).isNotNull(); - assertThat(event.getSourceName()).isEqualTo("test-repos"); - assertThat(event.getType()).isEqualTo(SCMEvent.Type.UPDATED); - assertThat(event.isMatch(mock(SCM.class))).isFalse(); - - BitbucketSCMSource scmSource = new BitbucketSCMSource("aMUNIZ", "test-repos"); - Map heads = event.heads(scmSource); - assertThat(heads.keySet()) - .first() - .usingRecursiveComparison() - .isEqualTo(new BranchSCMHead("main")); - assertThat(heads.values()) - .first() - .usingRecursiveComparison() - .isEqualTo(new SCMRevisionImpl(new BranchSCMHead("main"), "9fdd7b96d3f5c276d0b9e0bf38c879eb112d889a")); - } - - @Test - @Issue("JENKINS-55927") - void test_push_server_empty_changes() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("server/emptyPayload.json"), BitbucketType.SERVER, "origin"); - assertThat(scmEvent).isNull(); - } - - private String loadResource(String resource) throws IOException { - try (InputStream stream = this.getClass().getResourceAsStream(resource)) { - return IOUtils.toString(stream, StandardCharsets.UTF_8); - } - } -} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListenerTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListenerTest.java index bb359bd85..92fe7a760 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListenerTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListenerTest.java @@ -31,6 +31,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.test.util.BitbucketTestUtil; import hudson.model.TaskListener; @@ -81,7 +82,7 @@ void test_register() throws Exception { StringCredentials credentials = BitbucketTestUtil.registerHookCredentials("password", rule); - BitbucketEndpoint endpoint = new BitbucketServerEndpoint("datacenter", serverURL, true, "dummyId", true, credentials.getId()); + BitbucketEndpoint endpoint = new BitbucketServerEndpoint("datacenter", serverURL, new ServerWebhook(true, "dummyId", true, credentials.getId())); ((BitbucketServerEndpoint) endpoint).setBitbucketJenkinsRootUrl("https://jenkins.example.com/"); BitbucketEndpointConfiguration.get().updateEndpoint(endpoint); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java index 406d55143..00df0332a 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java @@ -48,17 +48,6 @@ static void init(JenkinsRule rule) { ConfigurationAsCode.get().configure(jcacURL); } - @Test - void given_instanceWithServerVersion6_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { - WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = mock(BitbucketSCMSource.class); - final String server = "http://bitbucket.example.com:8088"; - when(owner.getServerUrl()).thenReturn(server); - when(owner.getEndpointJenkinsRootURL()).thenReturn(server); - BitbucketWebHook hook = whc.getHook(owner); - assertThat(hook.getEvents()).contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); - } - @Test void given_instanceWithServerVersion6_5_when_getHooks_SERVER_MIRROR_REPO_SYNC_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); @@ -72,39 +61,6 @@ void given_instanceWithServerVersion6_5_when_getHooks_SERVER_MIRROR_REPO_SYNC_EV assertThat(hook.getEvents()).contains(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey()); } - @Test - void given_instanceWithServerVersion510_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { - WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = mock(BitbucketSCMSource.class); - final String server = "http://bitbucket.example.com:8089"; - when(owner.getServerUrl()).thenReturn(server); - when(owner.getEndpointJenkinsRootURL()).thenReturn(server); - BitbucketWebHook hook = whc.getHook(owner); - assertThat(hook.getEvents()).contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); - } - - @Test - void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_not_exists() { - WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = mock(BitbucketSCMSource.class); - final String server = "http://bitbucket.example.com:8090"; - when(owner.getServerUrl()).thenReturn(server); - when(owner.getEndpointJenkinsRootURL()).thenReturn(server); - BitbucketWebHook hook = whc.getHook(owner); - assertThat(hook.getEvents()).doesNotContain(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); - } - - @Test - void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_MOD_EVENT_not_exists() { - WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = mock(BitbucketSCMSource.class); - final String server = "http://bitbucket.example.com:8090"; - when(owner.getServerUrl()).thenReturn(server); - when(owner.getEndpointJenkinsRootURL()).thenReturn(server); - BitbucketWebHook hook = whc.getHook(owner); - assertThat(hook.getEvents()).doesNotContain(HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey()); - } - @Test void given_instanceWithServerVersion7_when_getHooks_SERVER_PR_FROM_REF_UPDATED_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/ExponentialBackOffRetryStrategyTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/ExponentialBackOffRetryStrategyTest.java index 9200df35f..dce7cf9a6 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/ExponentialBackOffRetryStrategyTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/ExponentialBackOffRetryStrategyTest.java @@ -25,7 +25,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; import java.io.IOException; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; @@ -42,7 +41,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIOException; -import static org.mockito.Mockito.mock; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; @@ -65,8 +63,7 @@ void test_retry(ClientAndServer mockServer) throws Exception { "test", "testRepos", (BitbucketAuthenticator) null, - false, - mock(BitbucketServerWebhookImplementation.class)) { + false) { @Override protected HttpClientBuilder setupClientBuilder() { return super.setupClientBuilder() diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpointTest.java index 2dd6f8540..282aead6d 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketCloudEndpointTest.java @@ -24,6 +24,7 @@ package com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; import com.damnhandy.uri.template.UriTemplate; import hudson.Util; import jenkins.model.Jenkins; @@ -42,7 +43,7 @@ void smokes() { BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(); assertThat(endpoint.getDisplayName()).isNotNull(); - assertThat(endpoint.getServerUrl()).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); + assertThat(endpoint.getServerURL()).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); /* The endpoints should set (literally, not normalized) and return * the bitbucketJenkinsRootUrl if the management of hooks is enabled */ @@ -77,7 +78,7 @@ void getUnmanagedDefaultRootUrl(JenkinsRule rule) { String jenkinsRootURL = Util.ensureEndsWith(URLUtils.normalizeURL(Jenkins.get().getRootUrl()), "/"); assertThat(new BitbucketCloudEndpoint().getEndpointJenkinsRootUrl()) .isEqualTo(jenkinsRootURL); - assertThat(new BitbucketCloudEndpoint(false, 0, 0, false, "{cred}", false, null).getEndpointJenkinsRootURL()) + assertThat(new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(false, "{cred}", false, null)).getEndpointJenkinsRootURL()) .isEqualTo(jenkinsRootURL); } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpointTest.java index c42197d15..159a94310 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/BitbucketServerEndpointTest.java @@ -24,6 +24,7 @@ package com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.URLUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import hudson.Util; import hudson.util.FormValidation; import jenkins.model.Jenkins; @@ -74,15 +75,15 @@ void smokes() { @Test void getUnmanagedDefaultRootUrl(JenkinsRule rule) { String jenkinsRootURL = Util.ensureEndsWith(URLUtils.normalizeURL(Jenkins.get().getRootUrl()), "/"); - assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", true, null, false, null).getEndpointJenkinsRootUrl()) + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", new ServerWebhook(true, null, false, null)).getEndpointJenkinsRootUrl()) .isEqualTo(jenkinsRootURL); - assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, "{cred}", false, null).getEndpointJenkinsRootURL()) + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", new ServerWebhook(false, "{cred}", false, null)).getEndpointJenkinsRootURL()) .isEqualTo(jenkinsRootURL); } @Test void getRepositoryUrl() { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null, false, null); + BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("Dummy", "http://dummy.example.com"); assertThat(endpoint.getRepositoryUrl("TST", "test-repo")).isEqualTo("http://dummy.example.com/projects/TST/repos/test-repo"); assertThat(endpoint.getRepositoryUrl("~tester", "test-repo")).isEqualTo("http://dummy.example.com/users/tester/repos/test-repo"); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/DummyEndpointConfiguration.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/DummyEndpointConfiguration.java index 0432ca7a9..75a9da1f1 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/DummyEndpointConfiguration.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/endpoint/DummyEndpointConfiguration.java @@ -25,6 +25,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointDescriptor; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.damnhandy.uri.template.UriTemplate; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; @@ -32,7 +33,7 @@ public class DummyEndpointConfiguration extends AbstractBitbucketEndpoint { DummyEndpointConfiguration(boolean manageHooks, String credentialsId) { - super(manageHooks, credentialsId, false, null); + super(new ServerWebhook(manageHooks, credentialsId, false, null)); } @Override @@ -40,12 +41,6 @@ public String getDisplayName() { return "Dummy"; } - @NonNull - @Override - public String getServerUrl() { - return getServerURL(); - } - @NonNull @Override public String getServerURL() { @@ -65,7 +60,7 @@ public EndpointType getType() { @NonNull @Override - public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + public String getRepositoryURL(@NonNull String repoOwner, @NonNull String repository) { return UriTemplate .fromTemplate("http://dummy.example.com{/owner,repo}") .set("owner", repoOwner) diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java index e81112523..c3f9e0199 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java @@ -37,6 +37,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.notifier.BitbucketBuildStatusNotifications.JobCheckoutListener; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhook; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; import com.cloudbees.jenkins.plugins.bitbucket.trait.BitbucketBuildStatusNotificationsTrait; import com.cloudbees.jenkins.plugins.bitbucket.trait.ForkPullRequestDiscoveryTrait; @@ -279,10 +281,10 @@ public static Stream buildServerURLsProvider() { @ParameterizedTest(name = "checkURL {0} against Bitbucket Server") @MethodSource("buildServerURLsProvider") void test_checkURL_for_Bitbucket_server(String jenkinsURL, String expectedExceptionMsg, @NonNull JenkinsRule r) { - BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("Bitbucket Server", "https://bitbucket.server", true, "dummy"); + BitbucketServerEndpoint endpoint = new BitbucketServerEndpoint("Bitbucket Server", "https://bitbucket.server", new ServerWebhook(true, "dummy")); BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); - BitbucketApi client = getApiMockClient(endpoint.getServerUrl()); + BitbucketApi client = getApiMockClient(endpoint.getServerURL()); if (expectedExceptionMsg != null) { assertThatIllegalStateException() .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) @@ -314,11 +316,10 @@ public static Stream buildCloudURLsProvider() { @ParameterizedTest(name = "checkURL {0} against Bitbucket Cloud") @MethodSource("buildCloudURLsProvider") void test_checkURL_for_Bitbucket_cloud(String jenkinsURL, String expectedExceptionMsg, @NonNull JenkinsRule r) { - @SuppressWarnings("deprecation") - BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, true, "second"); + BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhook(true, "second")); BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); - BitbucketApi client = getApiMockClient(endpoint.getServerUrl()); + BitbucketApi client = getApiMockClient(endpoint.getServerURL()); if (expectedExceptionMsg != null) { assertThatIllegalStateException() .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessorTest.java new file mode 100644 index 000000000..354e9be4d --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/AbstractWebhookProcessorTest.java @@ -0,0 +1,135 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.impl.webhook; + +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookProcessorException; +import com.cloudbees.plugins.credentials.CredentialsScope; +import hudson.util.Secret; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jenkins.scm.api.SCMHeadEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AbstractWebhookProcessorTest { + + private AbstractWebhookProcessor sut; + protected SCMHeadEvent scmEvent; + + @BeforeEach + void setup() { + sut = new AbstractWebhookProcessor() { + + @Override + public boolean canHandle(Map headers, MultiValuedMap parameters) { + return true; + } + + @Override + public void process(String eventType, String payload, Map origin, BitbucketEndpoint endpoint) { + throw new UnsupportedOperationException(); + } + }; + } + + @Test + void test_getEventType() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + + headers.put("X-Event-Key", "pullrequest:created"); + assertThat(sut.getEventType(headers, parameters)).isEqualTo("pullrequest:created"); + } + + void test_getServerURL() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", "https://localhost:8080"); + + assertThat(sut.getServerURL(headers, parameters)).isEqualTo("https://localhost:8080"); + } + + @Test + void test_signature() throws Exception { + BitbucketEndpoint endpoint = getEndpoint(); + Map headers = new HashMap<>(); + headers.put("X-Hub-Signature", "sha256=f205c729821c6954aff2afe72b965c34015b4baf96ea8ddc2cc44999c014a035"); + + assertDoesNotThrow(() -> sut.verifyPayload(headers, loadResource("signed_payload.json"), endpoint)); + } + + @Test + void test_bad_signature() throws Exception { + BitbucketEndpoint endpoint = getEndpoint(); + Map headers = new HashMap<>(); + headers.put("X-Hub-Signature", "sha256=f205c729821c6954aff2afe72b965c34015b4baf96ea8ddc2cc44999c014a036"); + + assertThatThrownBy(() -> sut.verifyPayload(headers, loadResource("signed_payload.json"), endpoint)) + .isInstanceOf(BitbucketWebhookProcessorException.class) + .hasMessage("Signature verification failed") + .asInstanceOf(InstanceOfAssertFactories.type(BitbucketWebhookProcessorException.class)) + .satisfies(ex -> assertThat(ex.getHttpCode()).isEqualTo(403)); + } + + @Test + void test_signature_is_missing() throws Exception { + BitbucketEndpoint endpoint = getEndpoint(); + + assertThatThrownBy(() -> sut.verifyPayload(Collections.emptyMap(), loadResource("signed_payload.json"), endpoint)) + .isInstanceOf(BitbucketWebhookProcessorException.class) + .hasMessage("Payload has not be signed, configure the webHook secret in Bitbucket as documented at https://github.com/jenkinsci/bitbucket-branch-source-plugin/blob/master/docs/USER_GUIDE.adoc#webhooks-registering") + .asInstanceOf(InstanceOfAssertFactories.type(BitbucketWebhookProcessorException.class)) + .satisfies(ex -> assertThat(ex.getHttpCode()).isEqualTo(403)); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("cloud/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } + + private BitbucketEndpoint getEndpoint() { + String credentialsId = "hmac"; + BitbucketEndpoint endpoint = mock(BitbucketEndpoint.class); + when(endpoint.hookSignatureCredentials()).thenReturn(new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, null, Secret.fromString("Gkvl$k$wyNpQAF42"))); + return endpoint; + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessorTest.java new file mode 100644 index 000000000..ec55dd9d8 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPullRequestWebhookProcessorTest.java @@ -0,0 +1,194 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; +import hudson.scm.SCM; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMEvent.Type; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMSource; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class CloudPullRequestWebhookProcessorTest { + + private CloudPullRequestWebhookProcessor sut; + private CloudPREvent scmEvent; + + @BeforeEach + void setup() { + sut = new CloudPullRequestWebhookProcessor() { + @Override + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + CloudPullRequestWebhookProcessorTest.this.scmEvent = (CloudPREvent) event; + } + }; + } + + @Test + void test_getServerURL_return_always_cloud_URL() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", "https://localhost:8080"); + + assertThat(sut.getServerURL(headers, parameters)).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_cloud_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getCloudHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:created"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:rejected"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:fulfilled"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:updated"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "repo:push"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + } + + @Test + void test_pullrequest_created() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("test-repos"); + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + assertThat(scmEvent.isMatch(mock(SCM.class))).isFalse(); + } + + @Test + void test_pullrequest_rejected() throws Exception { + sut.process(HookEventType.PULL_REQUEST_DECLINED.getKey(), loadResource("pullrequest_rejected.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("test-repos"); + assertThat(scmEvent.getType()).isEqualTo(Type.REMOVED); + assertThat(scmEvent.isMatch(mock(SCM.class))).isFalse(); + } + + @Test + void test_pullrequest_created_when_event_match_SCMNavigator() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMNavigator.class))).isFalse(); + + BitbucketSCMNavigator scmNavigator = new BitbucketSCMNavigator("amuniz"); + // cloud could not filter by ProjectKey + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // if set must match the project of repository from which the hook is generated + scmNavigator.setProjectKey("PRJKEY"); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // project key is case sensitive + scmNavigator.setProjectKey("prjkey"); + assertThat(scmEvent.isMatch(scmNavigator)).isFalse(); + + // workspace/owner is case insensitive + scmNavigator = new BitbucketSCMNavigator("AMUNIZ"); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + } + + @WithJenkins + @Test + void test_pullrequest_created_when_event_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMSource.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("amuniz", "test-repos"); + // skip scm source that has not been configured to discover PRs + assertThat(scmEvent.isMatch(scmSource)).isFalse(); + + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + + // workspace/owner is case insensitive + scmSource = new BitbucketSCMSource("AMUNIZ", "TEST-REPOS"); + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(1))); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + + assertThat(scmEvent.getPullRequests(scmSource)) + .isNotEmpty() + .hasSize(1); + } + + @WithJenkins + @Test + void test_pullrequest_rejected_returns_empty_pullrequests_when_event_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PULL_REQUEST_DECLINED.getKey(), loadResource("pullrequest_rejected.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("aMUNIZ", "test-repos"); + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + assertThat(scmEvent.getPullRequests(scmSource)).isEmpty(); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("cloud/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessorTest.java new file mode 100644 index 000000000..b2b217271 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudPushWebhookProcessorTest.java @@ -0,0 +1,169 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.impl.webhook.cloud; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import hudson.scm.SCM; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; +import jenkins.scm.api.SCMEvent.Type; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMRevision; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class CloudPushWebhookProcessorTest { + + private CloudPushWebhookProcessor sut; + private CloudPushEvent scmEvent; + + @BeforeEach + void setup() { + sut = new CloudPushWebhookProcessor() { + @Override + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + CloudPushWebhookProcessorTest.this.scmEvent = (CloudPushEvent) event; + } + }; + } + + @Test + void test_getServerURL_return_always_cloud_URL() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", "https://localhost:8080"); + + assertThat(sut.getServerURL(headers, parameters)).isEqualTo(BitbucketCloudEndpoint.SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_cloud_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getCloudHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:created"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:rejected"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:fulfilled"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:updated"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "repo:push"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + } + + @Test + void test_tag_created() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("tag_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("test-repos"); + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + assertThat(scmEvent.isMatch(mock(SCM.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("AMUNIZ", "test-repos"); + Map heads = scmEvent.heads(scmSource); + assertThat(heads.keySet()) + .first() + .usingRecursiveComparison() + .isEqualTo(new BitbucketTagSCMHead("simple-tag", 1738608795000L)); // verify is using last commit date + } + + @Test + void test_annotated_tag_created() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("annotated_tag_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("test-repos"); + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + assertThat(scmEvent.isMatch(mock(SCM.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("AMUNIz", "test-repos"); + Map heads = scmEvent.heads(scmSource); + assertThat(heads.keySet()) + .first() + .usingRecursiveComparison() + .isEqualTo(new BitbucketTagSCMHead("test-tag", 1738608816000L)); + } + + @Test + void test_commmit_created() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("commit_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("test-repos"); + assertThat(scmEvent.getType()).isEqualTo(Type.UPDATED); + assertThat(scmEvent.isMatch(mock(SCM.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("aMUNIZ", "test-repos"); + Map heads = scmEvent.heads(scmSource); + assertThat(heads.keySet()) + .first() + .usingRecursiveComparison() + .isEqualTo(new BranchSCMHead("feature/issue-819")); + assertThat(heads.values()) + .first() + .usingRecursiveComparison() + .isEqualTo(new SCMRevisionImpl(new BranchSCMHead("feature/issue-819"), "5ecffa3874e96920f24a2b3c0d0038e47d5cd1a4")); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("cloud/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessorTest.java new file mode 100644 index 000000000..7ba9efe12 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPullRequestWebhookProcessorTest.java @@ -0,0 +1,196 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMEvent.Type; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMSource; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SuppressWarnings("deprecation") +class PluginPullRequestWebhookProcessorTest { + private static final String SERVER_URL = "http://localhost:7990"; + + private PluginPullRequestWebhookProcessor sut; + private PluginPREvent scmEvent; + + @BeforeEach + void setup() { + sut = new PluginPullRequestWebhookProcessor() { + @Override + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + PluginPullRequestWebhookProcessorTest.this.scmEvent = (PluginPREvent) event; + } + }; + } + + @Test + void test_getServerURL_return_always_cloud_URL() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.getServerURL(headers, parameters)).isEqualTo(SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_cloud_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getPluginHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pullrequest:created"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:rejected"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:fulfilled"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:updated"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "repo:push"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + } + + @Test + void test_pullrequest_created() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + } + + @Test + void test_pullrequest_merged() throws Exception { + sut.process(HookEventType.PULL_REQUEST_MERGED.getKey(), loadResource("pullrequest_merged.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(Type.REMOVED); + } + + @Test + void test_pullrequest_updated() throws Exception { + sut.process(HookEventType.PULL_REQUEST_UPDATED.getKey(), loadResource("pullrequest_updated.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull().isInstanceOf(PluginPREvent.class); + + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(Type.UPDATED); + } + + @Test + void test_PREvent_match_SCMNavigator() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMNavigator.class))).isFalse(); + + BitbucketSCMNavigator scmNavigator = new BitbucketSCMNavigator("PROJECT_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isFalse(); + // match only if projectKey and serverURL matches + scmNavigator.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // if set must match the project of repository from which the hook is generated + scmNavigator.setProjectKey("PROJECT_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // project key is case sensitive + scmNavigator.setProjectKey("project_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isFalse(); + + // workspace/owner is case insensitive + scmNavigator = new BitbucketSCMNavigator("project_1"); + scmNavigator.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + } + + @WithJenkins + @Test + void test_PREvent_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED.getKey(), loadResource("pullrequest_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMSource.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("PROJECT_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + // skip scm source that has not been configured to discover PRs + assertThat(scmEvent.isMatch(scmSource)).isFalse(); + + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + + // workspace/owner is case insensitive + scmSource = new BitbucketSCMSource("project_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(1))); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + + assertThat(scmEvent.getPullRequests(scmSource)) + .isNotEmpty() + .hasSize(1); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("plugin/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessorTest.java new file mode 100644 index 000000000..2e658c125 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessorTest.java @@ -0,0 +1,231 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.impl.webhook.plugin; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; +import jenkins.scm.api.SCMEvent; +import jenkins.scm.api.SCMEvent.Type; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@SuppressWarnings("deprecation") +class PluginPushWebhookProcessorTest { + + private static final String SERVER_URL = "http://localhost:7990"; + private PluginPushWebhookProcessor sut; + private PluginPushEvent scmEvent; + + @BeforeEach + void setup() { + sut = new PluginPushWebhookProcessor() { + @Override + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + PluginPushWebhookProcessorTest.this.scmEvent = (PluginPushEvent) event; + } + }; + } + + @Test + void test_getServerURL_return_always_cloud_URL() throws Exception { + Map headers = new HashMap<>(); + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.getServerURL(headers, parameters)).isEqualTo(SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_native_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getPluginHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:opened"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:merged"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "repo:push"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pullrequest:updated"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + } + + @Test + void test_push_server_UPDATE_2() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("commit_update2.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(SCMEvent.Type.UPDATED); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("PROJECT_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + Map heads = scmEvent.heads(scmSource); + assertThat(heads) + .containsKey(new BranchSCMHead("master")) + .containsValue(new SCMRevisionImpl(new BranchSCMHead("master"), "500cf91e7b4b7d9f995cdb6e81cb5538216ac02e")); + } + + @Test + void test_push_server_UPDATE() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("commit_update.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(SCMEvent.Type.UPDATED); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("PROJEct_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + Map heads = scmEvent.heads(scmSource); + assertThat(heads) + .containsKey(new BranchSCMHead("test-webhook")) + .containsValue(new SCMRevisionImpl(new BranchSCMHead("test-webhook"), "c0158b3e6c8cecf3bddc39d20957a98660cd23fd")); + } + + @Test + void test_push_server_CREATED() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("branch_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(SCMEvent.Type.CREATED); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("pROJECT_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + Map heads = scmEvent.heads(scmSource); + assertThat(heads) + .containsKey(new BranchSCMHead("test-webhook")) + .containsValue(new SCMRevisionImpl(new BranchSCMHead("test-webhook"), "417b2f673581ee6000e260a5fa65e62b56c7a3cd")); + } + + @Test + void test_push_server_REMOVED() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("branch_deleted.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent).isNotNull(); + assertThat(scmEvent.getSourceName()).isEqualTo("rep_1"); + assertThat(scmEvent.getType()).isEqualTo(SCMEvent.Type.REMOVED); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("pROJECT_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + Map heads = scmEvent.heads(scmSource); + assertThat(heads).containsKey(new BranchSCMHead("test-webhook")); + assertThat(heads.values()).containsNull(); + } + + @Test + void test_PushEvent_match_SCMNavigator() throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("branch_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + assertThat(scmEvent.getType()).isEqualTo(Type.CREATED); + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMNavigator.class))).isFalse(); + + BitbucketSCMNavigator scmNavigator = new BitbucketSCMNavigator("PROJECT_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isFalse(); + // match only if projectKey and serverURL matches + scmNavigator.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // if set must match the project of repository from which the hook is generated + scmNavigator.setProjectKey("PROJECT_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + // project key is case sensitive + scmNavigator.setProjectKey("project_1"); + assertThat(scmEvent.isMatch(scmNavigator)).isFalse(); + + // workspace/owner is case insensitive + scmNavigator = new BitbucketSCMNavigator("project_1"); + scmNavigator.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmNavigator)).isTrue(); + } + + @WithJenkins + @Test + void test_PushEvent_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PUSH.getKey(), loadResource("branch_created.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + + // discard any scm navigator than bitbucket + assertThat(scmEvent.isMatch(mock(SCMSource.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("PROJECT_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + + // workspace/owner is case insensitive + scmSource = new BitbucketSCMSource("project_1", "rep_1"); + scmSource.setServerUrl(SERVER_URL); + assertThat(scmEvent.isMatch(scmSource)).isTrue(); + } + + @Test + @Issue("JENKINS-55927") + void test_push_server_empty_changes() throws Exception { + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("emptyPayload.json"), Collections.emptyMap(), mock(BitbucketEndpoint.class)); + assertThat(scmEvent).isNull(); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("plugin/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessorTest.java new file mode 100644 index 000000000..412877f49 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPingWebhookProcessorTest.java @@ -0,0 +1,105 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Allan Burdajewicz + * + * 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.jenkins.plugins.bitbucket.impl.webhook.server; + +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerPingWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jenkins.scm.api.SCMHeadEvent; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@WithJenkins +class ServerPingWebhookProcessorTest { + + private static final String SERVER_URL = "http://localhost:7990"; + private ServerPingWebhookProcessor sut; + private SCMHeadEvent scmEvent; + private BitbucketEndpoint endpoint; + + @BeforeEach + void setup() { + sut = new ServerPingWebhookProcessor() { + @Override + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + ServerPingWebhookProcessorTest.this.scmEvent = event; + } + }; + endpoint = mock(BitbucketEndpoint.class); + when(endpoint.getServerURL()).thenReturn(SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_native_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getNativeHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:opened"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:merged"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "diagnostics:ping"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + } + + @Test + void test_ping() throws Exception { + sut.process(HookEventType.SERVER_PING.getKey(), loadResource("ping.json"), Collections.emptyMap(), endpoint); + + assertThat(scmEvent).isNull(); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("native/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessorTest.java similarity index 56% rename from src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessorTest.java rename to src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessorTest.java index 6473f116d..f6ef1b498 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessorTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPullRequestWebhookProcessorTest.java @@ -21,17 +21,24 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; import com.cloudbees.jenkins.plugins.bitbucket.api.PullRequestBranchType; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerHeadEvent; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerPullRequestWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; import hudson.scm.SCM; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,6 +48,8 @@ import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,28 +59,72 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -class NativeServerPullRequestHookProcessorTest { +class ServerPullRequestWebhookProcessorTest { private static final String SERVER_URL = "http://localhost:7990"; - private NativeServerPullRequestHookProcessor sut; + private ServerPullRequestWebhookProcessor sut; private SCMHeadEvent scmEvent; + private BitbucketEndpoint endpoint; @BeforeEach void setup() { - sut = new NativeServerPullRequestHookProcessor() { + sut = new ServerPullRequestWebhookProcessor() { @Override - protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { - NativeServerPullRequestHookProcessorTest.this.scmEvent = event; + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + ServerPullRequestWebhookProcessorTest.this.scmEvent = event; } }; + endpoint = mock(BitbucketEndpoint.class); + when(endpoint.getServerURL()).thenReturn(SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_native_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getNativeHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:opened"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:merged"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:declined"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:deleted"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:modified"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:from_ref_updated"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "pr:reviewer:updated"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:reviewer:approved"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); } @WithJenkins @Test @Issue("JENKINS-75523") void test_pr_where_source_is_tag(JenkinsRule rule) throws Exception { - sut.process(HookEventType.SERVER_PULL_REQUEST_OPENED, loadResource("native/prOpenFromTagPayload.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_PULL_REQUEST_OPENED.getKey(), loadResource("prOpenFromTagPayload.json"), Collections.emptyMap(), endpoint); ServerHeadEvent event = (ServerHeadEvent) scmEvent; assertThat(event).isNotNull(); @@ -90,7 +143,7 @@ void test_pr_where_source_is_tag(JenkinsRule rule) throws Exception { } private String loadResource(String resource) throws IOException { - try (InputStream stream = this.getClass().getResourceAsStream(resource)) { + try (InputStream stream = this.getClass().getResourceAsStream("native/" + resource)) { return IOUtils.toString(stream, StandardCharsets.UTF_8); } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessorTest.java similarity index 69% rename from src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessorTest.java rename to src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessorTest.java index e69516295..45f8630cb 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessorTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerPushWebhookProcessorTest.java @@ -21,23 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.hooks; +package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMockApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerPushWebhookProcessor; +import com.cloudbees.jenkins.plugins.bitbucket.test.util.HookProcessorTestUtil; import hudson.scm.SCM; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMRevision; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -48,14 +57,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @WithJenkins -class NativeServerPushHookProcessorTest { +class ServerPushWebhookProcessorTest { private static final String SERVER_URL = "http://localhost:7990"; private static final String MIRROR_ID = "ABCD-1234-EFGH-5678"; - private NativeServerPushHookProcessor sut; + private ServerPushWebhookProcessor sut; private SCMHeadEvent scmEvent; + private BitbucketEndpoint endpoint; static JenkinsRule rule; @@ -66,18 +77,48 @@ static void init(JenkinsRule r) { @BeforeEach void setup() { - sut = new NativeServerPushHookProcessor() { + sut = new ServerPushWebhookProcessor() { @Override - protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { - NativeServerPushHookProcessorTest.this.scmEvent = event; + public void notifyEvent(SCMHeadEvent event, int delaySeconds) { + ServerPushWebhookProcessorTest.this.scmEvent = event; } }; + endpoint = mock(BitbucketEndpoint.class); + when(endpoint.getServerURL()).thenReturn(SERVER_URL); + } + + @Test + void test_reindexOnEmptyChanges_is_disable_by_default() throws Exception { + assertThat(sut.reindexOnEmptyChanges()).isFalse(); + } + + @Test + void test_canHandle_only_pass_specific_native_hook() throws Exception { + MultiValuedMap parameters = new ArrayListValuedHashMap<>(); + parameters.put("server_url", SERVER_URL); + + assertThat(sut.canHandle(new HashMap<>(), parameters)).isFalse(); + + Map headers = HookProcessorTestUtil.getNativeHeaders(); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:opened"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "pr:merged"); + assertThat(sut.canHandle(headers, parameters)).isFalse(); + + headers.put("X-Event-Key", "repo:refs_changed"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); + + headers.put("X-Event-Key", "mirror:repo_synchronized"); + assertThat(sut.canHandle(headers, parameters)).isTrue(); } @Test @Issue("JENKINS-55927") void test_mirror_sync_changes() throws Exception { - sut.process(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED, loadResource("native/mirrorSynchronized.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey(), loadResource("mirrorSynchronized.json"), Collections.emptyMap(), endpoint); ServerPushEvent event = (ServerPushEvent) scmEvent; assertThat(event).isNotNull(); @@ -97,7 +138,7 @@ void test_mirror_sync_changes() throws Exception { @Test @Issue("JENKINS-75604") void test_annotated_tag_create_event() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("native/annotated_tag_created.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("annotated_tag_created.json"), Collections.emptyMap(), endpoint); assertThat(scmEvent) .isInstanceOf(ServerPushEvent.class) .isNotNull(); @@ -118,7 +159,7 @@ void test_annotated_tag_create_event() throws Exception { @Test @Issue("JENKINS-75604") void test_tag_created_event() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("native/tag_created.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("tag_created.json"), Collections.emptyMap(), endpoint); assertThat(scmEvent) .isInstanceOf(ServerPushEvent.class) .isNotNull(); @@ -139,7 +180,7 @@ void test_tag_created_event() throws Exception { @Test @Issue("JENKINS-75604") void test_tag_deleted_event() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("native/tag_deleted.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("tag_deleted.json"), Collections.emptyMap(), endpoint); assertThat(scmEvent) .isInstanceOf(ServerPushEvent.class) .isNotNull(); @@ -160,14 +201,14 @@ void test_tag_deleted_event() throws Exception { @Test @Issue("JENKINS-55927") void test_mirror_sync_reflimitexceeed() throws Exception { - sut.process(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED, loadResource("native/mirrorSynchronized_refLimitExceeded.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey(), loadResource("mirrorSynchronized_refLimitExceeded.json"), Collections.emptyMap(), endpoint); ServerPushEvent event = (ServerPushEvent) scmEvent; assertThat(event).isNull(); } @Test void test_push() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("native/pushPayload.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("pushPayload.json"), Collections.emptyMap(), endpoint); ServerPushEvent event = (ServerPushEvent) scmEvent; assertThat(event).isNotNull(); @@ -190,13 +231,13 @@ void test_push() throws Exception { @Test @Issue("JENKINS-55927") void test_push_empty_changes() throws Exception { - sut.process(HookEventType.SERVER_REFS_CHANGED, loadResource("native/emptyPayload.json"), BitbucketType.SERVER, "origin", SERVER_URL); + sut.process(HookEventType.SERVER_REFS_CHANGED.getKey(), loadResource("emptyPayload.json"), Collections.emptyMap(), endpoint); ServerPushEvent event = (ServerPushEvent) scmEvent; assertThat(event).isNull(); } private String loadResource(String resource) throws IOException { - try (InputStream stream = this.getClass().getResourceAsStream(resource)) { + try (InputStream stream = this.getClass().getResourceAsStream("native/" + resource)) { return IOUtils.toString(stream, StandardCharsets.UTF_8); } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java index 2ed509d2a..1d66565e3 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClientTest.java @@ -35,7 +35,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketClientCertificateAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketOAuthAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketUsernamePasswordAuthenticator; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.test.util.BitbucketTestUtil; import hudson.ProxyConfiguration; import java.io.InputStream; @@ -269,7 +268,7 @@ void test_no_proxy_configurations() throws Exception { j.jenkins.setProxy(proxyConfiguration); AtomicReference builderReference = new AtomicReference<>(); - try(BitbucketApi client = new BitbucketServerAPIClient(serverURL, "amuniz", "test-repos", mock(BitbucketUsernamePasswordAuthenticator.class), false, BitbucketServerWebhookImplementation.NATIVE) { + try(BitbucketApi client = new BitbucketServerAPIClient(serverURL, "amuniz", "test-repos", mock(BitbucketUsernamePasswordAuthenticator.class), false) { @Override protected void setClientProxyParams(HttpClientBuilder builder) { builderReference.set(spy(builder)); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/HookProcessorTestUtil.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/HookProcessorTestUtil.java new file mode 100644 index 000000000..017f3d031 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/HookProcessorTestUtil.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.test.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class HookProcessorTestUtil { + + private HookProcessorTestUtil() { + } + + public static Map getCloudHeaders() { + Map headers = new HashMap<>(); + headers.put("User-Agent", "Bitbucket-Webhooks/2.0"); + headers.put("X-Attempt-Number", "1"); + headers.put("Content-Type", "application/json"); + headers.put("X-Hook-UUID", UUID.randomUUID().toString()); + headers.put("X-Request-UUID", UUID.randomUUID().toString()); + headers.put("traceparent", UUID.randomUUID().toString()); + headers.put("User-Agent", "Bitbucket-Webhooks/2.0"); + return headers; + } + + public static Map getNativeHeaders() { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json; charset=utf-8"); + headers.put("X-Request-Id", UUID.randomUUID().toString()); + headers.put("User-Agent", "Atlassian HttpClient 4.2.0 / Bitbucket-9.5.2 (9005002) / Default"); + return headers; + } + + public static Map getPluginHeaders() { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json; charset=utf-8"); + headers.put("X-Bitbucket-Type", "server"); + headers.put("User-Agent", "Bitbucket version: 8.18.0, Post webhook plugin version: 7.13.41-SNAPSHOT"); + return headers; + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/MockRequest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/MockRequest.java new file mode 100644 index 000000000..3e07424c4 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/test/util/MockRequest.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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.jenkins.plugins.bitbucket.test.util; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.apache.tools.ant.filters.StringInputStream; + +public class MockRequest extends ServletInputStream { + InputStream delegate; + + public MockRequest(String payload) { + this.delegate = new StringInputStream(payload); + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public boolean isReady() { + return !isFinished(); + } + + @Override + public boolean isFinished() { + try { + return delegate.available() == 0; + } catch (IOException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SCMNavigatorIntegrationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SCMNavigatorIntegrationTest.java index 910cf2481..453078018 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SCMNavigatorIntegrationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SCMNavigatorIntegrationTest.java @@ -55,7 +55,7 @@ class SCMNavigatorIntegrationTest { @Test void teamDiscoveringTest(JenkinsRule j) throws Exception { BitbucketEndpointConfiguration - .get().addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test", false, null)); + .get().addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test")); BitbucketMockApiFactory.add("http://bitbucket.test", BitbucketClientMockUtils.getAPIClientMock(true, false)); OrganizationFolder teamFolder = j.jenkins.createProject(OrganizationFolder.class, "test"); diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration.xml new file mode 100644 index 000000000..679be55b4 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration.xml @@ -0,0 +1,37 @@ + + + + + false + false + false + 360 + 180 + + + true + admin.basic.credentials + true + datacenter.hook.signature + http://host.docker.internal:8090/jenkins/ + server + http://localhost:7990/bitbucket + NATIVE + VERSION_7 + true + true + + + true + datacenter.certificate.credentials + false + http://host.docker.internal:8090/jenkins/ + server ngix + https://localhost:1443/bitbucket + PLUGIN + VERSION_7 + true + true + + + \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/server/pushPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/server/pushPayload.json deleted file mode 100644 index 8fb0aa90d..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/server/pushPayload.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "actor": { - "active": true, - "displayName": "Antonio Muniz", - "emailAddress": "amuniz@acme.com", - "id": 2, - "links": { - "self": [ - { - "href": "http://localhost:7990/users/amuniz" - } - ] - }, - "name": "amuniz", - "slug": "amuniz", - "type": "NORMAL" - }, - "push": { - "changes": [ - { - "created": false, - "closed": false, - "new": { - "type": "branch", - "name": "main", - "target": { - "type": "commit", - "hash": "9fdd7b96d3f5c276d0b9e0bf38c879eb112d889a" - } - }, - "old": { - "type": "branch", - "name": "main", - "target": { - "type": "commit", - "hash": "a8c13e8850dc60300be720019d1ffc1aa2fded87" - } - } - } - ] - }, - "date": "2024-11-27T01:34:45+0000", - "eventKey": "repo:push", - "repository": { - "scmId": "git", - "project": { - "key": "AMUNIZ", - "name": "prj" - }, - "slug": "test-repos", - "links": { - "self": [ - { - "href": "http://localhost:7990/projects/AMUNIZ/repos/test-repos/browse" - } - ] - }, - "public": false, - "owner": { - "username": "amuniz", - "displayName": "Antonio Muniz" - }, - "fullName": "amuniz/test-repos", - "ownerName": "amuniz" - } -} diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/annotated_tag_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/annotated_tag_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/annotated_tag_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/annotated_tag_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/commit_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/commit_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/commit_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/commit_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/pullrequest_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/pullrequest_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_rejected.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/pullrequest_rejected.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_rejected.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/pullrequest_rejected.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/signed_payload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/signed_payload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/signed_payload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/signed_payload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/tag_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/tag_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/tag_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/tag_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/annotated_tag_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/annotated_tag_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/annotated_tag_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/annotated_tag_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/emptyPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/emptyPayload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/emptyPayload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/emptyPayload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/mirrorSynchronized.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/mirrorSynchronized.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/mirrorSynchronized.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/mirrorSynchronized.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/mirrorSynchronized_refLimitExceeded.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/mirrorSynchronized_refLimitExceeded.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/mirrorSynchronized_refLimitExceeded.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/mirrorSynchronized_refLimitExceeded.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/ping_payload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/ping.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/ping_payload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/ping.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/prOpenFromTagPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/prOpenFromTagPayload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/prOpenFromTagPayload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/prOpenFromTagPayload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/pushPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/pushPayload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/pushPayload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/pushPayload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/signed_payload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/signed_payload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/signed_payload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/signed_payload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/tag_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/tag_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/tag_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/tag_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/tag_deleted.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/tag_deleted.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/native/tag_deleted.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/native/tag_deleted.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/branch_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/branch_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/branch_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/branch_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/branch_deleted.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/branch_deleted.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/branch_deleted.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/branch_deleted.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/commit_update.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/commit_update.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/commit_update.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/commit_update.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/commit_update2.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/commit_update2.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/commit_update2.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/commit_update2.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/server/emptyPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/emptyPayload.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/server/emptyPayload.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/emptyPayload.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_created.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_created.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_created.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_merged.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_merged.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_merged.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_merged.json diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_updated.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_updated.json similarity index 100% rename from src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/plugin/pullrequest_updated.json rename to src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/pullrequest_updated.json