From b89041bcd7aadda53a099f3493504a0fd6bc0332 Mon Sep 17 00:00:00 2001 From: Nikolas Falco Date: Mon, 25 Aug 2025 11:43:15 +0200 Subject: [PATCH] [JENKINS-76027] Allow Bitbucket build status to be customised Define extension point to allow customisation of build status. Additional data could be sei in the optionalData field where all entries will be serialised starting from the root object. --- .gitignore | 4 +- .../plugins/bitbucket/BitbucketNotifier.java | 69 --- .../plugins/bitbucket/api/BitbucketApi.java | 3 + .../api/BitbucketAuthenticatedClient.java | 102 ++++ .../bitbucket/api/BitbucketBuildStatus.java | 118 ++++- .../BitbucketBuildStatusCustomizer.java | 102 ++++ .../BitbucketBuildStatusNotifier.java} | 36 +- .../api/webhook/BitbucketWebhookManager.java | 59 +-- .../webhook/BitbucketWebhookProcessor.java | 2 + .../client/BitbucketCloudApiClient.java | 21 +- .../hooks/WebhookAutoRegisterListener.java | 20 +- .../buildstatus/CloudBuildStatusNotifier.java | 60 +++ .../ServerBuildStatusNotifier.java | 69 +++ .../impl/client/AbstractBitbucketApi.java | 54 +- .../BitbucketBuildStatusNotifications.java | 79 ++- .../webhook/cloud/CloudWebhookManager.java | 73 +-- .../webhook/plugin/PluginWebhookManager.java | 89 +--- .../webhook/server/ServerWebhookManager.java | 93 ++-- .../client/BitbucketServerAPIClient.java | 87 ++-- .../bitbucket/BitbucketSCMSourceTest.java | 68 --- ...ketBuildStatusNotificationsJUnit5Test.java | 335 ------------ ...ildStatusNotificationsJobListenerTest.java | 212 ++++++++ ...BitbucketBuildStatusNotificationsTest.java | 475 ++++++++++++------ .../bitbucket/impl/notifier/TestResults.java} | 59 ++- .../impl/webhook/DummyBitbucketWebhook.java | 67 --- .../impl/webhook/DummyWebhookManager.java | 104 ---- .../trait/BranchDiscoveryTraitTest.java | 8 +- .../ForkPullRequestDiscoveryTraitTest.java | 1 - .../OriginPullRequestDiscoveryTraitTest.java | 1 - .../bitbucket/trait/SSHCheckoutTraitTest.java | 97 ++-- ...bitbucketJenkinsRootUrl_emptyDefaulted.xml | 8 - .../bitbucketJenkinsRootUrl_goodAsIs.xml | 8 - .../bitbucketJenkinsRootUrl_normalized.xml | 8 - .../bitbucketJenkinsRootUrl_notslashed.xml | 8 - .../bitbucketJenkinsRootUrl_slashed.xml | 8 - 35 files changed, 1355 insertions(+), 1252 deletions(-) delete mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketNotifier.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketAuthenticatedClient.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusCustomizer.java rename src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/{webhook/BitbucketWebhookClient.java => buildstatus/BitbucketBuildStatusNotifier.java} (57%) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/CloudBuildStatusNotifier.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/ServerBuildStatusNotifier.java delete mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJobListenerTest.java rename src/{main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketDefaulNotifier.java => test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResults.java} (54%) delete mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyBitbucketWebhook.java delete mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyWebhookManager.java delete mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_emptyDefaulted.xml delete mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_goodAsIs.xml delete mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_normalized.xml delete mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_notslashed.xml delete mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_slashed.xml diff --git a/.gitignore b/.gitignore index be0861de7..c5ea8172d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ work/ # VSCode .factorypath -META-INF/ \ No newline at end of file +META-INF/ +/.apt_generated/ +/.apt_generated_tests/ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketNotifier.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketNotifier.java deleted file mode 100644 index 8909219b2..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketNotifier.java +++ /dev/null @@ -1,69 +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; - -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import java.io.IOException; - -/** - * Implementations must provides a concrete way to notify to Bitbucket commit. - */ -public interface BitbucketNotifier { - - /** - * Notify bitbucket about a new build status on a concrete commit. - * - * @param repoOwner repository owner name (username) - * @param repoName repository name - * @param hash commit hash - * @param content notification content - * @throws IOException if there was a communication error during notification. - * @throws InterruptedException if interrupted during notification. - */ - void notifyComment(@CheckForNull String repoOwner, @CheckForNull String repoName, String hash, String content) throws IOException, InterruptedException; - - /** - * Notify bitbucket through the build status API. - * - * @param status the status object to serialize - * @throws IOException if there was a communication error during notification. - * @throws InterruptedException if interrupted during notification. - */ - void notifyBuildStatus(BitbucketBuildStatus status) throws IOException, InterruptedException; - - /** - * Convenience method that calls {@link #notifyComment(String, String, String, String)} without owner - * and repository name. - * - * @param hash commit hash - * @param content notification content - * @throws IOException if there was a communication error during notification. - * @throws InterruptedException if interrupted during notification. - */ - default void notifyComment(String hash, String content) throws IOException, InterruptedException { - notifyComment(null, null, hash, content); - } - -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java index 70b1c2b5c..76afe79a6 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java @@ -23,6 +23,7 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusNotifier; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; @@ -297,8 +298,10 @@ List getRepositories(@CheckForNull UserRoleInRepo * Set the build status for the given commit hash. * * @param status the status object to be serialized + * @deprecated Use the appropriate {@link BitbucketBuildStatusNotifier} instead of this * @throws IOException if there was a network communications error. */ + @Deprecated(since = "937.0.0", forRemoval = true) void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException; /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketAuthenticatedClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketAuthenticatedClient.java new file mode 100644 index 000000000..e1fbe7653 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketAuthenticatedClient.java @@ -0,0 +1,102 @@ +/* + * 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; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; + +/** + * The implementation provides an authenticated client for a configured + * Bitbucket endpoint. + * + * @apiNote This interface is intended to be consumed in an extension point. + * + * @author Nikolas Falco + * @since 937.0.0 + */ +public interface BitbucketAuthenticatedClient extends AutoCloseable { + + /** + * The owner of the repository where register the webhook. + */ + @NonNull + String getRepositoryOwner(); + + /** + * Name of the repository where register the webhook. + */ + @CheckForNull + String getRepositoryName(); + + /** + * Perform an HTTP POST to the configured endpoint. + *

+ * Request will be sent as JSON + * + * @param path to call, it will prepend with the server URL + * @param payload to send + * @return the JSON string of the response + * @throws IOException in case of connection failures + */ + String post(@NonNull String path, @CheckForNull String payload) throws IOException; + + /** + * Perform an HTTP PUT to the configured endpoint. + *

+ * Request will be sent as JSON + * + * @param path to call, it will prepend with the server URL + * @param payload to send + * @return the JSON string of the response + * @throws IOException in case of connection failures + */ + String put(@NonNull String path, @CheckForNull String payload) throws IOException; + + /** + * Perform an HTTP DELETE to the configured endpoint. + *

+ * Request will be sent as JSON + * + * @param path to call, it will prepend with the server URL + * @return the JSON string of the response + * @throws IOException in case of connection failures + */ + String delete(@NonNull String path) throws IOException; + + /** + * Perform an HTTP GET to the configured endpoint. + *

+ * Request will be sent as JSON + * + * @param path to call, it will prepend with the server URL + * @return the JSON string of the response + * @throws IOException in case of connection failures + */ + @NonNull + String get(@NonNull String path) throws IOException; + + @Override + void close() throws IOException; +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java index 385f53706..2a48da352 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketBuildStatus.java @@ -23,14 +23,20 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusCustomizer; +import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; public class BitbucketBuildStatus { - /** * Enumeration of possible Bitbucket commit notification states */ @@ -43,20 +49,18 @@ public enum Status { CANCELLED("CANCELLED"), SUCCESSFUL("SUCCESSFUL"); - private final String status; - - Status(final String status) { - this.status = status; - } + @JsonValue + private final String label; - @Override - public String toString() { - return status; + Status(final String label) { + this.label = label; } } /** - * The commit hash to set the status on + * The commit hash to set the status on. + *

+ * This is not part of the payload. */ @JsonIgnore private String hash; @@ -107,9 +111,16 @@ public String toString() { */ private int buildNumber; + /** + * A set of new informations. + */ + private Map optionalData; + // Used for marshalling/unmarshalling @Restricted(DoNotUse.class) - public BitbucketBuildStatus() {} + public BitbucketBuildStatus() { + this.optionalData = new HashMap<>(); + } public BitbucketBuildStatus(String hash, String description, @@ -141,7 +152,9 @@ public BitbucketBuildStatus(@NonNull BitbucketBuildStatus other) { this.name = other.name; this.refname = other.refname; this.buildDuration = other.buildDuration; + this.buildNumber = other.buildNumber; this.parent = other.parent; + this.optionalData = other.optionalData != null ? new HashMap<>(other.optionalData) : new HashMap<>(); } public String getHash() { @@ -160,8 +173,8 @@ public void setDescription(String description) { this.description = description; } - public String getState() { - return state.toString(); + public Status getState() { + return state; } public void setState(Status state) { @@ -223,4 +236,83 @@ public void setParent(String parent) { public String getParent() { return parent; } + + /** + * This represent additional informations contributed by + * {@link BitbucketBuildStatusCustomizer}s. + *

+ * The contents of this map will be added to the root of the sent payload. + *

+ * For example: + * + *

+     * buildStatus.addOptionalData("testResults", new TestResult(1, 2, 3));
+     * buildStatus.addOptionalData("optX", true);
+     * 
+ * + * Will be serialised as: + * + *
+     * {
+     *     "description": "The build is in progress..."
+     *     ...
+     *     "testResult": {
+     *         "successful": 5,
+     *         "failed": 2,
+     *         "skipped": 1
+     *     },
+     *     "optX": true
+     * }
+     * 
+ * + * @return an unmodifiable map of extra informations + */ + @JsonAnyGetter + public Map getOptionalData() { + return Collections.unmodifiableMap(optionalData); + } + + /** + * Add a new attribute to the payload to send. If the attribute has already + * been valued than it is ignored. + * + * @param attribute attribute of build status, refer to the Bitbucket API + * @param value bean to associate to the given attribute name + * @see Cloud REST API + * @see Data Center REST API + */ + public void addOptionalData(String attribute, Object value) { + this.optionalData.putIfAbsent(attribute, value); + } + + @Override + public int hashCode() { + return Objects.hash(buildDuration, buildNumber, description, hash, key, name, parent, refname, state, url, optionalData); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BitbucketBuildStatus other = (BitbucketBuildStatus) obj; + return buildDuration == other.buildDuration + && buildNumber == other.buildNumber + && Objects.equals(description, other.description) + && Objects.equals(hash, other.hash) + && Objects.equals(key, other.key) + && Objects.equals(name, other.name) + && Objects.equals(parent, other.parent) + && Objects.equals(refname, other.refname) + && state == other.state + && Objects.equals(url, other.url) + && Objects.equals(optionalData, other.optionalData); + } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusCustomizer.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusCustomizer.java new file mode 100644 index 000000000..30e6b324e --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusCustomizer.java @@ -0,0 +1,102 @@ +/* + * 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.api.buildstatus; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionPoint; +import hudson.model.Run; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMTrait; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +public interface BitbucketBuildStatusCustomizer extends ExtensionPoint { + + /** + * Returns if this implementation supports the given endpoint type. + * + * @param type of the endpoint + * @return {@code true} if this implementation can manage API for this + * endpoint, {@code false} otherwise. + */ + boolean isApplicable(@NonNull EndpointType type); + + /** + * Trait instance associate to a {@link SCMSource} where gather extra + * configuration options. + *

+ * Each {@link BitbucketBuildStatusCustomizer} that would obtain additional + * configuration options per project must provide an own specific trait + * implementation. + * + * @param trait to apply + */ + default void apply(SCMSourceTrait trait) {} + + /** + * A list of traits class that this manager supports to obtain additional + * configuration options. + * + * @return a list of {@link SCMSourceTrait} classes. + */ + default Collection> supportedTraits() { + return Collections.emptyList(); + } + + /** + * Apply some customisations to a given build status. + *

+ * Any additional information must be supplied in the + * {@link BitbucketBuildStatus#getOptionalData()}. For tracing reason any + * changes if applied by the customizer it will be logged in the console. + * + * @param buildStatus to customise + * @param build current {@link Run} job. + */ + @Restricted(Beta.class) + @CheckForNull + void customize(Run build, @NonNull BitbucketBuildStatus buildStatus); + + /** + * Convenient method to apply only supported traits to this customiser. + * + * @param traits to apply if supported too + */ + default void withTraits(List traits) { + supportedTraits().forEach(traitClass -> { + SCMSourceTrait trait = SCMTrait.find(traits, traitClass); + if (trait != null) { + apply(trait); + } + }); + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusNotifier.java similarity index 57% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookClient.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusNotifier.java index 09af7c675..62bdabd64 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/buildstatus/BitbucketBuildStatusNotifier.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2025, Falco Nikolas + * 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 @@ -21,29 +21,25 @@ * 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; +package com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus; -import edu.umd.cs.findbugs.annotations.CheckForNull; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionPoint; import java.io.IOException; -/** - * The implementation provides an authenticated client to the configured - * Bitbucket endpoint. - * - * @author Nikolas Falco - */ -public interface BitbucketWebhookClient extends AutoCloseable { - - String post(@NonNull String path, @CheckForNull String payload) throws IOException; - - String put(@NonNull String path, @CheckForNull String payload) throws IOException; - - String delete(@NonNull String path) throws IOException; +public interface BitbucketBuildStatusNotifier extends ExtensionPoint { - @NonNull - String get(@NonNull String path) throws IOException; + /** + * Returns if this implementation supports the given endpoint type. + * + * @param type of the endpoint + * @return {@code true} if this implementation can manage API for this + * endpoint, {@code false} otherwise. + */ + boolean isApplicable(@NonNull EndpointType type); - @Override - void close() throws IOException; + void sendBuildStatus(@NonNull BitbucketBuildStatus status, @NonNull BitbucketAuthenticatedClient client) throws IOException; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookManager.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookManager.java index d1b859e00..b2195d692 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookManager.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/webhook/BitbucketWebhookManager.java @@ -23,13 +23,18 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api.webhook; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ExtensionPoint; import java.io.IOException; import java.util.Collection; +import java.util.Collections; +import java.util.List; import jenkins.scm.api.SCMSource; import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMTrait; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; @@ -39,31 +44,11 @@ * webhook commit * * @author Nikolas Falco + * @since 937.0.0 */ @Restricted(Beta.class) public interface BitbucketWebhookManager extends ExtensionPoint { - /** - * The owner of the repository where register the webhook. - * - * @param repositoryOwner name - */ - void setRepositoryOwner(@NonNull String repositoryOwner); - - /** - * Name of the repository where register the webhook. - * - * @param repositoryName - */ - void setRepositoryName(@NonNull String repositoryName); - - /** - * The base URL of endpoint of the Bitbucket host. - * - * @param serverURL the base of the endpoint to call. - */ - void setServerURL(@NonNull String serverURL); - /** * The callback URL where send event payload. *

@@ -73,8 +58,10 @@ public interface BitbucketWebhookManager extends ExtensionPoint { * endpoint to process own events. * * @param callbackURL used to send webhook payload. + * @param endpoint this webhook is registered for, it could be used to + * retrieve additional information to compose the callbackURL */ - void setCallbackURL(@NonNull String callbackURL); + void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint); /** * The configuration that returned this implementation class. @@ -89,19 +76,35 @@ public interface BitbucketWebhookManager extends ExtensionPoint { * * @return a list of {@link SCMSourceTrait} classes. */ - Collection> supportedTraits(); + default Collection> supportedTraits() { + return Collections.emptyList(); + } /** * Trait instance associate to a {@link SCMSource} where gather extra * configuration options. *

* Each {@link BitbucketWebhookConfiguration} that would obtain additional - * configuration options per project mst provide an own specific trait + * configuration options per project must provide an own specific trait * implementation. * * @param trait to apply */ - void apply(SCMSourceTrait trait); + default void apply(SCMSourceTrait trait) {} + + /** + * Convenient method to apply only supported traits to this customiser. + * + * @param traits to apply if supported too + */ + default void withTraits(List traits) { + supportedTraits().forEach(traitClass -> { + SCMSourceTrait trait = SCMTrait.find(traits, traitClass); + if (trait != null) { + apply(trait); + } + }); + } /** * Returns the list of all registered webhook at this repository related to @@ -111,7 +114,7 @@ public interface BitbucketWebhookManager extends ExtensionPoint { * @return a list of registered {@link BitbucketWebHook}. * @throws IOException in case of communication issue with Bitbucket */ - Collection read(@NonNull BitbucketWebhookClient client) throws IOException; + Collection read(@NonNull BitbucketAuthenticatedClient client) throws IOException; /** * Save a webhook (updating or creating a new one) using the actual @@ -120,7 +123,7 @@ public interface BitbucketWebhookManager extends ExtensionPoint { * @param client authenticated to communicate with Bitbucket * @throws IOException in case of communication issue with Bitbucket */ - void register(@NonNull BitbucketWebhookClient client) throws IOException; + void register(@NonNull BitbucketAuthenticatedClient client) throws IOException; /** * Remove the webhook from the Bitbucket repository with the given @@ -130,5 +133,5 @@ public interface BitbucketWebhookManager extends ExtensionPoint { * @param client authenticated to communicate with Bitbucket * @throws IOException in case of communication issue with Bitbucket */ - void remove(@NonNull String webhookId, @NonNull BitbucketWebhookClient client) throws IOException; + void remove(@NonNull String webhookId, @NonNull BitbucketAuthenticatedClient client) throws IOException; } 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 index d57ee187c..1698adcc6 100644 --- 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 @@ -46,6 +46,8 @@ * 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 in case * of multiple matches. + * + * @since 937.0.0 */ @Restricted(Beta.class) public interface BitbucketWebhookProcessor extends ExtensionPoint { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index ff152e506..ab80b71c8 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -24,6 +24,7 @@ package com.cloudbees.jenkins.plugins.bitbucket.client; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCloudWorkspace; @@ -43,6 +44,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositorySource; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; +import com.cloudbees.jenkins.plugins.bitbucket.impl.buildstatus.CloudBuildStatusNotifier; import com.cloudbees.jenkins.plugins.bitbucket.impl.client.AbstractBitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.impl.client.ICheckedCallable; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketAccessTokenAuthenticator; @@ -86,7 +88,6 @@ import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MINUTES; -import static org.apache.commons.lang3.StringUtils.abbreviate; public class BitbucketCloudApiClient extends AbstractBitbucketApi implements BitbucketApi { @@ -523,17 +524,11 @@ public List getWebHooks() throws IOException { /** * {@inheritDoc} */ + @Deprecated @Override public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException { - BitbucketBuildStatus newStatus = new BitbucketBuildStatus(status); - newStatus.setName(abbreviate(newStatus.getName(), 255)); - - String url = UriTemplate.fromTemplate(REPO_URL_TEMPLATE + "/commit/{hash}/statuses/build") - .set("owner", owner) - .set("repo", repositoryName) - .set("hash", newStatus.getHash()) - .expand(); - postRequest(url, JsonParser.toString(newStatus)); + CloudBuildStatusNotifier notifier = new CloudBuildStatusNotifier(); + notifier.sendBuildStatus(status, adapt(BitbucketAuthenticatedClient.class)); } /** @@ -682,6 +677,12 @@ protected HttpHost getHost() { return API_HOST; } + @NonNull + @Override + protected String getBaseURL() { + return "https://api.bitbucket.org"; + } + @NonNull @Override protected CloseableHttpClient getClient() { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java index 287da352c..3fe532eb6 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java @@ -28,11 +28,11 @@ import com.cloudbees.jenkins.plugins.bitbucket.WebhookRegistration; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; @@ -61,8 +61,6 @@ import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; -import jenkins.scm.api.trait.SCMSourceTrait; -import jenkins.scm.api.trait.SCMTrait; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; @@ -178,7 +176,7 @@ private synchronized void registerHooks(SCMSourceOwner owner) throws IOException } BitbucketWebhookManager manager = buildWebhookManager(source, endpoint); - try (BitbucketWebhookClient webhookClient = client.adapt(BitbucketWebhookClient.class)) { + try (BitbucketAuthenticatedClient webhookClient = client.adapt(BitbucketAuthenticatedClient.class)) { manager.register(webhookClient); } } @@ -189,22 +187,14 @@ private BitbucketWebhookManager buildWebhookManager(BitbucketSCMSource source, B BitbucketWebhookManager manager = ExtensionList.lookupFirst(webhookConfig.getManager()); // setup manager with base required information manager.apply(webhookConfig); - manager.setServerURL(endpoint.getServerURL()); - manager.setRepositoryOwner(source.getRepoOwner()); - manager.setRepositoryName(source.getRepository()); String callbackRootURL = getCallbackRootURL(webhookConfig); // this is the base callback URL that webhook usually should call to be processed String callbackURL = callbackRootURL + BitbucketSCMSourcePushHookReceiver.FULL_PATH; - manager.setCallbackURL(callbackURL); + manager.setCallbackURL(callbackURL, endpoint); // setup traits extra informations - for (Class traitClazz : manager.supportedTraits()) { - SCMSourceTrait trait = SCMTrait.find(source.getTraits(), traitClazz); - if (trait != null) { - manager.apply(trait); - } - } + manager.withTraits(source.getTraits()); return manager; } @@ -226,7 +216,7 @@ private void removeHooks(SCMSourceOwner owner) throws IOException { } BitbucketApi client = getClientBySource(source, endpoint); if (client != null) { - try (BitbucketWebhookClient webhookClient = client.adapt(BitbucketWebhookClient.class)) { + try (BitbucketAuthenticatedClient webhookClient = client.adapt(BitbucketAuthenticatedClient.class)) { if (webhookClient == null) { continue; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/CloudBuildStatusNotifier.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/CloudBuildStatusNotifier.java new file mode 100644 index 000000000..61d8025ac --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/CloudBuildStatusNotifier.java @@ -0,0 +1,60 @@ +/* + * 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.buildstatus; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusNotifier; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; +import com.damnhandy.uri.template.UriTemplate; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import java.io.IOException; + +import static org.apache.commons.lang3.StringUtils.abbreviate; + +@Extension +public class CloudBuildStatusNotifier implements BitbucketBuildStatusNotifier { + private static final String COMMIT_BUILD_STATUS_URL = "/2.0/repositories{/owner,repo}/commit/{hash}/statuses/build"; + + @Override + public void sendBuildStatus(@NonNull BitbucketBuildStatus status, @NonNull BitbucketAuthenticatedClient client) throws IOException { + BitbucketBuildStatus newStatus = new BitbucketBuildStatus(status); + newStatus.setName(abbreviate(newStatus.getName(), 255)); + + String url = UriTemplate.fromTemplate(COMMIT_BUILD_STATUS_URL) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) + .set("hash", newStatus.getHash()) + .expand(); + client.post(url, JsonParser.toString(newStatus)); + } + + @Override + public boolean isApplicable(@NonNull EndpointType type) { + return type == EndpointType.CLOUD; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/ServerBuildStatusNotifier.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/ServerBuildStatusNotifier.java new file mode 100644 index 000000000..0be54d13b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/buildstatus/ServerBuildStatusNotifier.java @@ -0,0 +1,69 @@ +/* + * 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.buildstatus; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusNotifier; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBuildStatus; +import com.damnhandy.uri.template.UriTemplate; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import java.io.IOException; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; + +import static org.apache.commons.lang3.StringUtils.abbreviate; +import static org.apache.commons.lang3.StringUtils.substring; + +@Extension +public class ServerBuildStatusNotifier implements BitbucketBuildStatusNotifier { + private static final String API_COMMIT_STATUS_PATH = "/rest/api/1.0/projects/{owner}/repos/{repo}/commits/{hash}/builds"; + + @Override + public void sendBuildStatus(@NonNull BitbucketBuildStatus status, @NonNull BitbucketAuthenticatedClient client) throws IOException { + BitbucketServerBuildStatus newStatus = new BitbucketServerBuildStatus(status); + newStatus.setName(abbreviate(newStatus.getName(), 255)); + + String key = status.getKey(); + if (StringUtils.length(key) > 255) { + newStatus.setKey(substring(key, 0, 255 - 33) + '/' + DigestUtils.md5Hex(key)); + } + + String url = UriTemplate.fromTemplate(API_COMMIT_STATUS_PATH) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) + .set("hash", newStatus.getHash()) + .expand(); + client.post(url, JsonParser.toString(newStatus)); + } + + @Override + public boolean isApplicable(@NonNull EndpointType type) { + return type == EndpointType.SERVER; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java index 4fecceb1c..c7cdc4bf0 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java @@ -24,19 +24,18 @@ package com.cloudbees.jenkins.plugins.bitbucket.impl.client; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; import com.cloudbees.jenkins.plugins.bitbucket.client.ClosingConnectionInputStream; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionList; import hudson.ProxyConfiguration; import hudson.util.Secret; @@ -242,6 +241,9 @@ protected void setClientProxyParams(HttpClientBuilder builder) { @NonNull protected abstract HttpHost getHost(); + @NonNull + protected abstract String getBaseURL(); + @NonNull protected abstract CloseableHttpClient getClient(); @@ -359,7 +361,7 @@ protected BitbucketAuthenticator getAuthenticator() { public List getWebHooks() throws IOException { logger.warning("getWebHooks is deprecated, do not use this API method anymore, webhook are now handled by the BitbucketWebhookManager."); BitbucketWebhookManager manager = buildManager(); - BitbucketWebhookClient webhookClient = adapt(BitbucketWebhookClient.class); + BitbucketAuthenticatedClient webhookClient = adapt(BitbucketAuthenticatedClient.class); return new ArrayList<>(manager.read(webhookClient)); } @@ -373,7 +375,7 @@ public void registerCommitWebHook(BitbucketWebHook hook) throws IOException { public void updateCommitWebHook(BitbucketWebHook hook) throws IOException { logger.warning("updateCommitWebHook is deprecated, do not use this API method anymore, webhook are now handled by the BitbucketWebhookManager."); BitbucketWebhookManager manager = buildManager(); - BitbucketWebhookClient webhookClient = adapt(BitbucketWebhookClient.class); + BitbucketAuthenticatedClient webhookClient = adapt(BitbucketAuthenticatedClient.class); manager.register(webhookClient); } @@ -381,55 +383,71 @@ public void updateCommitWebHook(BitbucketWebHook hook) throws IOException { public void removeCommitWebHook(BitbucketWebHook hook) throws IOException { logger.warning("removeCommitWebHook is deprecated, do not use this API method anymore, webhook are now handled by the BitbucketWebhookManager."); BitbucketWebhookManager integmanagerration = buildManager(); - BitbucketWebhookClient webhookClient = adapt(BitbucketWebhookClient.class); + BitbucketAuthenticatedClient webhookClient = adapt(BitbucketAuthenticatedClient.class); integmanagerration.remove(hook.getUuid(), webhookClient); } - @Deprecated - @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") @NonNull private BitbucketWebhookManager buildManager() { - String serverURL = getHost().toString(); // not 100% true in case of bitbucket data center...but this method must not be used so does not matter - BitbucketEndpoint endpoint = BitbucketEndpointProvider.lookupEndpoint(serverURL) + BitbucketEndpoint endpoint = BitbucketEndpointProvider.lookupEndpoint(getBaseURL()) .orElseThrow(); BitbucketWebhookConfiguration configuration = endpoint.getWebhook(); BitbucketWebhookManager manager = ExtensionList.lookupFirst(configuration.getManager()); manager.apply(configuration); - manager.setRepositoryOwner(getOwner()); - manager.setRepositoryName(getRepositoryName()); return manager; } @Override @SuppressWarnings("unchecked") public T adapt(Class clazz) { - if (clazz == BitbucketWebhookClient.class) { - return (T) new BitbucketWebhookClient() { + if (clazz == BitbucketAuthenticatedClient.class) { + return (T) new BitbucketAuthenticatedClient() { + + private AbstractBitbucketApi delegate = AbstractBitbucketApi.this; @Override public String post(@NonNull String path, @CheckForNull String payload) throws IOException { - return postRequest(path, payload); + return delegate.postRequest(completeURL(path), payload); } @Override public String put(@NonNull String path, @CheckForNull String payload) throws IOException { - return putRequest(path, payload); + return delegate.putRequest(completeURL(path), payload); } @Override public String delete(@NonNull String path) throws IOException { - return deleteRequest(path); + return delegate.deleteRequest(completeURL(path)); } @Override public String get(@NonNull String path) throws IOException { - return getRequest(path); + return delegate.getRequest(completeURL(path)); } @Override public void close() throws IOException { - AbstractBitbucketApi.this.close(); + //delegate.close(); + } + + @Override + public String getRepositoryOwner() { + return delegate.getOwner(); } + + @Override + public String getRepositoryName() { + return delegate.getRepositoryName(); + } + + private String completeURL(@NonNull String path) { + if (path.startsWith("/")) { + return delegate.getBaseURL() + "/" + path; + } else { + return delegate.getBaseURL() + path; + } + } + }; } else { return null; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java index 9b7c27782..2d57d0747 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotifications.java @@ -31,13 +31,21 @@ import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusCustomizer; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusNotifier; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.trait.BranchDiscoveryTrait.ExcludeOriginPRBranchesSCMHeadFilter; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import hudson.ExtensionList; import hudson.FilePath; import hudson.model.Result; import hudson.model.Run; @@ -50,6 +58,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; +import java.util.logging.Logger; import jenkins.model.JenkinsLocationConfiguration; import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMHead; @@ -57,6 +67,7 @@ import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; +import jenkins.scm.api.trait.SCMSourceTrait; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; @@ -67,6 +78,7 @@ * Only builds derived from a job that was created as part of a multi-branch project will be processed by this listener. */ public final class BitbucketBuildStatusNotifications { + private static final Logger logger = Logger.getLogger(BitbucketBuildStatusNotifications.class.getName()); private static String getRootURL(@NonNull Run build) { JenkinsLocationConfiguration cfg = JenkinsLocationConfiguration.get(); @@ -82,10 +94,10 @@ private static String getRootURL(@NonNull Run build) { * Check if the build URL is compatible with Bitbucket API. * For example, Bitbucket Cloud API requires fully qualified or IP * Where we actively do not allow localhost - * Throws an IllegalStateException if it is not valid, or return the url otherwise * * @param url the URL of the build to check * @param client the bitbucket client we are facing. + * @throws IllegalStateException if it is not valid, or return the url otherwise */ static String checkURL(@NonNull String url, BitbucketApi client) { try { @@ -111,10 +123,10 @@ private static void createStatus(@NonNull Run build, @NonNull BitbucketApi client, @NonNull String key, @NonNull String hash, - @Nullable String refName) throws IOException, InterruptedException { + @Nullable String refName) throws IOException { - final SCMSource source = SCMSource.SourceByItem.findSource(build.getParent()); - if (!(source instanceof BitbucketSCMSource)) { + final BitbucketSCMSource source = findBitbucketSCMSource(build); + if (source == null) { return; } @@ -132,8 +144,9 @@ private static void createStatus(@NonNull Run build, } boolean isCloud = BitbucketApiUtils.isCloud(client); + List traits = source.getTraits(); BitbucketSCMSourceContext context = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) - .withTraits(source.getTraits()); // NOSONAR + .withTraits(traits); // NOSONAR final Result result = build.getResult(); final String name = build.getFullDisplayName(); // use the build number as the display name of the status String buildDescription = build.getDescription(); @@ -174,7 +187,6 @@ private static void createStatus(@NonNull Run build, } if (state != null) { - BitbucketDefaulNotifier notifier = new BitbucketDefaulNotifier(client); String notificationKey = DigestUtils.md5Hex(key); String notificationParentKey = null; if (context.useReadableNotificationIds() && !isCloud) { @@ -185,8 +197,8 @@ private static void createStatus(@NonNull Run build, buildStatus.setBuildDuration(build.getDuration()); buildStatus.setBuildNumber(build.getNumber()); buildStatus.setParent(notificationParentKey); - // TODO testResults should be provided by an extension point that integrates JUnit or anything else plugin - notifier.notifyBuildStatus(buildStatus); + + sendNotification(source, build, buildStatus, client); if (result != null) { listener.getLogger().println("[Bitbucket] Build result notified"); } @@ -195,15 +207,50 @@ private static void createStatus(@NonNull Run build, } } + private static void sendNotification(@NonNull BitbucketSCMSource source, + @NonNull Run build, + @NonNull BitbucketBuildStatus buildStatus, + @NonNull BitbucketApi client) throws IOException { + EndpointType endpointType = BitbucketEndpointProvider.lookupEndpoint(source.getServerUrl()) + .map(BitbucketEndpoint::getType) + .orElseThrow(() -> new BitbucketException("No configured endpoint found for server URL " + source.getServerUrl())); + + BitbucketBuildStatus newBuildStatus = new BitbucketBuildStatus(buildStatus); + + List customizers = ExtensionList.lookup(BitbucketBuildStatusCustomizer.class) + .stream() + .filter(n -> n.isApplicable(endpointType)) + .toList(); + for (BitbucketBuildStatusCustomizer customizer : customizers) { + customizer.withTraits(source.getTraits()); + customizer.customize(build, newBuildStatus); + if (!buildStatus.equals(newBuildStatus)) { + logger.info("Build status enriched by " + customizer.getClass().getName()); + } + } + // restore unmodifiable fields to respect traits options or avoid strange behaviours on builds + newBuildStatus.setState(buildStatus.getState()); + newBuildStatus.setKey(buildStatus.getKey()); + newBuildStatus.setRefname(buildStatus.getRefname()); + newBuildStatus.setParent(buildStatus.getParent()); + newBuildStatus.setUrl(buildStatus.getUrl()); + + BitbucketBuildStatusNotifier notifier = ExtensionList.lookup(BitbucketBuildStatusNotifier.class) + .stream() + .filter(n -> n.isApplicable(endpointType)) + .findFirst() + .orElseThrow(() -> new BitbucketException("No notifier found that supports endpoint of type " + endpointType)); + notifier.sendBuildStatus(newBuildStatus, client.adapt(BitbucketAuthenticatedClient.class)); + } + private static @CheckForNull BitbucketSCMSource findBitbucketSCMSource(Run build) { SCMSource s = SCMSource.SourceByItem.findSource(build.getParent()); return s instanceof BitbucketSCMSource scm ? scm : null; } - private static void sendNotifications(BitbucketSCMSource source, Run build, TaskListener listener) - throws IOException, InterruptedException { - BitbucketSCMSourceContext sourceContext = new BitbucketSCMSourceContext(null, - SCMHeadObserver.none()).withTraits(source.getTraits()); + private static void sendNotifications(BitbucketSCMSource source, Run build, TaskListener listener) throws IOException { + BitbucketSCMSourceContext sourceContext = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(source.getTraits()); if (sourceContext.notificationsDisabled()) { listener.getLogger().println("[Bitbucket] Notification is disabled by configuration"); return; @@ -267,10 +314,8 @@ private static void sendNotifications(BitbucketSCMSource source, Run build } } } - try { + try (client) { createStatus(build, listener, client, key, hash, refName); - } finally { - client.close(); } } @@ -333,7 +378,7 @@ public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListene try { sendNotifications(source, build, listener); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { e.printStackTrace(listener.error("Could not send notifications")); } } @@ -355,7 +400,7 @@ public void onCompleted(Run build, TaskListener listener) { try { sendNotifications(source, build, listener); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { e.printStackTrace(listener.error("Could not send notifications")); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhookManager.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhookManager.java index 24c3cbd08..13d0158e9 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhookManager.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhookManager.java @@ -23,13 +23,15 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudPage; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudWebhook; import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentialsUtils; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; import com.damnhandy.uri.template.UriTemplate; @@ -57,7 +59,7 @@ @Extension public class CloudWebhookManager implements BitbucketWebhookManager { - private static final String WEBHOOK_URL = "https://api.bitbucket.org/2.0/repositories{/owner,repo}/hooks{/hook}{?page,pagelen}"; + private static final String WEBHOOK_URL = "/2.0/repositories{/owner,repo}/hooks{/hook}{?page,pagelen}"; private static final Logger logger = Logger.getLogger(CloudWebhookManager.class.getName()); // The list of events available in Bitbucket Cloud. @@ -70,9 +72,6 @@ public class CloudWebhookManager implements BitbucketWebhookManager { )); private CloudWebhookConfiguration configuration; - private String serverURL; - private String repositoryOwner; - private String repositoryName; private String callbackURL; @Override @@ -90,49 +89,19 @@ public void apply(BitbucketWebhookConfiguration configuration) { this.configuration = (CloudWebhookConfiguration) configuration; } - @NonNull - public String getRepositoryOwner() { - return repositoryOwner; - } - - @Override - public void setRepositoryOwner(@NonNull String repositoryOwner) { - this.repositoryOwner = repositoryOwner; - } - - @NonNull - public String getRepositoryName() { - return repositoryName; - } - - @Override - public void setRepositoryName(@NonNull String repositoryName) { - this.repositoryName = repositoryName; - } - - @NonNull - public String getServerURL() { - return serverURL; - } - - @Override - public void setServerURL(@NonNull String serverURL) { - this.serverURL = serverURL; - } - @Override - public void setCallbackURL(@NonNull String callbackURL) { + public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint) { this.callbackURL = callbackURL; } @Override @NonNull - public Collection read(@NonNull BitbucketWebhookClient client) throws IOException { + public Collection read(@NonNull BitbucketAuthenticatedClient client) throws IOException { String endpointJenkinsRootURL = ObjectUtils.firstNonNull(configuration.getEndpointJenkinsRootURL(), BitbucketWebhookConfiguration.getDefaultJenkinsRootURL()); String url = UriTemplate.fromTemplate(WEBHOOK_URL) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("pagelen", 100) .expand(); @@ -162,7 +131,7 @@ private BitbucketCloudWebhook buildPayload() { hook.setUrl(callbackURL); if (configuration.isEnableHookSignature()) { String signatureCredentialsId = configuration.getHookSignatureCredentialsId(); - StringCredentials signatureSecret = BitbucketCredentialsUtils.lookupCredentials(Jenkins.get(), serverURL, signatureCredentialsId, StringCredentials.class); + StringCredentials signatureSecret = BitbucketCredentialsUtils.lookupCredentials(Jenkins.get(), BitbucketCloudEndpoint.SERVER_URL, signatureCredentialsId, StringCredentials.class); if (signatureSecret != null) { hook.setSecret(Secret.toString(signatureSecret.getSecret())); } else { @@ -172,10 +141,10 @@ private BitbucketCloudWebhook buildPayload() { return hook; } - private void register(@NonNull BitbucketCloudWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { + private void register(@NonNull BitbucketCloudWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { String url = UriTemplate.fromTemplate(WEBHOOK_URL) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .expand(); client.post(url, JsonParser.toString(payload)); } @@ -212,37 +181,37 @@ private boolean shouldUpdate(@NonNull BitbucketCloudWebhook current, @NonNull Bi return update; } - private void update(@NonNull BitbucketCloudWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { + private void update(@NonNull BitbucketCloudWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { String url = UriTemplate.fromTemplate(WEBHOOK_URL) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("hook", payload.getUuid()) .expand(); client.put(url, JsonParser.toString(payload)); } @Override - public void remove(@NonNull String webhookId, @NonNull BitbucketWebhookClient client) throws IOException { + public void remove(@NonNull String webhookId, @NonNull BitbucketAuthenticatedClient client) throws IOException { String url = UriTemplate.fromTemplate(WEBHOOK_URL) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("hook", webhookId) .expand(); client.delete(url); } @Override - public void register(@NonNull BitbucketWebhookClient client) throws IOException { + public void register(@NonNull BitbucketAuthenticatedClient client) throws IOException { BitbucketCloudWebhook existingHook = (BitbucketCloudWebhook) read(client) .stream() .findFirst() .orElse(null); if (existingHook == null) { - logger.log(Level.INFO, "Registering cloud hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Registering cloud hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); register(buildPayload(), client); } else if (shouldUpdate(existingHook, buildPayload())) { - logger.log(Level.INFO, "Updating cloud hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Updating cloud hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); update(existingHook, client); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhookManager.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhookManager.java index 843490ec3..c0b8055e6 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhookManager.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginWebhookManager.java @@ -23,8 +23,9 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.plugin; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; @@ -74,9 +75,6 @@ public class PluginWebhookManager implements BitbucketWebhookManager { "TAG_CREATED")); private PluginWebhookConfiguration configuration; - private String serverURL; - private String repositoryOwner; - private String repositoryName; private String callbackURL; private String committersToIgnore; @@ -97,58 +95,23 @@ public void apply(BitbucketWebhookConfiguration configuration) { this.configuration = (PluginWebhookConfiguration) configuration; } - @NonNull - public String getRepositoryOwner() { - return repositoryOwner; - } - - @Override - public void setRepositoryOwner(@NonNull String repositoryOwner) { - this.repositoryOwner = repositoryOwner; - } - - @NonNull - public String getRepositoryName() { - return repositoryName; - } - - @Override - public void setRepositoryName(@NonNull String repositoryName) { - this.repositoryName = repositoryName; - } - - @NonNull - public String getServerURL() { - return serverURL; - } - @Override - public void setServerURL(@NonNull String serverURL) { - this.serverURL = serverURL; - } - - @Override - public void setCallbackURL(@NonNull String callbackURL) { - this.callbackURL = callbackURL; - } - - @NonNull - private String getCallbackURL() { - return UriTemplate.buildFromTemplate(callbackURL) + public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint) { + this.callbackURL = UriTemplate.buildFromTemplate(callbackURL) .query("server_url") .build() - .set("server_url", serverURL) - .expand(); + .set("server_url", endpoint.getServerURL()) + .expand();; } @Override @NonNull - public Collection read(@NonNull BitbucketWebhookClient client) throws IOException { + public Collection read(@NonNull BitbucketAuthenticatedClient client) throws IOException { String endpointJenkinsRootURL = ObjectUtils.firstNonNull(configuration.getEndpointJenkinsRootURL(), BitbucketWebhookConfiguration.getDefaultJenkinsRootURL()); - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .expand(); BitbucketPluginWebhook[] hooks = JsonParser.toJava(client.get(url), BitbucketPluginWebhook[].class); @@ -163,16 +126,16 @@ private BitbucketPluginWebhook buildPayload() { BitbucketPluginWebhook hook = new BitbucketPluginWebhook(); hook.setActive(true); hook.setDescription("Jenkins hook"); - hook.setUrl(getCallbackURL()); + hook.setUrl(callbackURL); hook.setEvents(PLUGIN_SERVER_EVENTS); hook.setCommittersToIgnore(committersToIgnore); return hook; } - private void register(@NonNull BitbucketPluginWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + private void register(@NonNull BitbucketPluginWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .expand(); client.post(url, JsonParser.toString(payload)); } @@ -208,38 +171,38 @@ private boolean shouldUpdate(@NonNull BitbucketPluginWebhook current, @NonNull B return update; } - private void update(@NonNull BitbucketPluginWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { + private void update(@NonNull BitbucketPluginWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { String url = UriTemplate - .fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("id", payload.getUuid()) .expand(); client.put(url, JsonParser.toString(payload)); } @Override - public void remove(@NonNull String webhookId, @NonNull BitbucketWebhookClient client) throws IOException { - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + public void remove(@NonNull String webhookId, @NonNull BitbucketAuthenticatedClient client) throws IOException { + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("id", webhookId) .expand(); client.delete(url); } @Override - public void register(@NonNull BitbucketWebhookClient client) throws IOException { + public void register(@NonNull BitbucketAuthenticatedClient client) throws IOException { BitbucketPluginWebhook existingHook = (BitbucketPluginWebhook) read(client) .stream() .findFirst() .orElse(null); if (existingHook == null) { - logger.log(Level.INFO, "Registering cloud hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Registering cloud hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); register(buildPayload(), client); } else if (shouldUpdate(existingHook, buildPayload())) { - logger.log(Level.INFO, "Updating cloud hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Updating cloud hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); update(existingHook, client); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhookManager.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhookManager.java index bbdf2ee39..8d0bec634 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhookManager.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhookManager.java @@ -23,8 +23,9 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; @@ -74,8 +75,6 @@ public class ServerWebhookManager implements BitbucketWebhookManager { private ServerWebhookConfiguration configuration; private String serverURL; - private String repositoryOwner; - private String repositoryName; private String callbackURL; @Override @@ -88,39 +87,14 @@ public void apply(SCMSourceTrait configurationTrait) { // nothing to configure } - @NonNull - public String getRepositoryOwner() { - return repositoryOwner; - } - - @Override - public void setRepositoryOwner(@NonNull String repositoryOwner) { - this.repositoryOwner = repositoryOwner; - } - - @NonNull - public String getRepositoryName() { - return repositoryName; - } - - @Override - public void setRepositoryName(@NonNull String repositoryName) { - this.repositoryName = repositoryName; - } - - @NonNull - public String getServerURL() { - return serverURL; - } - @Override - public void setServerURL(@NonNull String serverURL) { - this.serverURL = serverURL; - } - - @Override - public void setCallbackURL(@NonNull String callbackURL) { - this.callbackURL = callbackURL; + public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint) { + this.serverURL = endpoint.getServerURL(); + this.callbackURL = UriTemplate.buildFromTemplate(callbackURL) + .query("server_url") + .build() + .set("server_url", serverURL) + .expand(); } @Override @@ -128,23 +102,14 @@ public void apply(BitbucketWebhookConfiguration configuration) { this.configuration = (ServerWebhookConfiguration) configuration; } - @NonNull - private String getCallbackURL() { - return UriTemplate.buildFromTemplate(callbackURL) - .query("server_url") - .build() - .set("server_url", serverURL) - .expand(); - } - @Override @NonNull - public Collection read(@NonNull BitbucketWebhookClient client) throws IOException { + public Collection read(@NonNull BitbucketAuthenticatedClient client) throws IOException { String endpointJenkinsRootURL = ObjectUtils.firstNonNull(configuration.getEndpointJenkinsRootURL(), BitbucketWebhookConfiguration.getDefaultJenkinsRootURL()); - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("start", 0) .set("limit", 200) .expand(); @@ -163,7 +128,7 @@ private BitbucketServerWebhook buildPayload() { hook.setActive(true); hook.setDescription("Jenkins hook"); hook.setEvents(NATIVE_SERVER_EVENTS); - hook.setUrl(getCallbackURL()); + hook.setUrl(callbackURL); if (configuration.isEnableHookSignature()) { String signatureCredentialsId = configuration.getHookSignatureCredentialsId(); StringCredentials signatureSecret = BitbucketCredentialsUtils.lookupCredentials(Jenkins.get(), serverURL, signatureCredentialsId, StringCredentials.class); @@ -176,10 +141,10 @@ private BitbucketServerWebhook buildPayload() { return hook; } - private void register(@NonNull BitbucketServerWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + private void register(@NonNull BitbucketServerWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .expand(); client.post(url, JsonParser.toString(payload)); } @@ -216,38 +181,38 @@ private boolean shouldUpdate(@NonNull BitbucketServerWebhook current, @NonNull B return update; } - private void update(@NonNull BitbucketServerWebhook payload, @NonNull BitbucketWebhookClient client) throws IOException { + private void update(@NonNull BitbucketServerWebhook payload, @NonNull BitbucketAuthenticatedClient client) throws IOException { String url = UriTemplate - .fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + .fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("id", payload.getUuid()) .expand(); client.put(url, JsonParser.toString(payload)); } @Override - public void remove(@NonNull String webhookId, @NonNull BitbucketWebhookClient client) throws IOException { - String url = UriTemplate.fromTemplate(serverURL + WEBHOOK_API) - .set("owner", repositoryOwner) - .set("repo", repositoryName) + public void remove(@NonNull String webhookId, @NonNull BitbucketAuthenticatedClient client) throws IOException { + String url = UriTemplate.fromTemplate(WEBHOOK_API) + .set("owner", client.getRepositoryOwner()) + .set("repo", client.getRepositoryName()) .set("id", webhookId) .expand(); client.delete(url); } @Override - public void register(@NonNull BitbucketWebhookClient client) throws IOException { + public void register(@NonNull BitbucketAuthenticatedClient client) throws IOException { BitbucketServerWebhook existingHook = (BitbucketServerWebhook) read(client) .stream() .findFirst() .orElse(null); if (existingHook == null) { - logger.log(Level.INFO, "Registering server hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Registering server hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); register(buildPayload(), client); } else if (shouldUpdate(existingHook, buildPayload())) { - logger.log(Level.INFO, "Updating server hook for {0}/{1}", new Object[] { repositoryOwner, repositoryName }); + logger.log(Level.INFO, "Updating server hook for {0}/{1}", new Object[] { client.getRepositoryOwner(), client.getRepositoryName() }); update(existingHook, client); } } 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 c32f00a1e..8efd1386d 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 @@ -24,6 +24,7 @@ package com.cloudbees.jenkins.plugins.bitbucket.server.client; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; @@ -37,6 +38,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; +import com.cloudbees.jenkins.plugins.bitbucket.impl.buildstatus.ServerBuildStatusNotifier; import com.cloudbees.jenkins.plugins.bitbucket.impl.client.AbstractBitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.impl.client.BitbucketTlsSocketStrategy; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketAccessTokenAuthenticator; @@ -47,7 +49,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; 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; @@ -82,7 +83,6 @@ import jenkins.scm.api.SCMFile; import jenkins.scm.api.SCMFile.Type; import jenkins.scm.impl.avatars.AvatarImage; -import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -91,9 +91,6 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicNameValuePair; -import static org.apache.commons.lang3.StringUtils.abbreviate; -import static org.apache.commons.lang3.StringUtils.substring; - /** * Bitbucket API client. * Developed and test with Bitbucket 4.3.2 @@ -118,15 +115,9 @@ public class BitbucketServerAPIClient extends AbstractBitbucketApi implements Bi private static final String API_BROWSE_PATH = API_REPOSITORY_PATH + "/browse{/path*}{?at}"; private static final String API_PROJECT_PATH = API_BASE_PATH + "/projects/{owner}"; private static final String AVATAR_PATH = API_BASE_PATH + "/projects/{owner}/avatar.png"; - private static final String API_WEBHOOKS_PATH = API_BASE_PATH + "/projects/{owner}/repos/{repo}/webhooks{/id}{?start,limit}"; private static final String API_COMMITS_PATH = API_REPOSITORY_PATH + "/commits{?since,until,merges,start,limit}"; private static final String API_COMMIT_PATH = API_REPOSITORY_PATH + "/commits{/hash}"; private static final String API_COMMIT_COMMENT_PATH = API_REPOSITORY_PATH + "/commits{/hash}/comments"; - private static final String API_COMMIT_STATUS_PATH = API_BASE_PATH + "/projects/{owner}/repos/{repo}/commits/{hash}/builds"; - - private static final String WEBHOOK_BASE_PATH = "/rest/webhook/1.0"; - private static final String WEBHOOK_REPOSITORY_PATH = WEBHOOK_BASE_PATH + "/projects/{owner}/repos/{repo}/configurations"; - private static final String WEBHOOK_REPOSITORY_CONFIG_PATH = WEBHOOK_REPOSITORY_PATH + "/{id}"; private static final String API_MIRRORS_FOR_REPO_PATH = "/rest/mirroring/1.0/repos/{id}/mirrors"; private static final String API_MIRRORS_PATH = "/rest/mirroring/1.0/mirrorServers"; @@ -177,14 +168,7 @@ protected boolean isSupportedAuthenticator(@CheckForNull BitbucketAuthenticator /** * Bitbucket Server manages two top level entities, owner and/or project. * Only one of them makes sense for a specific client object. - */ - @NonNull - @Override - public String getOwner() { - return owner; - } - - /** + *

* In Bitbucket server the top level entity is the Project, but the JSON API accepts users as a replacement * of Projects in most of the URLs (it's called user centric API). * @@ -193,7 +177,9 @@ public String getOwner() { * * @return the ~user or project */ - public String getUserCentricOwner() { + @NonNull + @Override + public String getOwner() { return userCentric ? "~" + owner : owner; } @@ -214,7 +200,7 @@ public String getRepositoryName() { public List getPullRequests() throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUESTS_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName); return getPullRequests(template); } @@ -223,7 +209,7 @@ public List getPullRequests() throws IOException { public List getOutgoingOpenPullRequests(String fromRef) throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUESTS_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("at", fromRef) .set("direction", "outgoing") @@ -235,7 +221,7 @@ public List getOutgoingOpenPullRequests(String fromR public List getIncomingOpenPullRequests(String toRef) throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUESTS_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("at", toRef) .set("direction", "incoming") @@ -334,7 +320,7 @@ private void setupClosureForPRBranch(@NonNull BitbucketServerPullRequest pr) { private void callPullRequestChangesById(@NonNull String id) throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUEST_CHANGES_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("id", id) .set("limit", 1) @@ -345,7 +331,7 @@ private void callPullRequestChangesById(@NonNull String id) throws IOException { private boolean getPullRequestCanMergeById(@NonNull String id) throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUEST_MERGE_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("id", id) .expand(); @@ -360,7 +346,7 @@ private boolean getPullRequestCanMergeById(@NonNull String id) throws IOExceptio public BitbucketPullRequest getPullRequestById(@NonNull Integer id) throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_PULL_REQUEST_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("id", id) .expand(); @@ -387,7 +373,7 @@ public BitbucketRepository getRepository() throws IOException { } String url = UriTemplate .fromTemplate(this.baseURL + API_REPOSITORY_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .expand(); String response = getRequest(url); @@ -441,7 +427,7 @@ public void postCommitComment(@NonNull String hash, @NonNull String comment) thr postRequest( UriTemplate .fromTemplate(this.baseURL + API_COMMIT_COMMENT_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("hash", hash) .expand(), @@ -454,22 +440,11 @@ public void postCommitComment(@NonNull String hash, @NonNull String comment) thr /** * {@inheritDoc} */ + @Deprecated @Override public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException { - BitbucketServerBuildStatus newStatus = new BitbucketServerBuildStatus(status); - newStatus.setName(abbreviate(newStatus.getName(), 255)); - - String key = status.getKey(); - if (StringUtils.length(key) > 255) { - newStatus.setKey(substring(key, 0, 255 - 33) + '/' + DigestUtils.md5Hex(key)); - } - - String url = UriTemplate.fromTemplate(this.baseURL + API_COMMIT_STATUS_PATH) - .set("owner", getUserCentricOwner()) - .set("repo", repositoryName) - .set("hash", newStatus.getHash()) - .expand(); - postRequest(url, JsonParser.toString(newStatus)); + ServerBuildStatusNotifier notifier = new ServerBuildStatusNotifier(); + notifier.sendBuildStatus(status, adapt(BitbucketAuthenticatedClient.class)); } /** @@ -479,7 +454,7 @@ public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOExcep public boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path) throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_BROWSE_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("path", path.split(Operator.PATH.getSeparator())) .set("at", branchOrHash) @@ -501,7 +476,7 @@ public boolean checkPathExists(@NonNull String branchOrHash, @NonNull String pat public String getDefaultBranch() throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_DEFAULT_BRANCH_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .expand(); try { @@ -519,7 +494,7 @@ public String getDefaultBranch() throws IOException { @Override public BitbucketServerBranch getTag(@NonNull String tagName) throws IOException { String url = UriTemplate.fromTemplate(this.baseURL + API_TAG_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("tagName", tagName) .expand() @@ -561,7 +536,7 @@ public List getBranches() throws IOException { private List getServerBranches(String apiPath) throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + apiPath) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName); List branches = getPagedRequest(template, BitbucketServerBranch.class); @@ -577,7 +552,7 @@ private List getServerBranches(String apiPath) throws IOE private BitbucketServerBranch getSingleBranch(String branchName) throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_BRANCHES_FILTERED_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("filterText", branchName); @@ -597,7 +572,7 @@ private BitbucketServerBranch getSingleBranch(String branchName) throws IOExcept public BitbucketCommit resolveCommit(@NonNull String hash) throws IOException { String url = UriTemplate .fromTemplate(this.baseURL + API_COMMIT_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("hash", hash) .expand(); @@ -674,7 +649,7 @@ public List getRepositories(@CheckForNull UserRoleInR throws IOException { UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_REPOSITORIES_PATH) - .set("owner", getUserCentricOwner()); + .set("owner", getOwner()); List repositories = new ArrayList<>(); try { @@ -816,6 +791,12 @@ protected HttpHost getHost() { return BitbucketApiUtils.toHttpHost(this.baseURL); } + @NonNull + @Override + protected String getBaseURL() { + return this.baseURL; + } + @Override public Iterable getDirectoryContent(BitbucketSCMFile directory) throws IOException { List files = new ArrayList<>(); @@ -823,7 +804,7 @@ public Iterable getDirectoryContent(BitbucketSCMFile directory) throws String branchOrHash = directory.getHash().contains("+") ? directory.getRef() : directory.getHash(); UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_BROWSE_PATH + "{&start,limit}") - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("path", directory.getPath().split(Operator.PATH.getSeparator())) .set("at", branchOrHash) @@ -872,7 +853,7 @@ public InputStream getFileContent(BitbucketSCMFile file) throws IOException { String branchOrHash = file.getHash().contains("+") ? file.getRef() : file.getHash(); UriTemplate template = UriTemplate .fromTemplate(this.baseURL + API_BROWSE_PATH + "{&start,limit}") - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("path", file.getPath().split(Operator.PATH.getSeparator())) .set("at", branchOrHash) @@ -911,7 +892,7 @@ private Map collectLines(String response, final List line public SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException { String branchOrHash = file.getHash().contains("+") ? file.getRef() : file.getHash(); String url = UriTemplate.fromTemplate(this.baseURL + API_BROWSE_PATH + "{&type,blame}") - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("path", file.getPath().split(Operator.PATH.getSeparator())) .set("at", branchOrHash) @@ -943,7 +924,7 @@ public SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException { @Override public List getCommits(String fromCommit, String toCommit) throws IOException { UriTemplate uriTemplate = UriTemplate.fromTemplate(this.baseURL + API_COMMITS_PATH) - .set("owner", getUserCentricOwner()) + .set("owner", getOwner()) .set("repo", repositoryName) .set("since", fromCommit) .set("until", toCommit); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java index 80176415a..c4b2d3cd6 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java @@ -25,10 +25,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; 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.client.pullrequest.BitbucketCloudPullRequestCommit; -import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.impl.BitbucketPlugin; import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketRepoAvatarMetadataAction; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; @@ -42,7 +40,6 @@ import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; -import com.thoughtworks.xstream.XStream; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.plugins.git.extensions.GitSCMExtension; @@ -99,21 +96,6 @@ private BitbucketSCMSource load(String dataSet) { return bss; } - // Also initialize the external endpoint configuration storage for some - // tests. Relevant XMLs are in a subdir of this class' fixtures. - private void loadBEC(String dataSet) { - // Note to use original BitbucketSCMSourceTest::getClass() here to get proper paths - String path = getClass().getSimpleName() + "/" + BitbucketEndpointConfiguration.class.getSimpleName() + "/" + dataSet + ".xml"; - XStream xs = new BitbucketEndpointConfiguration().getConfigFile().getXStream(); - URL url = getClass().getResource(path); - BitbucketEndpointConfiguration bec = (BitbucketEndpointConfiguration) xs.fromXML(url); - for (BitbucketEndpoint abe : bec.getEndpoints()) { - if (abe != null) { - BitbucketEndpointConfiguration.get().updateEndpoint(abe); - } - } - } - @Test void modern() throws Exception { BitbucketSCMSource instance = load(testName); @@ -217,56 +199,6 @@ void given__instance__when__setCredentials__then__credentials_set() { assertThat(instance.getCredentialsId()).isEqualTo("test"); } - // NOTE: The tests below require that a BitbucketEndpointConfiguration with - // expected BB server and J root URLs exists, otherwise a dummy one is - // instantiated via readResolveServerUrl() in BitbucketSCMSource::readResolve() - // and then causes readResolve() call stack to revert the object from the - // properly loaded values (from XML fixtures) into the default Root URL lookup, - // as coded and intended (config does exist, so we honor it). -// @Test -// void bitbucketJenkinsRootUrl_emptyDefaulted() throws Exception { -// loadBEC(testName); -// BitbucketSCMSource instance = load(testName); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo(ClassicDisplayURLProvider.get().getRoot()); -// -// // Verify that an empty custom URL keeps returning the -// // current global root URL (ending with a slash), -// // meaning "current value at the moment when we ask". -// JenkinsLocationConfiguration.get().setUrl("http://localjenkins:80"); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("http://localjenkins/"); -// -// JenkinsLocationConfiguration.get().setUrl("https://ourjenkins.master:8443/ci"); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("https://ourjenkins.master:8443/ci/"); -// } -// -// @Test -// void bitbucketJenkinsRootUrl_goodAsIs() { -// loadBEC(testName); -// BitbucketSCMSource instance = load(testName); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("http://jenkins.test:8080/"); -// } -// -// @Test -// void bitbucketJenkinsRootUrl_normalized() { -// loadBEC(testName); -// BitbucketSCMSource instance = load(testName); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("https://jenkins.test/"); -// } -// -// @Test -// void bitbucketJenkinsRootUrl_slashed() { -// loadBEC(testName); -// BitbucketSCMSource instance = load(testName); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("https://jenkins.test/jenkins/"); -// } -// -// @Test -// void bitbucketJenkinsRootUrl_notslashed() { -// loadBEC(testName); -// BitbucketSCMSource instance = load(testName); -// assertThat(instance.getEndpointJenkinsRootURL()).isEqualTo("https://jenkins.test/jenkins/"); -// } - @Test void test_show_bitbucket_avatar_trait() throws Exception { BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient(BitbucketCloudEndpoint.SERVER_URL); 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 deleted file mode 100644 index fac85f719..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJUnit5Test.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2024, Nikolas Falco, 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.notifier; - -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.PullRequestSCMRevision; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMockApiFactory; -import com.cloudbees.jenkins.plugins.bitbucket.api.PullRequestBranchType; -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; -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.impl.notifier.BitbucketBuildStatusNotifications.JobCheckoutListener; -import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhookConfiguration; -import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhookConfiguration; -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; -import com.cloudbees.jenkins.plugins.bitbucket.trait.ForkPullRequestDiscoveryTrait.TrustEveryone; -import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.FilePath; -import hudson.model.Result; -import hudson.model.StreamBuildListener; -import hudson.scm.SCM; -import hudson.scm.SCMRevisionState; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; -import jenkins.branch.Branch; -import jenkins.branch.BranchProjectFactory; -import jenkins.branch.BranchSource; -import jenkins.model.JenkinsLocationConfiguration; -import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; -import jenkins.scm.api.SCMHead; -import jenkins.scm.api.SCMHeadOrigin; -import jenkins.scm.api.SCMRevision; -import jenkins.scm.api.SCMRevisionAction; -import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; -import jenkins.scm.api.trait.SCMSourceTrait; -import org.apache.commons.codec.digest.DigestUtils; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import org.mockito.ArgumentCaptor; - -import static com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.getApiMockClient; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@WithJenkins -class BitbucketBuildStatusNotificationsJUnit5Test { - @ParameterizedTest(name = "When build result is {1} expect to notify status {2}") - @MethodSource("buildStatusProvider") - void test_status_notification_for_given_build_result(UnaryOperator traitCustomizer, - Result buildResult, - Status expectedStatus, - BitbucketApi apiClient, - @NonNull JenkinsRule r) throws Exception { - StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); - URL localJenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); - JenkinsLocationConfiguration.get().setUrl(localJenkinsURL.toString()); - - String serverURL = BitbucketCloudEndpoint.SERVER_URL; - - BitbucketBuildStatusNotificationsTrait trait = traitCustomizer.apply(new BitbucketBuildStatusNotificationsTrait()); - WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); - doReturn(buildResult).when(build).getResult(); - - FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); - - BitbucketMockApiFactory.add(serverURL, apiClient); - - JobCheckoutListener listener = new JobCheckoutListener(); - listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); - - ArgumentCaptor captor = ArgumentCaptor.forClass(BitbucketBuildStatus.class); - verify(apiClient).postBuildStatus(captor.capture()); - assertThat(captor.getValue().getState()).isEqualTo(expectedStatus.name()); - } - - @Issue("JENKINS-72780") - @Test - void test_status_notification_name_when_UseReadableNotificationIds_is_true(@NonNull JenkinsRule r) throws Exception { - StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); - URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); - JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); - - String serverURL = "https://acme.bitbucket.org"; - - BitbucketBuildStatusNotificationsTrait trait = new BitbucketBuildStatusNotificationsTrait(); - trait.setUseReadableNotificationIds(true); - WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); - doReturn(Result.SUCCESS).when(build).getResult(); - - FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); - - BitbucketApi apiClient = mock(BitbucketServerAPIClient.class); - BitbucketMockApiFactory.add(serverURL, apiClient); - - JobCheckoutListener listener = new JobCheckoutListener(); - listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); - - ArgumentCaptor captor = ArgumentCaptor.forClass(BitbucketBuildStatus.class); - verify(apiClient).postBuildStatus(captor.capture()); - assertThat(captor.getValue().getKey()).isEqualTo("P/BRANCH-JOB"); - assertThat(captor.getValue().getParent()).isEqualTo("P"); - } - - @Issue("JENKINS-75203") - @Test - void test_status_notification_parent_key_null_if_cloud_is_true(@NonNull JenkinsRule r) throws Exception { - StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); - URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); - JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); - - String serverURL = BitbucketCloudEndpoint.SERVER_URL; - - BitbucketBuildStatusNotificationsTrait trait = new BitbucketBuildStatusNotificationsTrait(); - - WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); - doReturn(Result.SUCCESS).when(build).getResult(); - - FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); - - BitbucketApi apiClient = mock(BitbucketCloudApiClient.class); - BitbucketMockApiFactory.add(serverURL, apiClient); - - JobCheckoutListener listener = new JobCheckoutListener(); - listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); - - ArgumentCaptor captor = ArgumentCaptor.forClass(BitbucketBuildStatus.class); - verify(apiClient).postBuildStatus(captor.capture()); - assertThat(captor.getValue().getKey()).isNotEmpty(); - assertThat(captor.getValue().getParent()).isNull(); - } - - @Issue("JENKINS-74970") - @Test - void test_status_notification_on_fork(@NonNull JenkinsRule r) throws Exception { - StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); - URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); - JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); - - String serverURL = "https://acme.bitbucket.org"; - - ForkPullRequestDiscoveryTrait trait = new ForkPullRequestDiscoveryTrait(2, new TrustEveryone()); - BranchSCMHead targetHead = new BranchSCMHead("master"); - PullRequestSCMHead scmHead = new PullRequestSCMHead("name", "repoOwner", "repository1", "feature1", - PullRequestBranchType.BRANCH, "1", "title", targetHead, new SCMHeadOrigin.Fork("repository1"), ChangeRequestCheckoutStrategy.HEAD); - SCMRevisionImpl prRevision = new SCMRevisionImpl(scmHead, "cff417db"); - SCMRevisionImpl targetRevision = new SCMRevisionImpl(targetHead, "c341232342311"); - SCMRevision scmRevision = new PullRequestSCMRevision(scmHead, targetRevision, prRevision); - WorkflowRun build = prepareBuildForNotification(r, trait, serverURL, scmRevision); - doReturn(Result.SUCCESS).when(build).getResult(); - - FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); - - BitbucketApi apiClient = mock(BitbucketServerAPIClient.class); - BitbucketMockApiFactory.add(serverURL, apiClient); - - JobCheckoutListener listener = new JobCheckoutListener(); - listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); - - ArgumentCaptor captor = ArgumentCaptor.forClass(BitbucketBuildStatus.class); - verify(apiClient).postBuildStatus(captor.capture()); - assertThat(captor.getValue()).satisfies(status -> { - assertThat(status.getHash()).isEqualTo(prRevision.getHash()); - assertThat(status.getKey()).isEqualTo(DigestUtils.md5Hex("p/branch-job")); - assertThat(status.getParent()).isNull(); - assertThat(status.getRefname()).isEqualTo("refs/heads/" + scmHead.getBranchName()); - }); - } - - private static Stream buildStatusProvider() { - UnaryOperator notifyAbortAsCancelled = t -> { - t.setSendStoppedNotificationForAbortBuild(true); - return t; - }; - UnaryOperator notifyNotBuiltAsCancelled = t -> { - t.setDisableNotificationForNotBuildJobs(true); - return t; - }; - - return Stream.of( - Arguments.of(UnaryOperator.identity(), Result.ABORTED, Status.FAILED, mock(BitbucketCloudApiClient.class)), - Arguments.of(UnaryOperator.identity(), Result.ABORTED, Status.FAILED, mock(BitbucketServerAPIClient.class)), - Arguments.of(notifyAbortAsCancelled, Result.ABORTED, Status.STOPPED, mock(BitbucketCloudApiClient.class)), - Arguments.of(notifyAbortAsCancelled, Result.ABORTED, Status.CANCELLED, mock(BitbucketServerAPIClient.class)), - Arguments.of(UnaryOperator.identity(), Result.NOT_BUILT, Status.FAILED, mock(BitbucketCloudApiClient.class)), - Arguments.of(UnaryOperator.identity(), Result.NOT_BUILT, Status.FAILED, mock(BitbucketServerAPIClient.class)), - Arguments.of(notifyNotBuiltAsCancelled, Result.NOT_BUILT, Status.STOPPED, mock(BitbucketCloudApiClient.class)), - Arguments.of(notifyNotBuiltAsCancelled, Result.NOT_BUILT, Status.CANCELLED, mock(BitbucketServerAPIClient.class)) - ); - } - - private WorkflowRun prepareBuildForNotification(@NonNull JenkinsRule r, @NonNull SCMSourceTrait trait, @NonNull String serverURL) throws Exception { - SCMHead scmHead = new BranchSCMHead("master"); - SCMRevision scmRevision = new SCMRevisionImpl(scmHead, "c341232342311"); - return prepareBuildForNotification(r, trait, serverURL, scmRevision); - } - - private WorkflowRun prepareBuildForNotification(@NonNull JenkinsRule r, @NonNull SCMSourceTrait trait, @NonNull String serverURL, SCMRevision scmRevision) throws Exception { - BitbucketSCMSource scmSource = new BitbucketSCMSource("repoOwner", "repository"); - scmSource.setServerUrl(serverURL); - scmSource.setTraits(List.of(trait)); - - WorkflowMultiBranchProject project = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); - project.setSourcesList(List.of(new BranchSource(scmSource))); - scmSource.setOwner(project); - - WorkflowJob job = new WorkflowJob(project, "branch-job"); - - SCM scm = mock(SCM.class); - - WorkflowRun build = mock(WorkflowRun.class); - doReturn(List.of(new SCMRevisionAction(scmSource, scmRevision))).when(build).getActions(SCMRevisionAction.class); - doReturn(job).when(build).getParent(); - doReturn("builds/1/").when(build).getUrl(); - @SuppressWarnings("unchecked") - BranchProjectFactory projectFactory = mock(BranchProjectFactory.class); - when(projectFactory.isProject(job)).thenReturn(true); - when(projectFactory.asProject(job)).thenReturn(job); - Branch branch = new Branch(scmSource.getId(), scmRevision.getHead(), scm, Collections.emptyList()); - when(projectFactory.getBranch(job)).thenReturn(branch); - project.setProjectFactory(projectFactory); - - return build; - } - - public static Stream buildServerURLsProvider() { - return Stream.of( - Arguments.of("localhost", "Jenkins URL cannot start with http://localhost"), - Arguments.of("unconfigured-jenkins-location", "Could not determine Jenkins URL."), - Arguments.of("localhost.local", null), - Arguments.of("intranet.local:8080", null), - Arguments.of("www.mydomain.com:8000", null), - Arguments.of("www.mydomain.com", null) - ); - } - - @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", new ServerWebhookConfiguration(true, "dummy")); - BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); - - BitbucketApi client = getApiMockClient(endpoint.getServerURL()); - if (expectedExceptionMsg != null) { - assertThatIllegalStateException() - .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) - .withMessage(expectedExceptionMsg); - assertThatIllegalStateException() - .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)) - .withMessage(expectedExceptionMsg); - } else { - assertThat(BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)).isNotNull(); - assertThat(BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)).isNotNull(); - } - } - - public static Stream buildCloudURLsProvider() { - String fqdn = "Please use a fully qualified name or an IP address for Jenkins URL, this is required by Bitbucket cloud"; - - return Stream.of( - Arguments.of("localhost", "Jenkins URL cannot start with http://localhost"), - Arguments.of("unconfigured-jenkins-location", "Could not determine Jenkins URL."), - Arguments.of("intranet", fqdn), - Arguments.of("intranet:8080", fqdn), - Arguments.of("localhost.local", null), - Arguments.of("intranet.local:8080", null), - Arguments.of("www.mydomain.com:8000", null), - Arguments.of("www.mydomain.com", null) - ); - } - - @ParameterizedTest(name = "checkURL {0} against Bitbucket Cloud") - @MethodSource("buildCloudURLsProvider") - void test_checkURL_for_Bitbucket_cloud(String jenkinsURL, String expectedExceptionMsg, @NonNull JenkinsRule r) { - BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhookConfiguration(true, "second")); - BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); - - BitbucketApi client = getApiMockClient(endpoint.getServerURL()); - if (expectedExceptionMsg != null) { - assertThatIllegalStateException() - .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) - .withMessage(expectedExceptionMsg); - assertThatIllegalStateException() - .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)) - .withMessage(expectedExceptionMsg); - } else { - assertThat(BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)).isNotNull(); - assertThat(BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)).isNotNull(); - } - } -} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJobListenerTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJobListenerTest.java new file mode 100644 index 000000000..2cbe0da12 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsJobListenerTest.java @@ -0,0 +1,212 @@ +/* + * The MIT License + * + * Copyright (c) 2020, 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.notifier; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.FirstCheckoutCompletedInvisibleAction; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMockApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; +import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.trait.BranchDiscoveryTrait; +import hudson.model.Descriptor.FormException; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.model.TaskListener; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import jenkins.branch.BranchSource; +import jenkins.plugins.git.GitSampleRepoRule; +import jenkins.plugins.git.junit.jupiter.WithGitSampleRepo; +import jenkins.scm.api.SCMFile.Type; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.flow.FlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.multibranch.AbstractWorkflowBranchProjectFactory; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +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.SingleFileSCM; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.mockito.internal.stubbing.answers.Returns; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@WithGitSampleRepo +@WithJenkins +class BitbucketBuildStatusNotificationsJobListenerTest { + + private GitSampleRepoRule sampleRepo; + private JenkinsRule r; + + @BeforeEach + void setup(GitSampleRepoRule sampleRepo, JenkinsRule rule) throws Exception { + this.sampleRepo = sampleRepo; + this.r = spy(rule); + doReturn(new URL("http://example.com:" + extractJenkinsHttpPort(rule) + rule.contextPath + "/")).when(r).getURL(); + } + + private Integer extractJenkinsHttpPort(JenkinsRule rule) { + Field field = ReflectionUtils.findField(JenkinsRule.class, "localPort"); + field.setAccessible(true); + Integer localPort = (Integer) ReflectionUtils.getField(field, rule); + return localPort; + } + + @Test + void noInappropriateFirstCheckoutCompletedInvisibleAction() throws Exception { + FreeStyleProject p = r.createFreeStyleProject(); + p.setScm(new SingleFileSCM("file", "contents")); + FreeStyleBuild b = r.buildAndAssertSuccess(p); + assertThat(b.getAllActions()).doesNotHaveAnyElementsOfTypes(FirstCheckoutCompletedInvisibleAction.class); + } + + private WorkflowMultiBranchProject prepareFirstCheckoutCompletedInvisibleActionTest(String dsl) throws Exception { + String repoOwner = "bob"; + String repositoryName = "foo"; + String branchName = "master"; + String jenkinsfile = "Jenkinsfile"; + sampleRepo.init(); + sampleRepo.write(jenkinsfile, dsl); + sampleRepo.git("add", jenkinsfile); + sampleRepo.git("commit", "--all", "--message=defined"); + + BitbucketApi api = mock(BitbucketApi.class); + BitbucketBranch branch = mock(BitbucketBranch.class); + List branchList = Collections.singletonList(branch); + when(api.getBranches()).thenAnswer(new Returns(branchList)); + when(api.getBranch("master")).thenAnswer(new Returns(branch)); + when(branch.getName()).thenReturn(branchName); + when(branch.getRawNode()).thenReturn(sampleRepo.head()); + BitbucketCommit commit = mock(BitbucketCommit.class); + when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); + when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); + BitbucketRepository repository = mock(BitbucketRepository.class); + when(api.getRepository()).thenReturn(repository); + when(repository.getOwnerName()).thenReturn(repoOwner); + when(repository.getRepositoryName()).thenReturn(repositoryName); + when(repository.getScm()).thenReturn("git"); + when(repository.getLinks()).thenReturn( + Collections.singletonMap("clone", + Collections.singletonList(new BitbucketHref("http", sampleRepo.toString())) + ) + ); + when(api.getRepository()).thenReturn(repository); + when(api.getFileContent(any(BitbucketSCMFile.class))).thenReturn( + new ByteArrayInputStream(dsl.getBytes())); + when(api.getFile(any(BitbucketSCMFile.class))).thenReturn(new BitbucketSCMFile(mock(BitbucketSCMFile.class), "master", Type.REGULAR_FILE, "hash")); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); + + BitbucketSCMSource source = new BitbucketSCMSource(repoOwner, repositoryName); + WorkflowMultiBranchProject owner = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); + source.setTraits(Collections.singletonList( + new BranchDiscoveryTrait(true, true) + )); + owner.setSourcesList(Collections.singletonList(new BranchSource(source))); + source.setOwner(owner); + return owner; + } + + @Test + void firstCheckoutCompletedInvisibleAction() throws Exception { + String dsl = "node { checkout scm }"; + WorkflowMultiBranchProject owner = prepareFirstCheckoutCompletedInvisibleActionTest(dsl); + + owner.scheduleBuild2(0).getFuture().get(); + owner.getComputation().writeWholeLogTo(System.out); + assertThat(owner.getIndexing().getResult()).isEqualTo(Result.SUCCESS); + r.waitUntilNoActivity(); + WorkflowJob master = owner.getItem("master"); + WorkflowRun run = master.getLastBuild(); + run.writeWholeLogTo(System.out); + assertThat(run.getResult()).isEqualTo(Result.SUCCESS); + assertThat(run.getAllActions()).hasAtLeastOneElementOfType(FirstCheckoutCompletedInvisibleAction.class); + } + + @Issue("JENKINS-66040") + @Test + void shouldNotSetFirstCheckoutCompletedInvisibleActionOnOtherCheckoutWithNonDefaultFactory() throws Exception { + String dsl = "node { checkout(scm: [$class: 'GitSCM', userRemoteConfigs: [[url: 'https://github.com/jenkinsci/bitbucket-branch-source-plugin.git']], branches: [[name: 'master']]]) }"; + WorkflowMultiBranchProject owner = prepareFirstCheckoutCompletedInvisibleActionTest(dsl); + owner.setProjectFactory(new DummyWorkflowBranchProjectFactory(dsl)); + + owner.scheduleBuild2(0).getFuture().get(); + owner.getComputation().writeWholeLogTo(System.out); + assertThat(owner.getIndexing().getResult()).isEqualTo(Result.SUCCESS); + r.waitUntilNoActivity(); + WorkflowJob master = owner.getItem("master"); + WorkflowRun run = master.getLastBuild(); + run.writeWholeLogTo(System.out); + assertThat(run.getResult()).isEqualTo(Result.SUCCESS); + assertThat(run.getAllActions()).doesNotHaveAnyElementsOfTypes(FirstCheckoutCompletedInvisibleAction.class); + } + + private static class DummyWorkflowBranchProjectFactory extends AbstractWorkflowBranchProjectFactory { + private final String dsl; + + public DummyWorkflowBranchProjectFactory(String dsl) { + this.dsl = dsl; + } + + @Override + protected FlowDefinition createDefinition() { + try { + return new CpsFlowDefinition(dsl, true); + } catch (FormException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("serial") + @Override + protected SCMSourceCriteria getSCMSourceCriteria(SCMSource source) { + return new SCMSourceCriteria() { + @Override + public boolean isHead(Probe probe, TaskListener listener) throws IOException { + return true; + } + }; + } + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsTest.java index 29a083dcc..305d96d6f 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketBuildStatusNotificationsTest.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2020, CloudBees, Inc. + * Copyright (c) 2024, Nikolas Falco, 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 @@ -24,189 +24,382 @@ package com.cloudbees.jenkins.plugins.bitbucket.impl.notifier; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.FirstCheckoutCompletedInvisibleAction; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus.Status; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMockApiFactory; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; +import com.cloudbees.jenkins.plugins.bitbucket.api.PullRequestBranchType; +import com.cloudbees.jenkins.plugins.bitbucket.api.buildstatus.BitbucketBuildStatusCustomizer; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpointProvider; +import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint; -import com.cloudbees.jenkins.plugins.bitbucket.trait.BranchDiscoveryTrait; -import hudson.model.Descriptor.FormException; -import hudson.model.FreeStyleBuild; -import hudson.model.FreeStyleProject; +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.notifier.BitbucketBuildStatusNotifications.JobCompletedListener; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud.CloudWebhookConfiguration; +import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.server.ServerWebhookConfiguration; +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; +import com.cloudbees.jenkins.plugins.bitbucket.trait.ForkPullRequestDiscoveryTrait.TrustEveryone; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.FilePath; import hudson.model.Result; -import hudson.model.TaskListener; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.lang.reflect.Field; +import hudson.model.Run; +import hudson.model.StreamBuildListener; +import hudson.scm.SCM; +import hudson.scm.SCMRevisionState; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import jenkins.branch.Branch; +import jenkins.branch.BranchProjectFactory; import jenkins.branch.BranchSource; -import jenkins.plugins.git.GitSampleRepoRule; -import jenkins.plugins.git.junit.jupiter.WithGitSampleRepo; -import jenkins.scm.api.SCMFile.Type; -import jenkins.scm.api.SCMSource; -import jenkins.scm.api.SCMSourceCriteria; -import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; -import org.jenkinsci.plugins.workflow.flow.FlowDefinition; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMSourceTrait; +import net.javacrumbs.jsonunit.assertj.JsonAssertions; +import org.apache.commons.codec.digest.DigestUtils; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.jenkinsci.plugins.workflow.multibranch.AbstractWorkflowBranchProjectFactory; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.SingleFileSCM; +import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import org.mockito.internal.stubbing.answers.Returns; -import org.springframework.util.ReflectionUtils; +import org.mockito.ArgumentCaptor; +import static com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory.getApiMockClient; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.endsWith; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@WithGitSampleRepo @WithJenkins class BitbucketBuildStatusNotificationsTest { + @TestExtension + public static class BuildStatusTestCustomizer implements BitbucketBuildStatusCustomizer { + @Override + public void customize(Run build, BitbucketBuildStatus buildStatus) { + if (buildStatus.getState() != null && buildStatus.getState() != Status.INPROGRESS) { + buildStatus.addOptionalData("testResults", new TestResults(5, 2, 1)); + } + } + @Override + public boolean isApplicable(EndpointType type) { + return type == EndpointType.SERVER; + } + } - private GitSampleRepoRule sampleRepo; - private JenkinsRule r; + @ParameterizedTest(name = "When build result is {1} expect to notify status {2}") + @MethodSource("buildStatusProvider") + void test_status_notification_for_given_build_result(UnaryOperator traitCustomizer, + Result buildResult, + Status expectedStatus, + BitbucketApi apiClient, + @NonNull JenkinsRule r) throws Exception { + StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); + URL localJenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); + JenkinsLocationConfiguration.get().setUrl(localJenkinsURL.toString()); - @BeforeEach - void setup(GitSampleRepoRule sampleRepo, JenkinsRule rule) throws Exception { - this.sampleRepo = sampleRepo; - this.r = spy(rule); - doReturn(new URL("http://example.com:" + extractJenkinsHttpPort(rule) + rule.contextPath + "/")).when(r).getURL(); - } + String serverURL = BitbucketCloudEndpoint.SERVER_URL; + + BitbucketBuildStatusNotificationsTrait trait = traitCustomizer.apply(new BitbucketBuildStatusNotificationsTrait()); + WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); + doReturn(buildResult).when(build).getResult(); + + FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); - private Integer extractJenkinsHttpPort(JenkinsRule rule) { - Field field = ReflectionUtils.findField(JenkinsRule.class, "localPort"); - field.setAccessible(true); - Integer localPort = (Integer) ReflectionUtils.getField(field, rule); - return localPort; + BitbucketAuthenticatedClient client = mock(BitbucketAuthenticatedClient.class); + when(apiClient.adapt(BitbucketAuthenticatedClient.class)).thenReturn(client); + BitbucketMockApiFactory.add(serverURL, apiClient); + + JobCheckoutListener listener = new JobCheckoutListener(); + listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(client).post(anyString(), captor.capture()); + assertThatJson(captor.getValue()).isObject().containsEntry("state", expectedStatus.toString()); } + @Issue("JENKINS-76027") @Test - void noInappropriateFirstCheckoutCompletedInvisibleAction() throws Exception { - FreeStyleProject p = r.createFreeStyleProject(); - p.setScm(new SingleFileSCM("file", "contents")); - FreeStyleBuild b = r.buildAndAssertSuccess(p); - assertThat(b.getAllActions()).doesNotHaveAnyElementsOfTypes(FirstCheckoutCompletedInvisibleAction.class); + void test_server_customizer(@NonNull JenkinsRule r) throws Exception { + StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); + URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); + JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); + + String serverURL = "https://acme.bitbucket.org"; + BitbucketEndpointProvider.registerEndpoint("JENKINS-76027", serverURL, null); + + BitbucketBuildStatusNotificationsTrait trait = new BitbucketBuildStatusNotificationsTrait(); + trait.setUseReadableNotificationIds(true); + WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); + doReturn(Result.SUCCESS).when(build).getResult(); + + BitbucketAuthenticatedClient client = buildAuthenticatedClient(serverURL); + + JobCompletedListener listener = new JobCompletedListener(); + listener.onCompleted(build, taskListener); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(client).post(endsWith("/rest/api/1.0/projects/repoOwner/repos/repository/commits/c341232342311/builds"), captor.capture()); + assertThatJson(captor.getValue()) + .isObject() + .containsEntry("testResults", JsonAssertions.json("{\"successful\":5,\"failed\":2,\"skipped\":1}")); } - private WorkflowMultiBranchProject prepareFirstCheckoutCompletedInvisibleActionTest(String dsl) throws Exception { - String repoOwner = "bob"; - String repositoryName = "foo"; - String branchName = "master"; - String jenkinsfile = "Jenkinsfile"; - sampleRepo.init(); - sampleRepo.write(jenkinsfile, dsl); - sampleRepo.git("add", jenkinsfile); - sampleRepo.git("commit", "--all", "--message=defined"); - - BitbucketApi api = mock(BitbucketApi.class); - BitbucketBranch branch = mock(BitbucketBranch.class); - List branchList = Collections.singletonList(branch); - when(api.getBranches()).thenAnswer(new Returns(branchList)); - when(api.getBranch("master")).thenAnswer(new Returns(branch)); - when(branch.getName()).thenReturn(branchName); - when(branch.getRawNode()).thenReturn(sampleRepo.head()); - BitbucketCommit commit = mock(BitbucketCommit.class); - when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); - when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); - BitbucketRepository repository = mock(BitbucketRepository.class); - when(api.getRepository()).thenReturn(repository); - when(repository.getOwnerName()).thenReturn(repoOwner); - when(repository.getRepositoryName()).thenReturn(repositoryName); - when(repository.getScm()).thenReturn("git"); - when(repository.getLinks()).thenReturn( - Collections.singletonMap("clone", - Collections.singletonList(new BitbucketHref("http", sampleRepo.toString())) - ) - ); - when(api.getRepository()).thenReturn(repository); - when(api.getFileContent(any(BitbucketSCMFile.class))).thenReturn( - new ByteArrayInputStream(dsl.getBytes())); - when(api.getFile(any(BitbucketSCMFile.class))).thenReturn(new BitbucketSCMFile(mock(BitbucketSCMFile.class), "master", Type.REGULAR_FILE, "hash")); - BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); - - BitbucketSCMSource source = new BitbucketSCMSource(repoOwner, repositoryName); - WorkflowMultiBranchProject owner = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); - source.setTraits(Collections.singletonList( - new BranchDiscoveryTrait(true, true) - )); - owner.setSourcesList(Collections.singletonList(new BranchSource(source))); - source.setOwner(owner); - return owner; + @Issue("JENKINS-72780") + @Test + void test_status_notification_name_when_UseReadableNotificationIds_is_true(@NonNull JenkinsRule r) throws Exception { + StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); + URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); + JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); + + String serverURL = "https://acme.bitbucket.org"; + BitbucketEndpointProvider.registerEndpoint("JENKINS-72780", serverURL, null); + + BitbucketBuildStatusNotificationsTrait trait = new BitbucketBuildStatusNotificationsTrait(); + trait.setUseReadableNotificationIds(true); + WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); + doReturn(Result.SUCCESS).when(build).getResult(); + + FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); + + BitbucketAuthenticatedClient client = buildAuthenticatedClient(serverURL); + + JobCheckoutListener listener = new JobCheckoutListener(); + listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(client).post(anyString(), captor.capture()); + assertThatJson(captor.getValue()) + .isObject() + .containsEntry("key", "P/BRANCH-JOB") + .containsEntry("parent", "P") + .containsEntry("testResults", JsonAssertions.json("{\"successful\":5,\"failed\":2,\"skipped\":1}")); } + @Issue("JENKINS-75203") @Test - void firstCheckoutCompletedInvisibleAction() throws Exception { - String dsl = "node { checkout scm }"; - WorkflowMultiBranchProject owner = prepareFirstCheckoutCompletedInvisibleActionTest(dsl); - - owner.scheduleBuild2(0).getFuture().get(); - owner.getComputation().writeWholeLogTo(System.out); - assertThat(owner.getIndexing().getResult()).isEqualTo(Result.SUCCESS); - r.waitUntilNoActivity(); - WorkflowJob master = owner.getItem("master"); - WorkflowRun run = master.getLastBuild(); - run.writeWholeLogTo(System.out); - assertThat(run.getResult()).isEqualTo(Result.SUCCESS); - assertThat(run.getAllActions()).hasAtLeastOneElementOfType(FirstCheckoutCompletedInvisibleAction.class); + void test_status_notification_parent_key_null_if_cloud_is_true(@NonNull JenkinsRule r) throws Exception { + StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); + URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); + JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); + + String serverURL = BitbucketCloudEndpoint.SERVER_URL; + + BitbucketBuildStatusNotificationsTrait trait = new BitbucketBuildStatusNotificationsTrait(); + + WorkflowRun build = prepareBuildForNotification(r, trait, serverURL); + doReturn(Result.SUCCESS).when(build).getResult(); + + FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); + + BitbucketAuthenticatedClient client = buildAuthenticatedClient(serverURL); + + JobCheckoutListener listener = new JobCheckoutListener(); + listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(client).post(endsWith("/2.0/repositories/repoOwner/repository/commit/c341232342311/statuses/build"), captor.capture()); + assertThatJson(captor.getValue()) + .isObject() + .containsKey("key").isNotNull() + .doesNotContainKey("parent"); } - @Issue("JENKINS-66040") + @Issue("JENKINS-74970") @Test - void shouldNotSetFirstCheckoutCompletedInvisibleActionOnOtherCheckoutWithNonDefaultFactory() throws Exception { - String dsl = "node { checkout(scm: [$class: 'GitSCM', userRemoteConfigs: [[url: 'https://github.com/jenkinsci/bitbucket-branch-source-plugin.git']], branches: [[name: 'master']]]) }"; - WorkflowMultiBranchProject owner = prepareFirstCheckoutCompletedInvisibleActionTest(dsl); - owner.setProjectFactory(new DummyWorkflowBranchProjectFactory(dsl)); - - owner.scheduleBuild2(0).getFuture().get(); - owner.getComputation().writeWholeLogTo(System.out); - assertThat(owner.getIndexing().getResult()).isEqualTo(Result.SUCCESS); - r.waitUntilNoActivity(); - WorkflowJob master = owner.getItem("master"); - WorkflowRun run = master.getLastBuild(); - run.writeWholeLogTo(System.out); - assertThat(run.getResult()).isEqualTo(Result.SUCCESS); - assertThat(run.getAllActions()).doesNotHaveAnyElementsOfTypes(FirstCheckoutCompletedInvisibleAction.class); + void test_status_notification_on_fork(@NonNull JenkinsRule r) throws Exception { + StreamBuildListener taskListener = new StreamBuildListener(System.out, StandardCharsets.UTF_8); + URL jenkinsURL = new URL("http://example.com:" + r.getURL().getPort() + r.contextPath + "/"); + JenkinsLocationConfiguration.get().setUrl(jenkinsURL.toString()); + + String serverURL = "https://acme.bitbucket.org"; + BitbucketEndpointProvider.registerEndpoint("JENKINS-74970", serverURL, null); + + ForkPullRequestDiscoveryTrait trait = new ForkPullRequestDiscoveryTrait(2, new TrustEveryone()); + BranchSCMHead targetHead = new BranchSCMHead("master"); + PullRequestSCMHead scmHead = new PullRequestSCMHead("name", "repoOwner", "repository1", "feature1", + PullRequestBranchType.BRANCH, "1", "title", targetHead, new SCMHeadOrigin.Fork("repository1"), ChangeRequestCheckoutStrategy.HEAD); + SCMRevisionImpl prRevision = new SCMRevisionImpl(scmHead, "cff417db"); + SCMRevisionImpl targetRevision = new SCMRevisionImpl(targetHead, "c341232342311"); + SCMRevision scmRevision = new PullRequestSCMRevision(scmHead, targetRevision, prRevision); + WorkflowRun build = prepareBuildForNotification(r, trait, serverURL, scmRevision); + doReturn(Result.SUCCESS).when(build).getResult(); + + FilePath workspace = r.jenkins.getWorkspaceFor(build.getParent()); + + BitbucketAuthenticatedClient client = buildAuthenticatedClient(serverURL); + + JobCheckoutListener listener = new JobCheckoutListener(); + listener.onCheckout(build, null, workspace, taskListener, null, SCMRevisionState.NONE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(client).post(endsWith("/rest/api/1.0/projects/repoOwner/repos/repository/commits/" + prRevision.getHash() + "/builds"), captor.capture()); + assertThatJson(captor.getValue()) + .isObject() + .containsEntry("key", DigestUtils.md5Hex("p/branch-job")) + .containsEntry("ref", "refs/heads/" + scmHead.getBranchName()) + .doesNotContainKey("parent"); } - private static class DummyWorkflowBranchProjectFactory extends AbstractWorkflowBranchProjectFactory { - private final String dsl; + private BitbucketAuthenticatedClient buildAuthenticatedClient(String serverURL) { + Class apiClientClass = BitbucketApiUtils.isCloud(serverURL) ? BitbucketCloudApiClient.class : BitbucketServerAPIClient.class; + BitbucketApi apiClient = mock(apiClientClass); + BitbucketAuthenticatedClient authClient = mock(BitbucketAuthenticatedClient.class); + when(authClient.getRepositoryOwner()).thenReturn("repoOwner"); + when(authClient.getRepositoryName()).thenReturn("repository"); + when(apiClient.adapt(BitbucketAuthenticatedClient.class)).thenReturn(authClient); + BitbucketMockApiFactory.add(serverURL, apiClient); + return authClient; + } - public DummyWorkflowBranchProjectFactory(String dsl) { - this.dsl = dsl; - } + private static Stream buildStatusProvider() { + UnaryOperator notifyAbortAsCancelled = t -> { + t.setSendStoppedNotificationForAbortBuild(true); + return t; + }; + UnaryOperator notifyNotBuiltAsCancelled = t -> { + t.setDisableNotificationForNotBuildJobs(true); + return t; + }; - @Override - protected FlowDefinition createDefinition() { - try { - return new CpsFlowDefinition(dsl, true); - } catch (FormException e) { - throw new RuntimeException(e); - } + return Stream.of( + Arguments.of(UnaryOperator.identity(), Result.ABORTED, Status.FAILED, mock(BitbucketCloudApiClient.class)), + Arguments.of(UnaryOperator.identity(), Result.ABORTED, Status.FAILED, mock(BitbucketServerAPIClient.class)), + Arguments.of(notifyAbortAsCancelled, Result.ABORTED, Status.STOPPED, mock(BitbucketCloudApiClient.class)), + Arguments.of(notifyAbortAsCancelled, Result.ABORTED, Status.CANCELLED, mock(BitbucketServerAPIClient.class)), + Arguments.of(UnaryOperator.identity(), Result.NOT_BUILT, Status.FAILED, mock(BitbucketCloudApiClient.class)), + Arguments.of(UnaryOperator.identity(), Result.NOT_BUILT, Status.FAILED, mock(BitbucketServerAPIClient.class)), + Arguments.of(notifyNotBuiltAsCancelled, Result.NOT_BUILT, Status.STOPPED, mock(BitbucketCloudApiClient.class)), + Arguments.of(notifyNotBuiltAsCancelled, Result.NOT_BUILT, Status.CANCELLED, mock(BitbucketServerAPIClient.class)) + ); + } + + private WorkflowRun prepareBuildForNotification(@NonNull JenkinsRule r, @NonNull SCMSourceTrait trait, @NonNull String serverURL) throws Exception { + SCMHead scmHead = new BranchSCMHead("master"); + SCMRevision scmRevision = new SCMRevisionImpl(scmHead, "c341232342311"); + return prepareBuildForNotification(r, trait, serverURL, scmRevision); + } + + private WorkflowRun prepareBuildForNotification(@NonNull JenkinsRule r, @NonNull SCMSourceTrait trait, @NonNull String serverURL, SCMRevision scmRevision) throws Exception { + BitbucketSCMSource scmSource = new BitbucketSCMSource("repoOwner", "repository"); + scmSource.setServerUrl(serverURL); + scmSource.setTraits(List.of(trait)); + + WorkflowMultiBranchProject project = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); + project.setSourcesList(List.of(new BranchSource(scmSource))); + scmSource.setOwner(project); + + WorkflowJob job = new WorkflowJob(project, "branch-job"); + + SCM scm = mock(SCM.class); + + WorkflowRun build = mock(WorkflowRun.class); + doReturn(List.of(new SCMRevisionAction(scmSource, scmRevision))).when(build).getActions(SCMRevisionAction.class); + doReturn(job).when(build).getParent(); + doReturn("builds/1/").when(build).getUrl(); + @SuppressWarnings("unchecked") + BranchProjectFactory projectFactory = mock(BranchProjectFactory.class); + when(projectFactory.isProject(job)).thenReturn(true); + when(projectFactory.asProject(job)).thenReturn(job); + Branch branch = new Branch(scmSource.getId(), scmRevision.getHead(), scm, Collections.emptyList()); + when(projectFactory.getBranch(job)).thenReturn(branch); + project.setProjectFactory(projectFactory); + + return build; + } + + public static Stream buildServerURLsProvider() { + return Stream.of( + Arguments.of("localhost", "Jenkins URL cannot start with http://localhost"), + Arguments.of("unconfigured-jenkins-location", "Could not determine Jenkins URL."), + Arguments.of("localhost.local", null), + Arguments.of("intranet.local:8080", null), + Arguments.of("www.mydomain.com:8000", null), + Arguments.of("www.mydomain.com", null) + ); + } + + @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", new ServerWebhookConfiguration(true, "dummy")); + BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); + + BitbucketApi client = getApiMockClient(endpoint.getServerURL()); + if (expectedExceptionMsg != null) { + assertThatIllegalStateException() + .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) + .withMessage(expectedExceptionMsg); + assertThatIllegalStateException() + .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)) + .withMessage(expectedExceptionMsg); + } else { + assertThat(BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)).isNotNull(); + assertThat(BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)).isNotNull(); } + } - @SuppressWarnings("serial") - @Override - protected SCMSourceCriteria getSCMSourceCriteria(SCMSource source) { - return new SCMSourceCriteria() { - @Override - public boolean isHead(Probe probe, TaskListener listener) throws IOException { - return true; - } - }; + public static Stream buildCloudURLsProvider() { + String fqdn = "Please use a fully qualified name or an IP address for Jenkins URL, this is required by Bitbucket cloud"; + + return Stream.of( + Arguments.of("localhost", "Jenkins URL cannot start with http://localhost"), + Arguments.of("unconfigured-jenkins-location", "Could not determine Jenkins URL."), + Arguments.of("intranet", fqdn), + Arguments.of("intranet:8080", fqdn), + Arguments.of("localhost.local", null), + Arguments.of("intranet.local:8080", null), + Arguments.of("www.mydomain.com:8000", null), + Arguments.of("www.mydomain.com", null) + ); + } + + @ParameterizedTest(name = "checkURL {0} against Bitbucket Cloud") + @MethodSource("buildCloudURLsProvider") + void test_checkURL_for_Bitbucket_cloud(String jenkinsURL, String expectedExceptionMsg, @NonNull JenkinsRule r) { + BitbucketCloudEndpoint endpoint = new BitbucketCloudEndpoint(false, 0, 0, new CloudWebhookConfiguration(true, "second")); + BitbucketEndpointConfiguration.get().setEndpoints(List.of(endpoint)); + + BitbucketApi client = getApiMockClient(endpoint.getServerURL()); + if (expectedExceptionMsg != null) { + assertThatIllegalStateException() + .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)) + .withMessage(expectedExceptionMsg); + assertThatIllegalStateException() + .isThrownBy(() -> BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)) + .withMessage(expectedExceptionMsg); + } else { + assertThat(BitbucketBuildStatusNotifications.checkURL("http://" + jenkinsURL + "/build/sample", client)).isNotNull(); + assertThat(BitbucketBuildStatusNotifications.checkURL("https://" + jenkinsURL + "/build/sample", client)).isNotNull(); } } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketDefaulNotifier.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResults.java similarity index 54% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketDefaulNotifier.java rename to src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResults.java index f58b3e25c..477c21cbb 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/BitbucketDefaulNotifier.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/notifier/TestResults.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016, CloudBees, Inc. + * 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 @@ -23,32 +23,51 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.impl.notifier; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketNotifier; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.IOException; - /** - * The default Bitbucket notifier implementation that sends notifications. + * A summary of the passed, failed and skipped tests */ -public class BitbucketDefaulNotifier implements BitbucketNotifier { +public class TestResults { + + private int successful; + private int failed; + private int skipped; + + public TestResults() { + } + + public TestResults(int successful, int failed, int skipped) { + this.successful = successful; + this.failed = failed; + this.skipped = skipped; + } - private final BitbucketApi bitbucket; + public TestResults(TestResults other) { + this.successful = other.successful; + this.failed = other.failed; + this.skipped = other.skipped; + } + + public int getSuccessful() { + return successful; + } - public BitbucketDefaulNotifier(@NonNull BitbucketApi bitbucket) { - this.bitbucket = bitbucket; + public void setSuccessful(int successful) { + this.successful = successful; } - @Override - public void notifyComment(String repoOwner, String repoName, String hash, String content) - throws IOException, InterruptedException { - bitbucket.postCommitComment(hash, content); + public int getFailed() { + return failed; } - @Override - public void notifyBuildStatus(BitbucketBuildStatus status) throws IOException, InterruptedException { - bitbucket.postBuildStatus(status); + public void setFailed(int failed) { + this.failed = failed; } -} + public int getSkipped() { + return skipped; + } + + public void setSkipped(int skipped) { + this.skipped = skipped; + } +} \ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyBitbucketWebhook.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyBitbucketWebhook.java deleted file mode 100644 index cb300f2c1..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyBitbucketWebhook.java +++ /dev/null @@ -1,67 +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.impl.webhook; - -import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.EndpointType; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; -import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.Extension; - -public class DummyBitbucketWebhook extends AbstractBitbucketWebhookConfiguration { - - DummyBitbucketWebhook(boolean manageHooks, String credentialsId) { - super(manageHooks, credentialsId, false, null); - } - - @Override - public String getDisplayName() { - return "Dummy"; - } - - @NonNull - @Override - public String getId() { - return "DUMMY"; - } - - @NonNull - @Override - public String getEndpointJenkinsRootURL() { - return "http://master.example.com"; - } - - @Override - public Class getManager() { - return DummyWebhookManager.class; - } - - @Extension // TestExtension could be used only for embedded classes - public static class DescriptorImpl extends AbstractBitbucketWebhookDescriptorImpl { - @Override - public boolean isApplicable(@NonNull EndpointType type) { - return true; - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyWebhookManager.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyWebhookManager.java deleted file mode 100644 index f21c0d457..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/DummyWebhookManager.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookClient; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration; -import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager; -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import jenkins.scm.api.trait.SCMSourceTrait; - -public class DummyWebhookManager implements BitbucketWebhookManager { - - private String repositoryOwner; - private String repositoryName; - private String serverURL; - private String callbackURL; - - public String getRepositoryOwner() { - return repositoryOwner; - } - - public String getRepositoryName() { - return repositoryName; - } - - public String getServerURL() { - return serverURL; - } - - public String getCallbackURL() { - return callbackURL; - } - - @Override - public void setRepositoryOwner(String repositoryOwner) { - this.repositoryOwner = repositoryOwner; - } - - @Override - public void setRepositoryName(String repositoryName) { - this.repositoryName = repositoryName; - } - - @Override - public void setServerURL(String serverURL) { - this.serverURL = serverURL; - } - - @Override - public void setCallbackURL(String callbackURL) { - this.callbackURL = callbackURL; - } - - @Override - public void apply(BitbucketWebhookConfiguration configuration) { - } - - @Override - public Collection> supportedTraits() { - return Collections.emptyList(); - } - - @Override - public void apply(SCMSourceTrait trait) { - } - - @Override - public Collection read(BitbucketWebhookClient client) throws IOException { - return Collections.emptyList(); - } - - @Override - public void register(BitbucketWebhookClient client) throws IOException { - } - - @Override - public void remove(String webhookId, BitbucketWebhookClient client) throws IOException { - } - -} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTraitTest.java index 121573024..c6e8b1c5c 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTraitTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTraitTest.java @@ -30,9 +30,7 @@ import jenkins.scm.api.trait.SCMHeadFilter; import jenkins.scm.api.trait.SCMHeadPrefilter; import org.hamcrest.Matcher; -import org.junit.ClassRule; import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; @@ -43,9 +41,6 @@ import static org.junit.Assume.assumeThat; public class BranchDiscoveryTraitTest { - @ClassRule - public static JenkinsRule j = new JenkinsRule(); - @Test public void given__discoverAll__when__appliedToContext__then__noFilter() throws Exception { BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); @@ -112,8 +107,7 @@ public void given__onlyPRs__when__appliedToContext__then__filter() throws Except @Test public void given__descriptor__when__displayingOptions__then__allThreePresent() { - ListBoxModel options = - j.jenkins.getDescriptorByType(BranchDiscoveryTrait.DescriptorImpl.class).doFillStrategyIdItems(); + ListBoxModel options = new BranchDiscoveryTrait.DescriptorImpl().doFillStrategyIdItems(); assertThat(options.size(), is(3)); assertThat(options.get(0).value, is("1")); assertThat(options.get(1).value, is("2")); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/ForkPullRequestDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/ForkPullRequestDiscoveryTraitTest.java index 490c6d9bd..6cb5ffb84 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/ForkPullRequestDiscoveryTraitTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/ForkPullRequestDiscoveryTraitTest.java @@ -24,7 +24,6 @@ package com.cloudbees.jenkins.plugins.bitbucket.trait; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; -import com.cloudbees.jenkins.plugins.bitbucket.trait.ForkPullRequestDiscoveryTrait; import java.util.Collections; import java.util.EnumSet; import java.util.Set; diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/OriginPullRequestDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/OriginPullRequestDiscoveryTraitTest.java index 2fa1188c1..4f3917590 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/OriginPullRequestDiscoveryTraitTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/OriginPullRequestDiscoveryTraitTest.java @@ -24,7 +24,6 @@ package com.cloudbees.jenkins.plugins.bitbucket.trait; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; -import com.cloudbees.jenkins.plugins.bitbucket.trait.OriginPullRequestDiscoveryTrait; import java.util.Collections; import java.util.EnumSet; import java.util.Set; diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SSHCheckoutTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SSHCheckoutTraitTest.java index caad64290..ba6de98f1 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SSHCheckoutTraitTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/SSHCheckoutTraitTest.java @@ -34,91 +34,98 @@ import hudson.security.SecurityRealm; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.MockFolder; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assume.assumeThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; -public class SSHCheckoutTraitTest { - @ClassRule - public static JenkinsRule j = new JenkinsRule(); +@WithJenkins +class SSHCheckoutTraitTest { + private static JenkinsRule j; + + @BeforeAll + static void init(JenkinsRule rule) { + SSHCheckoutTraitTest.j = rule; + } @Test - public void given__legacyConfig__when__creatingTrait__then__convertedToModern() throws Exception { - assertThat(new SSHCheckoutTrait(BitbucketSCMSource.DescriptorImpl.ANONYMOUS).getCredentialsId(), - is(nullValue())); + void given__legacyConfig__when__creatingTrait__then__convertedToModern() throws Exception { + assertThat(new SSHCheckoutTrait(BitbucketSCMSource.DescriptorImpl.ANONYMOUS).getCredentialsId()).isNull(); } @Test - public void given__sshCheckoutWithCredentials__when__decoratingGit__then__credentialsApplied() throws Exception { + void given__sshCheckoutWithCredentials__when__decoratingGit__then__credentialsApplied() throws Exception { SSHCheckoutTrait instance = new SSHCheckoutTrait("keyId"); - BitbucketGitSCMBuilder probe = - new BitbucketGitSCMBuilder(new BitbucketSCMSource("example", "does-not-exist"), - new BranchSCMHead("main"), null, "scanId"); - assumeThat(probe.credentialsId(), is("scanId")); + BitbucketGitSCMBuilder probe = new BitbucketGitSCMBuilder(new BitbucketSCMSource("example", "does-not-exist"), new BranchSCMHead("main"), null, "scanId"); + assumeThat(probe.credentialsId()).isEqualTo("scanId"); + instance.decorateBuilder(probe); - assertThat(probe.credentialsId(), is("keyId")); + assertThat(probe.credentialsId()).isEqualTo("keyId"); } @Test - public void given__sshCheckoutWithAgentKey__when__decoratingGit__then__useAgentKeyApplied() throws Exception { + void given__sshCheckoutWithAgentKey__when__decoratingGit__then__useAgentKeyApplied() throws Exception { SSHCheckoutTrait instance = new SSHCheckoutTrait(null); - BitbucketGitSCMBuilder probe = - new BitbucketGitSCMBuilder(new BitbucketSCMSource( "example", "does-not-exist"), - new BranchSCMHead("main"), null, "scanId"); - assumeThat(probe.credentialsId(), is("scanId")); + BitbucketGitSCMBuilder probe = new BitbucketGitSCMBuilder(new BitbucketSCMSource( "example", "does-not-exist"), new BranchSCMHead("main"), null, "scanId"); + assumeThat(probe.credentialsId()).isEqualTo("scanId"); + instance.decorateBuilder(probe); - assertThat(probe.credentialsId(), is(nullValue())); + assertThat(probe.credentialsId()).isNull(); } @Test - public void given__descriptor__when__displayingCredentials__then__contractEnforced() throws Exception { + void given__descriptor__when__displayingCredentials__then__contractEnforced() throws Exception { final SSHCheckoutTrait.DescriptorImpl d = j.jenkins.getDescriptorByType(SSHCheckoutTrait.DescriptorImpl.class); final MockFolder dummy = j.createFolder("dummy"); SecurityRealm realm = j.jenkins.getSecurityRealm(); AuthorizationStrategy strategy = j.jenkins.getAuthorizationStrategy(); try { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy(); mockStrategy.grant(Jenkins.ADMINISTER).onRoot().to("admin"); mockStrategy.grant(Item.CONFIGURE).onItems(dummy).to("bob"); mockStrategy.grant(Item.EXTENDED_READ).onItems(dummy).to("jim"); j.jenkins.setAuthorizationStrategy(mockStrategy); - try (ACLContext context = ACL.as(User.get("admin"))) { + + try (ACLContext context = ACL.as(User.getOrCreateByIdOrFullName("admin"))) { ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); - assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); - assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, - is("does-not-exist")); + assertThat(rsp).describedAs("Expecting only the provided value so that form config unchanged") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEqualTo("does-not-exist")); + rsp = d.doFillCredentialsIdItems(null, "", "does-not-exist"); - assertThat("Expecting just the empty entry", rsp, hasSize(1)); - assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + assertThat(rsp).describedAs("Expecting just the empty entry") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEmpty()); } - try (ACLContext context = ACL.as(User.get("bob"))) { + try (ACLContext context = ACL.as(User.getOrCreateByIdOrFullName("bob"))) { ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); - assertThat("Expecting just the empty entry", rsp, hasSize(1)); - assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + assertThat(rsp).describedAs("Expecting just the empty entry") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEmpty()); + rsp = d.doFillCredentialsIdItems(null, "", "does-not-exist"); - assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); - assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, - is("does-not-exist")); + assertThat(rsp).describedAs("Expecting only the provided value so that form config unchanged") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEqualTo("does-not-exist")); } - try (ACLContext context = ACL.as(User.get("jim"))) { + try (ACLContext context = ACL.as(User.getOrCreateByIdOrFullName("jim"))) { ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); - assertThat("Expecting just the empty entry", rsp, hasSize(1)); - assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + assertThat(rsp).describedAs("Expecting just the empty entry") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEmpty()); } - try (ACLContext context = ACL.as(User.get("sue"))) { + try (ACLContext context = ACL.as(User.getOrCreateByIdOrFullName("sue"))) { ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); - assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); - assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, - is("does-not-exist")); + assertThat(rsp).describedAs("Expecting only the provided value so that form config unchanged") + .hasSize(1) + .element(0).satisfies(el -> assertThat(el.value).isEqualTo("does-not-exist")); } } finally { j.jenkins.setSecurityRealm(realm); diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_emptyDefaulted.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_emptyDefaulted.xml deleted file mode 100644 index 5abf7d6ee..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_emptyDefaulted.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - bb-beescloud - DUB - stunning-adventure - https://bitbucket-hook-1.test - diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_goodAsIs.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_goodAsIs.xml deleted file mode 100644 index 3031da528..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_goodAsIs.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - bb-beescloud - DUB - stunning-adventure - https://bitbucket-hook-2.test - diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_normalized.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_normalized.xml deleted file mode 100644 index 50aeb73d6..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_normalized.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - bb-beescloud - DUB - stunning-adventure - https://bitbucket-hook-3.test - diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_notslashed.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_notslashed.xml deleted file mode 100644 index a544dcb3c..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_notslashed.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - bb-beescloud - DUB - stunning-adventure - https://bitbucket-hook-4.test - diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_slashed.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_slashed.xml deleted file mode 100644 index 020d45a20..000000000 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/bitbucketJenkinsRootUrl_slashed.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - bb-beescloud - DUB - stunning-adventure - https://bitbucket-hook-5.test -