From 2a06876c5a7adee971295135d0963cf2133c9cd7 Mon Sep 17 00:00:00 2001 From: Bryan Turner Date: Tue, 19 Jun 2018 20:05:17 -0700 Subject: [PATCH] Replace ExecutorService with BucketedExecutor. Using the ScheduledExecutorService is better than performing the pushes directly in the hook method, but it still has limitations: - Busy repositories can result in parallel pushes to the mirror (#23) - The ScheduledExecutorService being used is a shared resources, which can result in other callers being starved out A better approach is to use the BucketedExecutor. It offers several improvements that are tailor-made for the work being done here: - Buckets can be used to control what can be done in parallel - Tasks with the same key are grouped, and at most a single node in a Data Center cluster can run them - Tasks with different keys can be run in parallel - Work can be shared among cluster nodes, in Data Center installations - Tasks can be scheduled from one cluster node and run on another - Locking is inherent in the BucketedExecutor's design, which means the hook doesn't need to do any locking of its own - This means there aren't any blocked threads. This fixes issue #23 by ensuring at most a single push happens to any given mirror, and by allowing the overall concurrency, even in Data Center installations, to be controlled. (#34) While I was making changes, because the minimum supported version for the plugin is already Bitbucket Server 5.5, I switched the code over to the new PostRepositoryHook SPI. This _could_ be extended to allow the hook to respond to a wider array of events, so mirroring would be triggered after pull request merges, branch or tag creation, etc. However, for the sake of consistency, the current code still only triggers pushes after other pushes. (Related to #45) Other things: - Added the ability to configure a timeout (#39) - Added the ability to configure the number of retries, and the number of BucketedExecutor threads _per cluster node_ - Added the ability to pass -Dbitbucket.test.version=5.11.1 (or any other version) to test against a version of Bitbucket Server other than the one being compiled against - Marked the plugin Data Center-compatible - Simplified wiring for DefaultPasswordEncryptor and removed the init method from the PasswordEncryptor interface --- pom.xml | 22 +- .../hook/DefaultPasswordEncryptor.java | 11 +- .../bitbucket/hook/MirrorBucketProcessor.java | 147 +++++++++ .../bitbucket/hook/MirrorRepositoryHook.java | 278 +++++------------- .../bitbucket/hook/MirrorRequest.java | 33 +++ .../bitbucket/hook/MirrorSettings.java | 15 + .../bitbucket/hook/PasswordEncryptor.java | 9 - src/main/resources/atlassian-plugin.xml | 19 +- .../hook/DefaultPasswordEncryptorTest.java | 95 ++---- .../hook/MirrorBucketProcessorTest.java | 155 ++++++++++ .../hook/MirrorRepositoryHookTest.java | 200 ++++--------- 11 files changed, 531 insertions(+), 453 deletions(-) create mode 100644 src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java create mode 100644 src/main/java/com/englishtown/bitbucket/hook/MirrorRequest.java create mode 100644 src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java create mode 100644 src/test/java/com/englishtown/bitbucket/hook/MirrorBucketProcessorTest.java diff --git a/pom.xml b/pom.xml index 08889e1..cac7f27 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,4 @@ - 4.0.0 @@ -31,9 +30,8 @@ 1.8 5.5.0 - ${bitbucket.version} - 3.1.0 - 6.3.7 + ${bitbucket.version} + 6.3.17 @@ -64,13 +62,23 @@ bitbucket-git-api provided + + com.atlassian.bitbucket.server + bitbucket-git-common + provided + + com.atlassian.sal sal-api - ${atlassian-sal-api.version} provided + + com.atlassian.bitbucket.server + bitbucket-test-util + test + junit junit @@ -96,8 +104,8 @@ bitbucket bitbucket - ${bitbucket.version} - ${bitbucket.data.version} + ${bitbucket.test.version} + ${bitbucket.test.version} diff --git a/src/main/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptor.java b/src/main/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptor.java index 0877e2d..7d9dd86 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptor.java +++ b/src/main/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptor.java @@ -1,6 +1,7 @@ package com.englishtown.bitbucket.hook; import com.atlassian.sal.api.pluginsettings.PluginSettings; +import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory; import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; @@ -16,11 +17,12 @@ public class DefaultPasswordEncryptor implements PasswordEncryptor { private SecretKey secretKey; - public static final String ENCRYPTED_PREFIX = "encrypted:"; - public static final String SETTINGS_CRYPTO_KEY = "crypto.key"; + static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror"; + static final String ENCRYPTED_PREFIX = "encrypted:"; + static final String SETTINGS_CRYPTO_KEY = "crypto.key"; - @Override - public void init(PluginSettings pluginSettings) { + public DefaultPasswordEncryptor(PluginSettingsFactory settingsFactory) { + PluginSettings pluginSettings = settingsFactory.createSettingsForKey(PLUGIN_SETTINGS_KEY); try { String keyBase64; @@ -40,7 +42,6 @@ public class DefaultPasswordEncryptor implements PasswordEncryptor { } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } - } protected byte[] runCipher(byte[] data, boolean encrypt) { diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java new file mode 100644 index 0000000..ac2297e --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java @@ -0,0 +1,147 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.concurrent.BucketProcessor; +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.permission.Permission; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.scm.Command; +import com.atlassian.bitbucket.scm.ScmCommandBuilder; +import com.atlassian.bitbucket.scm.ScmService; +import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler; +import com.atlassian.bitbucket.server.ApplicationPropertiesService; +import com.atlassian.bitbucket.user.SecurityService; +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.List; +import java.util.Locale; + +import static com.englishtown.bitbucket.hook.MirrorRepositoryHook.PROP_PREFIX; + +public class MirrorBucketProcessor implements BucketProcessor { + + static final String PROP_TIMEOUT = PROP_PREFIX + "timeout"; + + private static final String DEFAULT_REFSPEC = "+refs/heads/*:refs/heads/*"; + + private static final Logger log = LoggerFactory.getLogger(MirrorBucketProcessor.class); + + private final I18nService i18nService; + private final PasswordEncryptor passwordEncryptor; + private final RepositoryService repositoryService; + private final ScmService scmService; + private final SecurityService securityService; + private final Duration timeout; + + public MirrorBucketProcessor(I18nService i18nService, PasswordEncryptor passwordEncryptor, + ApplicationPropertiesService propertiesService, RepositoryService repositoryService, + ScmService scmService, SecurityService securityService) { + this.i18nService = i18nService; + this.passwordEncryptor = passwordEncryptor; + this.repositoryService = repositoryService; + this.scmService = scmService; + this.securityService = securityService; + + timeout = Duration.ofSeconds(propertiesService.getPluginProperty(PROP_TIMEOUT, 120L)); + } + + @Override + public void process(@Nonnull String key, @Nonnull List requests) { + if (requests.isEmpty()) { + return; + } + // Every request is for the same mirror URL, and the same repository ID. In case the + // settings (e.g. username/password) have been changed since the first request was + // queued, we process the _last_ request in the list. Since mirroring pushes all of + // the configured refspecs, any single request should roll up changes from any number + // of requests + MirrorRequest request = requests.get(requests.size() - 1); + + securityService.withPermission(Permission.REPO_READ, "Mirror changes") + .call(() -> { + Repository repository = repositoryService.getById(request.getRepositoryId()); + if (repository == null) { + log.debug("{}: Repository has been deleted", request.getRepositoryId()); + return null; + } + if (repositoryService.isEmpty(repository)) { + log.debug("{}: The repository is empty", repository); + return null; + } + runMirrorCommand(request.getSettings(), repository); + + return null; + }); + } + + private void runMirrorCommand(MirrorSettings settings, Repository repository) { + log.warn("{}: Preparing to push changes to mirror", repository); + + String password = passwordEncryptor.decrypt(settings.password); + String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password); + + // Call push command with the prune flag and refspecs for heads and tags + // Do not use the mirror flag as pull-request refs are included + ScmCommandBuilder builder = scmService.createBuilder(repository) + .command("push") + .argument("--prune") // this deletes locally deleted branches + .argument(authenticatedUrl) + .argument("--force"); + + // Use an atomic transaction to have a consistent state + if (settings.atomic) { + builder.argument("--atomic"); + } + + // Add refspec args + String refspecs = Strings.isNullOrEmpty(settings.refspec) ? DEFAULT_REFSPEC : settings.refspec; + for (String refspec : refspecs.split("\\s|\\n")) { + if (!Strings.isNullOrEmpty(refspec)) { + builder.argument(refspec); + } + } + + // Add tags refspec + if (settings.tags) { + builder.argument("+refs/tags/*:refs/tags/*"); + } + // Add notes refspec + if (settings.notes) { + builder.argument("+refs/notes/*:refs/notes/*"); + } + + PasswordHandler passwordHandler = new PasswordHandler(settings.password, + new GitCommandExitHandler(i18nService, repository)); + + Command command = builder.errorHandler(passwordHandler) + .exitHandler(passwordHandler) + .build(passwordHandler); + command.setTimeout(timeout); + + Object result = command.call(); + log.warn("{}: Push completed with the following output:\n{}", repository, result); + } + + String getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) { + // Only http(s) has username/password + if (!mirrorRepoUrl.toLowerCase(Locale.ROOT).startsWith("http")) { + return mirrorRepoUrl; + } + + URI uri = URI.create(mirrorRepoUrl); + String userInfo = username + ":" + password; + + try { + return new URI(uri.getScheme(), userInfo, uri.getHost(), uri.getPort(), + uri.getPath(), uri.getQuery(), uri.getFragment()).toString(); + } catch (URISyntaxException e) { + throw new IllegalStateException("The configured mirror URL (" + mirrorRepoUrl + ") is invalid", e); + } + } +} diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java index e6c8a7d..a55e4ef 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java @@ -1,47 +1,37 @@ package com.englishtown.bitbucket.hook; -import com.atlassian.bitbucket.hook.repository.AsyncPostReceiveRepositoryHook; -import com.atlassian.bitbucket.hook.repository.RepositoryHookContext; -import com.atlassian.bitbucket.i18n.I18nService; -import com.atlassian.bitbucket.repository.RefChange; +import com.atlassian.bitbucket.concurrent.BucketedExecutor; +import com.atlassian.bitbucket.concurrent.BucketedExecutorSettings; +import com.atlassian.bitbucket.concurrent.ConcurrencyPolicy; +import com.atlassian.bitbucket.concurrent.ConcurrencyService; +import com.atlassian.bitbucket.hook.repository.PostRepositoryHook; +import com.atlassian.bitbucket.hook.repository.PostRepositoryHookContext; +import com.atlassian.bitbucket.hook.repository.RepositoryPushHookRequest; import com.atlassian.bitbucket.repository.Repository; -import com.atlassian.bitbucket.repository.RepositoryService; -import com.atlassian.bitbucket.scm.CommandExitHandler; -import com.atlassian.bitbucket.scm.DefaultCommandExitHandler; -import com.atlassian.bitbucket.scm.ScmCommandBuilder; -import com.atlassian.bitbucket.scm.ScmService; -import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder; -import com.atlassian.bitbucket.setting.RepositorySettingsValidator; +import com.atlassian.bitbucket.scm.git.GitScm; +import com.atlassian.bitbucket.scope.RepositoryScope; +import com.atlassian.bitbucket.scope.Scope; +import com.atlassian.bitbucket.scope.ScopeVisitor; +import com.atlassian.bitbucket.server.ApplicationPropertiesService; import com.atlassian.bitbucket.setting.Settings; import com.atlassian.bitbucket.setting.SettingsValidationErrors; -import com.atlassian.sal.api.pluginsettings.PluginSettings; -import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory; -import com.google.common.base.Strings; +import com.atlassian.bitbucket.setting.SettingsValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.lang.reflect.Method; import java.net.URI; -import java.net.URISyntaxException; -import java.util.*; -import java.util.concurrent.ScheduledExecutorService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; -public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { - - protected static class MirrorSettings { - String mirrorRepoUrl; - String username; - String password; - String suffix; - String refspec; - boolean tags; - boolean notes; - boolean atomic; - } +public class MirrorRepositoryHook implements PostRepositoryHook, SettingsValidator { - public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror"; + static final String PROP_PREFIX = "plugin.com.englishtown.stash-hook-mirror.push."; + static final String PROP_ATTEMPTS = PROP_PREFIX + "attempts"; + static final String PROP_THREADS = PROP_PREFIX + "threads"; static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl"; static final String SETTING_USERNAME = "username"; static final String SETTING_PASSWORD = "password"; @@ -49,185 +39,82 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep static final String SETTING_TAGS = "tags"; static final String SETTING_NOTES = "notes"; static final String SETTING_ATOMIC = "atomic"; - static final int MAX_ATTEMPTS = 5; - static final String DEFAULT_REFSPEC = "+refs/heads/*:refs/heads/*"; - private final ScmService scmService; - private final I18nService i18nService; - private final ScheduledExecutorService executor; private final PasswordEncryptor passwordEncryptor; private final SettingsReflectionHelper settingsReflectionHelper; - private final RepositoryService repositoryService; + private final BucketedExecutor pushExecutor; private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); - public MirrorRepositoryHook( - ScmService scmService, - I18nService i18nService, - ScheduledExecutorService executor, - PasswordEncryptor passwordEncryptor, - SettingsReflectionHelper settingsReflectionHelper, - PluginSettingsFactory pluginSettingsFactory, - RepositoryService repositoryService - ) { + public MirrorRepositoryHook(ConcurrencyService concurrencyService, + PasswordEncryptor passwordEncryptor, + ApplicationPropertiesService propertiesService, + MirrorBucketProcessor pushProcessor, + SettingsReflectionHelper settingsReflectionHelper) { logger.debug("MirrorRepositoryHook: init started"); - // Set fields - this.scmService = scmService; - this.i18nService = i18nService; - this.executor = executor; this.passwordEncryptor = passwordEncryptor; this.settingsReflectionHelper = settingsReflectionHelper; - this.repositoryService = repositoryService; - // Init password encryptor - PluginSettings pluginSettings = pluginSettingsFactory.createSettingsForKey(PLUGIN_SETTINGS_KEY); - passwordEncryptor.init(pluginSettings); + int attempts = propertiesService.getPluginProperty(PROP_ATTEMPTS, 5); + int threads = propertiesService.getPluginProperty(PROP_THREADS, 3); + + pushExecutor = concurrencyService.getBucketedExecutor(getClass().getSimpleName(), + new BucketedExecutorSettings.Builder<>(MirrorRequest::toString, pushProcessor) + .batchSize(Integer.MAX_VALUE) // Coalesce all requests into a single push + .maxAttempts(attempts) + .maxConcurrency(threads, ConcurrencyPolicy.PER_NODE) + .build()); logger.debug("MirrorRepositoryHook: init completed"); } /** - * Calls the remote bitbucket instance(s) to push the latest changes - *

- * Callback method that is called just after a push is completed (or a pull request accepted). - * This hook executes after the processing of a push and will not block the user client. - *

- * Despite being asynchronous, the user who initiated this change is still available from + * Schedules pushes to apply the latest changes to any configured mirrors. * - * @param context the context which the hook is being run with - * @param refChanges the refs that have just been updated + * @param context provides any settings which have been configured for the hook + * @param request describes the push */ @Override - public void postReceive( - @Nonnull RepositoryHookContext context, - @Nonnull Collection refChanges) { - - logger.debug("MirrorRepositoryHook: postReceive started."); - - List mirrorSettings = getMirrorSettings(context.getSettings()); - - for (MirrorSettings settings : mirrorSettings) { - runMirrorCommand(settings, context.getRepository()); - } - - } - - void runMirrorCommand(MirrorSettings settings, final Repository repository) { - if (repositoryService.isEmpty(repository)) { + public void postUpdate(@Nonnull PostRepositoryHookContext context, + @Nonnull RepositoryPushHookRequest request) { + Repository repository = request.getRepository(); + if (!GitScm.ID.equalsIgnoreCase(repository.getScmId())) { return; } - try { - final String password = passwordEncryptor.decrypt(settings.password); - final String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password); - - executor.submit(new Runnable() { - - int attempts = 0; - - @Override - public void run() { - try { - ScmCommandBuilder obj = scmService.createBuilder(repository); - if (!(obj instanceof GitScmCommandBuilder)) { - logger.warn("Repository " + repository.getName() + " is not a git repo, cannot mirror"); - return; - } - GitScmCommandBuilder builder = (GitScmCommandBuilder) obj; - PasswordHandler passwordHandler = getPasswordHandler(builder, password); - - // Call push command with the prune flag and refspecs for heads and tags - // Do not use the mirror flag as pull-request refs are included - builder.command("push") - .argument("--prune") // this deletes locally deleted branches - .argument(authenticatedUrl) - .argument("--force"); - - // Use an atomic transaction to have a consistent state - if (settings.atomic) { - builder.argument("--atomic"); - } - - // Add refspec args - String refspecs = Strings.isNullOrEmpty(settings.refspec) ? DEFAULT_REFSPEC : settings.refspec; - for (String refspec : refspecs.split("\\s|\\n")) { - if (!Strings.isNullOrEmpty(refspec)) { - builder.argument(refspec); - } - } - - // Add tags refspec - if (settings.tags) { - builder.argument("+refs/tags/*:refs/tags/*"); - } - // Add notes refspec - if (settings.notes) { - builder.argument("+refs/notes/*:refs/notes/*"); - } - - builder.errorHandler(passwordHandler) - .exitHandler(passwordHandler); - - String result = builder.build(passwordHandler).call(); - - logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result); - - } catch (Exception e) { - if (++attempts >= MAX_ATTEMPTS) { - logger.error("Failed to mirror repository " + repository.getName() + " after " + attempts - + " attempts.", e); - } else { - logger.warn("Failed to mirror repository " + repository.getName() + ", " + - "retrying in 1 minute (attempt {} of {}).", attempts, MAX_ATTEMPTS); - executor.schedule(this, 1, TimeUnit.MINUTES); - } - } - - } - }); - - } catch (Exception e) { - logger.error("MirrorRepositoryHook: Error running mirror hook", e); - } - } - - protected String getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { - - // Only http(s) has username/password - if (!mirrorRepoUrl.toLowerCase().startsWith("http")) { - return mirrorRepoUrl; - } - - URI uri = URI.create(mirrorRepoUrl); - String userInfo = username + ":" + password; - - return new URI(uri.getScheme(), userInfo, uri.getHost(), uri.getPort(), - uri.getPath(), uri.getQuery(), uri.getFragment()).toString(); - + logger.debug("{}: MirrorRepositoryHook: postReceive started.", repository); + getMirrorSettings(context.getSettings()) + .forEach(settings -> pushExecutor.schedule(new MirrorRequest(repository, settings), 5L, TimeUnit.SECONDS)); } /** - * Validate the given {@code settings} before they are persisted. + * Validates hook settings before they are persisted, and encrypts any user-supplied password. * - * @param settings to be validated - * @param errors callback for reporting validation errors. - * @param repository the context {@code Repository} the settings will be associated with + * @param settings to be validated + * @param errors callback for reporting validation errors + * @param scope the scope for which the hook has been configured */ @Override - public void validate( - @Nonnull Settings settings, - @Nonnull SettingsValidationErrors errors, - @Nonnull Repository repository) { + public void validate(@Nonnull Settings settings, @Nonnull SettingsValidationErrors errors, @Nonnull Scope scope) { + Repository repository = scope.accept(new ScopeVisitor() { + + @Override + public Repository visit(@Nonnull RepositoryScope scope) { + return scope.getRepository(); + } + }); + if (repository == null) { + return; + } try { boolean ok = true; logger.debug("MirrorRepositoryHook: validate started."); List mirrorSettings = getMirrorSettings(settings, false, false, false); - for (MirrorSettings ms : mirrorSettings) { - if (!validate(ms, settings, errors)) { + if (!validate(ms, errors)) { ok = false; } } @@ -235,28 +122,23 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep // If no errors, run the mirror command if (ok) { updateSettings(mirrorSettings, settings); - for (MirrorSettings ms : mirrorSettings) { - runMirrorCommand(ms, repository); - } + mirrorSettings.forEach(ms -> pushExecutor.schedule(new MirrorRequest(repository, ms), 5L, TimeUnit.SECONDS)); } - } catch (Exception e) { logger.error("Error running MirrorRepositoryHook validate.", e); errors.addFormError(e.getMessage()); } - } - protected List getMirrorSettings(Settings settings) { + private List getMirrorSettings(Settings settings) { return getMirrorSettings(settings, true, true, true); } - protected List getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) { - - List results = new ArrayList<>(); + private List getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) { Map allSettings = settings.asMap(); int count = 0; + List results = new ArrayList<>(); for (String key : allSettings.keySet()) { if (key.startsWith(SETTING_MIRROR_REPO_URL)) { String suffix = key.substring(SETTING_MIRROR_REPO_URL.length()); @@ -278,8 +160,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep return results; } - protected boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) { - + private boolean validate(MirrorSettings ms, SettingsValidationErrors errors) { boolean result = true; boolean isHttp = false; @@ -331,11 +212,8 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep return result; } - protected void updateSettings(List mirrorSettings, Settings settings) { - - Map values = new HashMap(); - - // Store each mirror setting + private void updateSettings(List mirrorSettings, Settings settings) { + Map values = new HashMap<>(); for (MirrorSettings ms : mirrorSettings) { values.put(SETTING_MIRROR_REPO_URL + ms.suffix, ms.mirrorRepoUrl); values.put(SETTING_USERNAME + ms.suffix, ms.username); @@ -348,23 +226,5 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep // Unfortunately the settings are stored in an immutable map, so need to cheat with reflection settingsReflectionHelper.set(values, settings); - } - - protected PasswordHandler getPasswordHandler(GitScmCommandBuilder builder, String password) { - - try { - Method method = builder.getClass().getDeclaredMethod("createExitHandler"); - method.setAccessible(true); - CommandExitHandler exitHandler = (CommandExitHandler) method.invoke(builder); - - return new PasswordHandler(password, exitHandler); - - } catch (Throwable t) { - logger.warn("Unable to create exit handler", t); - } - - return new PasswordHandler(password, new DefaultCommandExitHandler(i18nService)); - } - } diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRequest.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRequest.java new file mode 100644 index 0000000..3fe558f --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRequest.java @@ -0,0 +1,33 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.repository.Repository; + +import java.io.Serializable; + +class MirrorRequest implements Serializable { + + private final int repositoryId; + private final MirrorSettings settings; + + MirrorRequest(Repository repository, MirrorSettings settings) { + this(repository.getId(), settings); + } + + MirrorRequest(int repositoryId, MirrorSettings settings) { + this.repositoryId = repositoryId; + this.settings = settings; + } + + int getRepositoryId() { + return repositoryId; + } + + MirrorSettings getSettings() { + return settings; + } + + @Override + public String toString() { + return repositoryId + ":" + settings.mirrorRepoUrl; + } +} diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java new file mode 100644 index 0000000..7b873a5 --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java @@ -0,0 +1,15 @@ +package com.englishtown.bitbucket.hook; + +import java.io.Serializable; + +class MirrorSettings implements Serializable { + + String mirrorRepoUrl; + String username; + String password; + String suffix; + String refspec; + boolean tags; + boolean notes; + boolean atomic; +} diff --git a/src/main/java/com/englishtown/bitbucket/hook/PasswordEncryptor.java b/src/main/java/com/englishtown/bitbucket/hook/PasswordEncryptor.java index 326e727..ea8b855 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/PasswordEncryptor.java +++ b/src/main/java/com/englishtown/bitbucket/hook/PasswordEncryptor.java @@ -1,19 +1,10 @@ package com.englishtown.bitbucket.hook; -import com.atlassian.sal.api.pluginsettings.PluginSettings; - /** * Service to encrypt/decrypt git user passwords */ public interface PasswordEncryptor { - /** - * Initialize the password encryptor with the atlassian plugin settings - * - * @param pluginSettings the plugin settings - */ - void init(PluginSettings pluginSettings); - /** * Checks whether the password is encrypted * diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index 377d157..dee3f5b 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -1,33 +1,34 @@ - ${project.description} ${project.version} + true execute_java + + - + - - com.englishtown.bitbucket.hook.PasswordEncryptor - - - com.englishtown.bitbucket.hook.SettingsReflectionHelper - + + + + + key="mirror-repository-hook" class="bean:mirrorRepositoryHook" + configurable="true"> Mirror Hook icons/mirror-icon.png diff --git a/src/test/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptorTest.java b/src/test/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptorTest.java index 9de97ca..b1c4b5a 100644 --- a/src/test/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptorTest.java +++ b/src/test/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptorTest.java @@ -1,6 +1,7 @@ package com.englishtown.bitbucket.hook; import com.atlassian.sal.api.pluginsettings.PluginSettings; +import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -9,7 +10,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; /** * DefaultPasswordEncryptor unit tests @@ -23,43 +24,22 @@ public class DefaultPasswordEncryptorTest { @Mock private PluginSettings pluginSettings; + @Mock + private PluginSettingsFactory pluginSettingsFactory; private DefaultPasswordEncryptor encryptor; @Before - public void setUp() throws Exception { - - when(pluginSettings.get(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY)).thenReturn(CRYPTO_KEY); - - encryptor = new DefaultPasswordEncryptor(); - encryptor.init(pluginSettings); - - } - - @Test - public void testInit() throws Exception { - - DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor(); - PluginSettings pluginSettings = mock(PluginSettings.class); - encryptor.init(pluginSettings); - - verify(pluginSettings).put(eq(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY), anyString()); - + public void setUp() { + when(pluginSettingsFactory.createSettingsForKey(DefaultPasswordEncryptor.PLUGIN_SETTINGS_KEY)) + .thenReturn(pluginSettings); when(pluginSettings.get(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY)).thenReturn(CRYPTO_KEY); - encryptor.init(pluginSettings); - - // Verify put hasn't been called again - verify(pluginSettings).put(eq(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY), anyString()); - + encryptor = new DefaultPasswordEncryptor(pluginSettingsFactory); } @Test - public void testRunCipher() throws Exception { - - DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor(); - encryptor.init(pluginSettings); - + public void testRunCipher() { String clearText = "clear text"; byte[] clearData = clearText.getBytes(); byte[] encryptedData; @@ -73,60 +53,33 @@ public class DefaultPasswordEncryptorTest { assertArrayEquals(clearData, resultData); assertEquals(clearText, resultText); - } @Test - public void testIsEncrypted() throws Exception { - - DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor(); - String password = "clear-text-key"; - boolean result; - - result = encryptor.isEncrypted(password); - assertFalse(result); - - password = null; - - result = encryptor.isEncrypted(password); - assertFalse(result); - - password = ""; - - result = encryptor.isEncrypted(password); - assertFalse(result); - - password = DefaultPasswordEncryptor.ENCRYPTED_PREFIX + "encrypted-key"; - - result = encryptor.isEncrypted(password); - assertTrue(result); + public void testIsEncrypted() { + assertFalse(encryptor.isEncrypted("clear-text-key")); + assertFalse(encryptor.isEncrypted(null)); + assertFalse(encryptor.isEncrypted("")); + assertTrue(encryptor.isEncrypted(DefaultPasswordEncryptor.ENCRYPTED_PREFIX + "encrypted-key")); } @Test - public void testEncrypt() throws Exception { - - String password = "test"; - String encrypted; - String clear; - String result; + public void testEncrypt() { + assertFalse(encryptor.isEncrypted("test")); - assertFalse(encryptor.isEncrypted(password)); - - encrypted = encryptor.encrypt(password); + String encrypted = encryptor.encrypt("test"); assertTrue(encryptor.isEncrypted(encrypted)); - result = encryptor.encrypt(encrypted); - assertEquals(encrypted, result); - - clear = encryptor.decrypt(encrypted); - assertEquals(password, clear); + String reencrypted = encryptor.encrypt(encrypted); + assertEquals(encrypted, reencrypted); - assertFalse(encryptor.isEncrypted(clear)); + String decrypted = encryptor.decrypt(encrypted); + assertEquals("test", decrypted); - result = encryptor.decrypt(clear); - assertEquals(clear, result); + assertFalse(encryptor.isEncrypted(decrypted)); + String redecrypted = encryptor.decrypt(decrypted); + assertEquals(decrypted, redecrypted); } - } diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorBucketProcessorTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorBucketProcessorTest.java new file mode 100644 index 0000000..25fb568 --- /dev/null +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorBucketProcessorTest.java @@ -0,0 +1,155 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.i18n.SimpleI18nService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.scm.ScmService; +import com.atlassian.bitbucket.scm.git.command.GitCommand; +import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder; +import com.atlassian.bitbucket.server.ApplicationPropertiesService; +import com.atlassian.bitbucket.user.DummySecurityService; +import com.atlassian.bitbucket.user.SecurityService; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import static com.atlassian.bitbucket.mockito.MockitoUtils.returnArg; +import static com.atlassian.bitbucket.mockito.MockitoUtils.returnFirst; +import static com.atlassian.bitbucket.mockito.MockitoUtils.returnsSelf; +import static com.englishtown.bitbucket.hook.MirrorBucketProcessor.PROP_TIMEOUT; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link MirrorRepositoryHook} + */ +public class MirrorBucketProcessorTest { + + private static final String URL_HTTP = "https://bitbucket-mirror.englishtown.com/scm/test/test.git"; + private static final String URL_SSH = "ssh://git@bitbucket-mirror.englishtown.com/scm/test/test.git"; + + private static final MirrorSettings SETTINGS = new MirrorSettings() { + { + mirrorRepoUrl = URL_SSH; + password = "test-password"; + refspec = "+refs/heads/master:refs/heads/master +refs/heads/develop:refs/heads/develop"; + username = "test-user"; + + atomic = true; + notes = true; + tags = true; + } + }; + private static final MirrorRequest REQUEST = new MirrorRequest(1, SETTINGS); + private static final List REQUESTS = Collections.singletonList(REQUEST); + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule().silent(); + + @Mock + private GitScmCommandBuilder builder; + @Mock + private GitCommand command; + @Spy + private I18nService i18nService = new SimpleI18nService(); + @Mock + private PasswordEncryptor passwordEncryptor; + private MirrorBucketProcessor processor; + @Mock + private ApplicationPropertiesService propertiesService; + @Mock + private Repository repository; + @Mock + private ScmService scmService; + @Mock + private RepositoryService repositoryService; + @Spy + private SecurityService securityService = new DummySecurityService(); + + @Before + public void setup() { + when(builder.command(anyString())).thenAnswer(returnsSelf()); + when(builder.argument(anyString())).thenAnswer(returnsSelf()); + when(builder.errorHandler(any())).thenAnswer(returnsSelf()); + when(builder.exitHandler(any())).thenAnswer(returnsSelf()); + when(builder.build(any())).thenReturn(command); + + when(passwordEncryptor.decrypt(anyString())).thenAnswer(returnFirst()); + when(propertiesService.getPluginProperty(eq(PROP_TIMEOUT), anyLong())).thenAnswer(returnArg(1)); + + doReturn(builder).when(scmService).createBuilder(any()); + + processor = new MirrorBucketProcessor(i18nService, passwordEncryptor, + propertiesService, repositoryService, scmService, securityService); + } + + @Test + public void testProcess() { + when(repositoryService.getById(eq(1))).thenReturn(repository); + + processor.process("ignored", REQUESTS); + + verify(builder).command(eq("push")); + verify(builder).argument(eq("--prune")); + verify(builder).argument(eq("--force")); + verify(builder).argument(eq(URL_SSH)); + verify(builder).argument(eq("--atomic")); + verify(builder).argument(eq("+refs/heads/master:refs/heads/master")); + verify(builder).argument(eq("+refs/heads/develop:refs/heads/develop")); + verify(builder).argument(eq("+refs/tags/*:refs/tags/*")); + verify(builder).argument(eq("+refs/notes/*:refs/notes/*")); + verify(command).call(); + verify(command).setTimeout(eq(Duration.ofSeconds(120L))); + verify(passwordEncryptor).decrypt(eq(SETTINGS.password)); + verify(scmService).createBuilder(same(repository)); + } + + @Test + public void testProcessWithDeletedRepository() { + processor.process("ignored", REQUESTS); + + verify(repositoryService).getById(eq(1)); + verifyNoMoreInteractions(repositoryService); + verifyZeroInteractions(scmService); + } + + @Test + public void testProcessWithEmptyRepository() { + when(repositoryService.getById(eq(1))).thenReturn(repository); + when(repositoryService.isEmpty(same(repository))).thenReturn(true); + + processor.process("ignored", REQUESTS); + + verify(repositoryService).getById(eq(1)); + verify(repositoryService).isEmpty(same(repository)); + verifyZeroInteractions(passwordEncryptor, scmService); + } + + @Test + public void testProcessWithoutRequests() { + processor.process("ignored", Collections.emptyList()); + + verifyZeroInteractions(repositoryService, scmService); + } + + @Test + public void testGetAuthenticatedUrlForHttp() { + String url = processor.getAuthenticatedUrl(URL_HTTP, "user", "password"); + assertEquals("https://user:password@bitbucket-mirror.englishtown.com/scm/test/test.git", url); + } + + @Test + public void testGetAuthenticatedUrlForSsh() { + String url = processor.getAuthenticatedUrl(URL_SSH, "user", "password"); + assertEquals(URL_SSH, url); + } +} diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java index 98a0442..a74fc04 100644 --- a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java @@ -1,35 +1,32 @@ package com.englishtown.bitbucket.hook; -import com.atlassian.bitbucket.hook.repository.RepositoryHookContext; -import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.concurrent.BucketedExecutor; +import com.atlassian.bitbucket.concurrent.ConcurrencyService; +import com.atlassian.bitbucket.hook.repository.PostRepositoryHookContext; +import com.atlassian.bitbucket.hook.repository.RepositoryPushHookRequest; import com.atlassian.bitbucket.repository.Repository; -import com.atlassian.bitbucket.repository.RepositoryService; -import com.atlassian.bitbucket.scm.CommandErrorHandler; -import com.atlassian.bitbucket.scm.CommandExitHandler; -import com.atlassian.bitbucket.scm.CommandOutputHandler; -import com.atlassian.bitbucket.scm.ScmService; -import com.atlassian.bitbucket.scm.git.command.GitCommand; -import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder; +import com.atlassian.bitbucket.scm.git.GitScm; +import com.atlassian.bitbucket.scope.Scope; +import com.atlassian.bitbucket.scope.Scopes; +import com.atlassian.bitbucket.server.ApplicationPropertiesService; import com.atlassian.bitbucket.setting.Settings; import com.atlassian.bitbucket.setting.SettingsValidationErrors; -import com.atlassian.sal.api.pluginsettings.PluginSettings; -import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import static com.atlassian.bitbucket.mockito.MockitoUtils.returnArg; +import static com.englishtown.bitbucket.hook.MirrorRepositoryHook.PROP_ATTEMPTS; +import static com.englishtown.bitbucket.hook.MirrorRepositoryHook.PROP_THREADS; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; @@ -38,151 +35,65 @@ import static org.mockito.Mockito.*; */ public class MirrorRepositoryHookTest { - private MirrorRepositoryHook hook; - private GitScmCommandBuilder builder; - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); + public MockitoRule mockitoRule = MockitoJUnit.rule().silent(); + + private final String mirrorRepoUrlHttp = "https://bitbucket-mirror.englishtown.com/scm/test/test.git"; + private final String mirrorRepoUrlSsh = "ssh://git@bitbucket-mirror.englishtown.com/scm/test/test.git"; + private final String password = "test-password"; + private final String refspec = "+refs/heads/master:refs/heads/master +refs/heads/develop:refs/heads/develop"; + private final String username = "test-user"; @Mock - private ScmService scmService; + private BucketedExecutor bucketedExecutor; @Mock - private GitCommand cmd; + private MirrorBucketProcessor bucketProcessor; @Mock - private ScheduledExecutorService executor; + private ConcurrencyService concurrencyService; + private MirrorRepositoryHook hook; @Mock private PasswordEncryptor passwordEncryptor; @Mock - private SettingsReflectionHelper settingsReflectionHelper; - @Mock - private PluginSettingsFactory pluginSettingsFactory; - @Mock - private PluginSettings pluginSettings; - @Mock - private RepositoryService repositoryService; - - private final String mirrorRepoUrlHttp = "https://bitbucket-mirror.englishtown.com/scm/test/test.git"; - private final String mirrorRepoUrlSsh = "ssh://git@bitbucket-mirror.englishtown.com/scm/test/test.git"; - private final String username = "test-user"; - private final String password = "test-password"; - private final String repository = "https://test-user:test-password@bitbucket-mirror.englishtown.com/scm/test/test.git"; - private final String refspec = "+refs/heads/master:refs/heads/master +refs/heads/develop:refs/heads/develop"; - + private ApplicationPropertiesService propertiesService; @Captor - ArgumentCaptor argumentCaptor; + private ArgumentCaptor requestCaptor; + @Mock + private SettingsReflectionHelper settingsReflectionHelper; @Before public void setup() { + doReturn(bucketedExecutor).when(concurrencyService).getBucketedExecutor(anyString(), any()); - builder = mock(GitScmCommandBuilder.class); - when(builder.command(anyString())).thenReturn(builder); - when(builder.argument(anyString())).thenReturn(builder); - when(builder.errorHandler(any(CommandErrorHandler.class))).thenReturn(builder); - when(builder.exitHandler(any(CommandExitHandler.class))).thenReturn(builder); - when(builder.build(any(CommandOutputHandler.class))).thenReturn(cmd); - - doReturn(builder).when(scmService).createBuilder(any()); - - when(pluginSettingsFactory.createSettingsForKey(anyString())).thenReturn(pluginSettings); - - hook = new MirrorRepositoryHook(scmService, mock(I18nService.class), executor, passwordEncryptor - , settingsReflectionHelper, pluginSettingsFactory, repositoryService); + when(propertiesService.getPluginProperty(eq(PROP_ATTEMPTS), anyInt())).thenAnswer(returnArg(1)); + when(propertiesService.getPluginProperty(eq(PROP_THREADS), anyInt())).thenAnswer(returnArg(1)); + hook = new MirrorRepositoryHook(concurrencyService, passwordEncryptor, + propertiesService, bucketProcessor, settingsReflectionHelper); } @Test - public void testPostReceive() throws Exception { + public void testPostReceive() { when(passwordEncryptor.decrypt(anyString())).thenReturn(password); Repository repo = mock(Repository.class); + when(repo.getId()).thenReturn(1); + when(repo.getScmId()).thenReturn(GitScm.ID); - hook.postReceive(buildContext(repo), new ArrayList<>()); - verifyExecutor(); - } - - @Test - public void testEmptyRepositoriesNotMirrored() { - Repository repo = mock(Repository.class); - when(repositoryService.isEmpty(repo)).thenReturn(true); + hook.postUpdate(buildContext(), new RepositoryPushHookRequest.Builder(repo).build()); - hook.postReceive(buildContext(repo), new ArrayList<>()); + verify(repo).getId(); + verify(repo).getScmId(); + verify(bucketedExecutor).schedule(requestCaptor.capture(), eq(5L), same(TimeUnit.SECONDS)); - verify(executor, never()).submit(ArgumentMatchers.any()); + MirrorRequest request = requestCaptor.getValue(); + assertEquals(1, request.getRepositoryId()); } @Test - public void testRunMirrorCommand_Retries() throws Exception { - - when(scmService.createBuilder(any())).thenThrow(new RuntimeException("Intentional unit test exception")); - MirrorRepositoryHook hook = new MirrorRepositoryHook(scmService, mock(I18nService.class), executor, - passwordEncryptor, settingsReflectionHelper, pluginSettingsFactory, repositoryService); - MirrorRepositoryHook.MirrorSettings ms = new MirrorRepositoryHook.MirrorSettings(); - ms.mirrorRepoUrl = mirrorRepoUrlHttp; - ms.username = username; - ms.password = password; - hook.runMirrorCommand(ms, mock(Repository.class)); - - verify(executor).submit(argumentCaptor.capture()); - Runnable runnable = argumentCaptor.getValue(); - runnable.run(); - - verify(executor, times(1)).schedule(argumentCaptor.capture(), anyLong(), any(TimeUnit.class)); - runnable = argumentCaptor.getValue(); - runnable.run(); - - verify(executor, times(2)).schedule(argumentCaptor.capture(), anyLong(), any(TimeUnit.class)); - runnable = argumentCaptor.getValue(); - runnable.run(); - - verify(executor, times(3)).schedule(argumentCaptor.capture(), anyLong(), any(TimeUnit.class)); - runnable = argumentCaptor.getValue(); - runnable.run(); - - verify(executor, times(4)).schedule(argumentCaptor.capture(), anyLong(), any(TimeUnit.class)); - runnable = argumentCaptor.getValue(); - runnable.run(); - - // Make sure it is only called 5 times - runnable.run(); - runnable.run(); - runnable.run(); - verify(executor, times(4)).schedule(argumentCaptor.capture(), anyLong(), any(TimeUnit.class)); - - } - - private void verifyExecutor() throws Exception { - - verify(executor).submit(argumentCaptor.capture()); - Runnable runnable = argumentCaptor.getValue(); - runnable.run(); - - verify(builder, times(1)).command(eq("push")); - verify(builder, times(1)).argument(eq("--prune")); - verify(builder, times(1)).argument(eq("--atomic")); - verify(builder, times(1)).argument(eq(repository)); - verify(builder, times(1)).argument(eq("--force")); - verify(builder, times(1)).argument(eq("+refs/heads/master:refs/heads/master")); - verify(builder, times(1)).argument(eq("+refs/heads/develop:refs/heads/develop")); - verify(builder, times(1)).argument(eq("+refs/tags/*:refs/tags/*")); - verify(builder, times(1)).argument(eq("+refs/notes/*:refs/notes/*")); - verify(cmd, times(1)).call(); - - } - - @Test - public void testGetAuthenticatedUrl() throws Exception { - - String result = hook.getAuthenticatedUrl(mirrorRepoUrlHttp, username, password); - assertEquals(repository, result); - - } - - @Test - public void testValidate() throws Exception { - + public void testValidate() { Settings settings = mock(Settings.class); - Map map = new HashMap(); + Map map = new HashMap<>(); map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0", ""); when(settings.asMap()).thenReturn(map); @@ -215,15 +126,16 @@ public class MirrorRepositoryHookTest { .thenReturn(""); Repository repo = mock(Repository.class); + Scope scope = Scopes.repository(repo); SettingsValidationErrors errors; errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, times(1)).addFormError(anyString()); verify(errors, never()).addFieldError(anyString(), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), anyString()); @@ -231,54 +143,55 @@ public class MirrorRepositoryHookTest { verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_REFSPEC + "0"), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), anyString()); verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD + "0"), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors, never()).addFieldError(anyString(), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD + "0"), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), anyString()); verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD + "0"), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors, never()).addFieldError(anyString(), anyString()); errors = mock(SettingsValidationErrors.class); - hook.validate(settings, errors, repo); + hook.validate(settings, errors, scope); verify(errors, never()).addFormError(anyString()); verify(errors, never()).addFieldError(anyString(), anyString()); } - private RepositoryHookContext buildContext(Repository repo) { - RepositoryHookContext context = mock(RepositoryHookContext.class); + private PostRepositoryHookContext buildContext() { Settings settings = defaultSettings(); + + PostRepositoryHookContext context = mock(PostRepositoryHookContext.class); when(context.getSettings()).thenReturn(settings); - when(context.getRepository()).thenReturn(repo); + return context; } private Settings defaultSettings() { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL, ""); Settings settings = mock(Settings.class); @@ -290,6 +203,7 @@ public class MirrorRepositoryHookTest { when(settings.getBoolean(eq(MirrorRepositoryHook.SETTING_TAGS), eq(true))).thenReturn(true); when(settings.getBoolean(eq(MirrorRepositoryHook.SETTING_NOTES), eq(true))).thenReturn(true); when(settings.getBoolean(eq(MirrorRepositoryHook.SETTING_ATOMIC), eq(true))).thenReturn(true); + return settings; } }